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);
}
... ...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。