SpringSecurity登錄流程
在若依平台上的賬號密碼登錄流程如下:
SysLoginController.login()--->loginService.login()--->authenticationManager.authenticate(){
--->daoAuthenticationProvider.authenticate()--->userDetailsService.loadUserByUsername()
} ---> 根據用户信息創建token返回
其中:
- AuthenticationManager、DaoAuthenticationProvider、UserDetailsService的綁定關係在SecurityConfig中實現
- AuthenticationManager具體實現是ProvideManager,其裏面有一個List<AuthenticationProvider>,在調用authenticate方法時,會for循環調用 AuthenticationProvider的authenticate方法,如果ProvideManager能處理,那麼for循環終止,如果不能處理變回繼續調用下一個。AuthenticationProvider通過supports(...)判端是支持某種方式認證
- DaoAuthenticationProvider是SpringSecurity實現的一個根據賬號密碼認證的Provider,其會調用UserDetailsService的loadUserByUsername去查詢用户的詳細信息,包括權限,因此我們一般需要實現該接口,根據用户和密碼實現查詢用户的功能。
- authenticationManager.authenticate(Authentication authentication),其中UsernamePasswordAuthenticationToken是SpringSecurity實現的通過賬號和密碼登錄的Authentication
手機號和驗證碼登錄流程
我們需要仿照賬號密碼登錄流程,實現短信密碼登錄流程,因此我們需要自定義實現:
- SysLoginController.smsLogin() 手機驗證碼登錄接口入口
- loginService.smsLogin() 手機號登錄業務實現
- SmsAuthenticationToken,自定義,繼承AbstractAuthenticationToken,AuthenticationManager認證時的參數
- SmsAuthProvider,自定義,繼承AuthenticationProvider,實現認證功能,主要是檢查手機驗證碼是否正確
- UserDetailsServiceImpl新增一個方法,根據手機號查詢用户的詳細信息
- 當然發送驗證碼、從redis中獲取驗證碼判斷是否正確這裏不再詳細描述
手機號驗證碼登錄實現
1.接口入口
@PostMapping("/smsLogin")
public AjaxResult loginWithSmsCode(@RequestBody @Valid SmsLoginParam loginParam) {
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.smsLogin(loginParam.getPhone(),loginParam.getCode());
ajax.put(Constants.TOKEN, token);
return ajax;
}
2.loginService.smsLogin()業務實現
/**
* 短信登錄
*
* @param phone 手機號
* @param smsCode 手機驗證碼
* @return 結果
*/
public String smsLogin(String phone,String smsCode) {
// 驗證碼校驗
Authentication authentication = null;
try {
//通過手機號查詢出密碼
SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(phone, smsCode);
// 該方法會去調用UserDetailsServiceImpl.loadUserByPhoneNumber
authentication = authenticationManager.authenticate(smsAuthenticationToken);
} catch (Exception e) {
if (e instanceof BadCredentialsException) {
AsyncManager.me().execute(AsyncFactory.recordLogininfor(phone, ServletUtils.getTenantId(),Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage());
} else {
AsyncManager.me().execute(AsyncFactory.recordLogininfor(phone,ServletUtils.getTenantId(), Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage());
}
} finally {
AuthenticationContextHolder.clearContext();
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(phone, ServletUtils.getTenantId(),Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUserId());
// 生成token
return tokenService.createToken(loginUser);
}
3.SmsAuthenticationToken實現
/**
* @author ljq
* @date 2024/11/12
* @description 實現短信驗證碼的憑證,類似UsernamePasswordAuthenticationToken
*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* 手機號
*/
private final Object principal;
/**
* 驗證碼
*/
private Object credentials;
public SmsAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public SmsAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
// must use super, as we override
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
4.SmsAuthProvider實現
/**
* @author ljq
* @date 2024/11/12
* @description TODO
*/
public class SmsAuthProvider implements AuthenticationProvider {
private final SmsUserDetailsService smsUserDetailsService;
private final SmsCaptchaRedisCache smsCaptchaRedisCache;
public SmsAuthProvider(SmsUserDetailsService smsUserDetailsService, SmsCaptchaRedisCache smsCaptchaRedisCache) {
this.smsUserDetailsService = smsUserDetailsService;
this.smsCaptchaRedisCache = smsCaptchaRedisCache;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String phone = (String) authentication.getPrincipal();
String smsCode = (String) authentication.getCredentials();
if (!smsCaptchaRedisCache.isExistCaptcha(phone)) {
throw new BadCredentialsException("驗證碼已過期");
}
if (!smsCaptchaRedisCache.checkCaptcha(phone, smsCode)) {
throw new BadCredentialsException("驗證碼錯誤");
}
UserDetails userDetails = smsUserDetailsService.loadUserByPhoneNumber(phone);
return new SmsAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}
5.UserDetailsServiceImpl實現根據手機號查詢用户詳情
@Service
public class UserDetailsServiceImpl implements UserDetailsService, SmsUserDetailsService {
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private ISysUserService userService;
@Autowired
private SysPasswordService passwordService;
@Autowired
private SysPermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
... ...
return createLoginUser(user);
}
@Override
public UserDetails loadUserByPhoneNumber(String phoneNumber) throws UsernameNotFoundException {
String tenantId = ServletUtils.getHeader(HEADER_KEY_TENANT);
SysUser user = userService.selectUserByPhoneNumber(phoneNumber,tenantId);
if (StringUtils.isNull(user)) {
log.info("登錄用户電話:{} 不存在.", phoneNumber);
//throw new ServiceException(MessageUtils.message("user.not.exists"));
throw new ServiceException("電話不存在");
} else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
log.info("登錄用户電話:{} 已被刪除.", phoneNumber);
throw new ServiceException(MessageUtils.message("user.password.delete"));
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登錄用户電話:{} 已被停用.", phoneNumber);
throw new ServiceException(MessageUtils.message("user.blocked"));
}
return createLoginUser(user);
}
public UserDetails createLoginUser(SysUser user) {
return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
}
}
其中SmsUserDetailsService為新定義的一個接口,裏面聲明瞭loadUserByPhoneNumber方法
6.在SecurityConfig中AuthenticationManager、SmsAuthProvider、SmsUserDetailsService關係綁定
... ...
@Bean
public AuthenticationManager authenticationManager() {
//賬號密碼登錄校驗
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
//短信登錄校驗,UserDetailsServiceImpl 也實現了SmsUserDetailsService,所以可以直接強轉
SmsAuthProvider smsAuthProvider = new SmsAuthProvider((SmsUserDetailsService) userDetailsService, smsCaptchaRedisCache);
return new ProviderManager(daoAuthenticationProvider,smsAuthProvider);
}
... ...