SpringSecurity在UserDetailService抛异常
背景
所有的项目都会涉及到账户安全问题,会对账号设置密码或者短信验证码或者其他的三方授权,用户才能登录到系统中。
而做得比较完善的框架为数不对,SpringSecurity是目前比较主流的认证鉴权框架。Oauth2是比较完善的认证协议。两者结合便出现了SpringSecurityOAuth2.本篇将分享在使用这套框架的时候后关于异常怎么抛出的问题。
使用UserDetailService
使用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());
R<UserInfo> r1 = remoteUserService.registerByPhone(registerWxUserDTO);
if (r1.isFaild()) {
throw new LoginException(r1.getMsg());
}
UserInfo thirdUser = r1.getData();
thirdUser.getSysUser().setPassword(NOOP + "marker");
return getUserDetails(thirdUser);
}
}
这段代码有几处地方抛出了异常,而这些异常在SpringSecurity框架下经过层层转换,到你真真需要拦截的时候,发现拦截不了了。
为什么UserDetailService抛出的异常不能拦截到
来看看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 在把原始的异常拿出来包装返回对象。
使用WebResponseExceptionTranslator来解救异常
这段代码的意思就是提取原始的异常信息,然后判断,然后再转换为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 ResponseEntity<OAuth2Exception> translate(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 ResponseEntity<OAuth2Exception> handleOAuth2Exception(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 ResponseEntity<OAuth2Exception> handleRException(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));
}
}