所有的项目都会涉及到账户安全问题,会对账号设置密码或者短信验证码或者其他的三方授权,用户才能登录到系统中。 而做得比较完善的框架为数不对,SpringSecurity是目前比较主流的认证鉴权框架。Oauth2是比较完善的认证协议。两者结合便出现了SpringSecurityOAuth2.本篇将分享在使用这套框架的时候后关于异常怎么抛出的问题。
使用SpringSecurity我们就知道需要定义UserDetailService来通过username拉去用户的信息(账号、密码、锁定状态、过期状态等等)。而这套做法主要真多password模式的。当我们需要使用自定义的模式的时候,验证逻辑只有写在UserdetailService的内部。 例如下边代码:
package com.xxx.xxx.auth.grant.mobile; /** * 手机验证码登陆, 用户相关获取 * (主要用于会员端的手机验证码登录) * * @author marker */ @Slf4j @Service("mobileUserDetailsService") public class MobileUserDetailsService extends MyUserDetailsServiceAdapter implements MyUserDetailsService { @Resource private RemoteUserService remoteUserService; @Resource private RedisTemplate redisTemplate; @Resource private LoginCountService loginCountService; @Resource private SystemAuthConfig systemAuthConfig; @Override @Transactional public UserDetails loadUserByUsername(String phone) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); String platform = request.getHeader(CommonHeaders.PLATFORM);// 平台 String code = (String) request.getAttribute("code");// 前端传递的验证码 String codeType = (String) request.getAttribute("codeType");// 短信验证类型 String source = (String) request.getAttribute("source"); // 会员来源(来自门店服务的member_source) String countryCode = (String) request.getAttribute("countryCode"); // 国家区号 不含+ // 获取redis短信验证码 String key = String.format(KeyFormat.SMS_CODE_FORMAT, codeType, countryCode, phone); String val = (String) redisTemplate.opsForValue().get(key); // 判断是否开启了ios审核模式 if (systemAuthConfig.getAuditEnable()) { String auditPhone = systemAuthConfig.getAuditPhone(); if (StringUtil.isNotBlank(auditPhone) && auditPhone.equals(phone)) { val = systemAuthConfig.getAuditPhoneCode(); } } if (!code.equals(val)) { // 校验验证码是否正确 // 登录失败次数+1 loginCountService.countPlus1(phone); int count = loginCountService.getRemainLoginCount(phone); log.warn("登录失败,还可登录{}次", count); throw new LoginException("验证码不正确"); } redisTemplate.delete(key); // 验证完成后删除短信验证码 String userInfo = (String) request.getAttribute("userInfo");// 用户信息 if (StringUtil.isBlank(userInfo)) { userInfo = "{}"; } UserInfoExt wxUserInfo = JSON.parseObject(userInfo, UserInfoExt.class); // 如果Type不等于绑定信息 RegisterWxUserDTO registerWxUserDTO = new RegisterWxUserDTO(); registerWxUserDTO.setCountryCode(countryCode); registerWxUserDTO.setPhone(phone); registerWxUserDTO.setNickname(wxUserInfo.getNickname()); registerWxUserDTO.setAvatar(wxUserInfo.getHeadimgurl()); registerWxUserDTO.setOpenid(wxUserInfo.getOpenid()); registerWxUserDTO.setUnionid(wxUserInfo.getUnionid()); registerWxUserDTO.setPlatform(platform); registerWxUserDTO.setSource(source); // 用于判断把用户信息绑定到对应的openid里 registerWxUserDTO.setType(wxUserInfo.getOauthType()); Rr1 = remoteUserService.registerByPhone(registerWxUserDTO); if (r1.isFaild()) { throw new LoginException(r1.getMsg()); } UserInfo thirdUser = r1.getData(); thirdUser.getSysUser().setPassword(NOOP + "marker"); return getUserDetails(thirdUser); } }
这段代码有几处地方抛出了异常,而这些异常在SpringSecurity框架下经过层层转换,到你真真需要拦截的时候,发现拦截不了了。
来看看SpringSecurity的源代码 在DaoAuthenticationProvider.class
类中,找到这个retrieveUser
方法
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication);
这个方法主要就是调用userdetailService.loadUserByUsername 方法,加载用户的信息,包含账号密码、角色、权限等等信息 来看看他的实现代码,不得不吐槽一下为啥转换跑出来的异常。。。
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
他我们自定义的异常做了一层转换,导致我们的异常不能按照我们的思路去捕获。 那怎么解决呢,这段代码hi框架层提供了的,我们如果改他的源代码会比较麻烦,或者是开源项目提交一段优化代码,但别人的设计也可能有他设计的道理。所以在全局拦截器中拦截 InternalAuthenticationServiceException 在把原始的异常拿出来包装返回对象。
这段代码的意思就是提取原始的异常信息,然后判断,然后再转换为R对象返回给前端。 当然这个包装的异常提供了序列化的机制,能够自动转换为R对象。
package com.xxx.xxx.common.security.component; /** * @author system * OAuth Server 异常处理,重写oauth 默认实现 */ @Slf4j public class MyWebResponseExceptionTranslator implements WebResponseExceptionTranslator { private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); protected MessageSourceAccessor messages = SecurityMessageSourceUtil.getAccessor(); @Override public ResponseEntitytranslate(Exception e) { // Try to extract a SpringSecurityException from the stacktrace Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e); Exception ase = (AuthenticationException) throwableAnalyzer .getFirstThrowableOfType(AuthenticationException.class, causeChain); // LoginException 处理 ase = (LoginException) throwableAnalyzer.getFirstThrowableOfType(LoginException.class, causeChain); if (ase != null) {// return handleRException(new RException(((LoginException) ase).getResult())); } // RException 处理 ase = (RException) throwableAnalyzer.getFirstThrowableOfType(RException.class, causeChain); if (ase != null) {// return handleRException((RException)ase); } // OAuth2Exception ase = (ClientException) throwableAnalyzer.getFirstThrowableOfType(ClientException.class, causeChain); if (ase != null) { // 如果是oauth2的异常,读取自定义message配置 String oAuth2ErrorCode = ((ClientException) ase).getErrorMessage(); String msg = messages.getMessage( "springSecurityOauth2."+oAuth2ErrorCode, ase.getMessage(), Locale.CHINA); return handleOAuth2Exception(OAuth2Exception.create(oAuth2ErrorCode, msg)); } return handleOAuth2Exception(new ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e)); } private ResponseEntityhandleOAuth2Exception(OAuth2Exception e) { int status = e.getHttpErrorCode(); HttpHeaders headers = new HttpHeaders(); headers.set(HttpHeaders.CACHE_CONTROL, "no-store"); headers.set(HttpHeaders.PRAGMA, "no-cache"); headers.set(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.toString()); // 客户端异常直接返回客户端,不然无法解析 if (e instanceof ClientAuthenticationException) { return new ResponseEntity<>(e, headers, HttpStatus.valueOf(status)); } return new ResponseEntity<>(new MyAuth2Exception(e.getMessage(), e.getOAuth2ErrorCode()), headers, HttpStatus.valueOf(status)); } private ResponseEntityhandleRException(RException e) { HttpHeaders headers = new HttpHeaders(); headers.set(HttpHeaders.CACHE_CONTROL, "no-store"); headers.set(HttpHeaders.PRAGMA, "no-cache"); headers.set(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.toString()); MyAuth2Exception exception = new MyAuth2Exception(e.getResult()); // 客户端异常 return new ResponseEntity<>(exception, headers, HttpStatus.valueOf(200)); } }