SpringSecurity自定义登录验证-手机验证码登录
其实实现原理上跟账号密码登录一样的
1、自定义短信验证Token
定义一个仅使用手机号验证权限的鉴权Token,SpringSecurity原生的UsernamePasswordAuthenticationToken
是使用username和password,如下图
principal相当于username,credentials相当于password,所以我们仿照他的写一个跟据手机号鉴权的Token即可:
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.util.Assert;
import java.util.Collection;
/**
* 短信登录令牌
*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object telephone;
/**
* SmsCodeAuthenticationFilter中构建的未认证的Authentication
*
* @param telephone
*/
public SmsAuthenticationToken(Object telephone) {
super(null);
this.telephone = telephone;
this.setAuthenticated(false);
}
/**
* SmsCodeAuthenticationProvider中构建已认证的Authentication
*
* @param telephone
* @param authorities
*/
public SmsAuthenticationToken(Object telephone, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.telephone = telephone;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.telephone;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
2、实现UserDetailsService接口
这一步就是封装权限的操作,实现类似于SpringSecurity的账号密码封装权限,只不过这里调用userService传入的参数是手机号,如果用户存在就返回一个带有权限的 UserDetails
实现类对象(我这里实现类是LoginUser
)
/**
* 查询短信登录信息并封装为 UserDetails 这里可以抽取一个抽象类,权限加载和校验租户等逻辑交给父类处理
*/
@Service("smsUserDetailsService")
public class SmsUserDetailsService implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(SmsUserDetailsService.class);
@Resource
private ISysUserService userService;
@Resource
private SysPermissionService permissionService;
/**
* loadUserByUsername
*
* @param phone
* @return LoginUser
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
SysUser user = userService.getUserByTelephone(phone);
if (StringUtils.isNull(user)) {
log.info("手机号:{} 不存在.", phone);
throw new InternalAuthenticationServiceException("手机号:" + phone + " 不存在");
} else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
log.info("登录用户:{} 已被删除.", phone);
throw new ServiceException("对不起,您的账号:" + phone + " 已被删除");
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", phone);
throw new DisabledException("对不起,您的账号:" + phone + " 已停用");
}
return createLoginUser(user);
}
public UserDetails createLoginUser(SysUser user) {
return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
}
}
3、自定义认证authenticate
第二步返回了带有权限的 LoginUser 对象,在这里需要重写authenticate()方法,调用loadUserByUsername()方法实现身份认证逻辑返回验证Token
/**
* 短信登录校验器
*/
//@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public SmsAuthenticationProvider(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
/**
* 重写 authenticate方法,实现身份验证逻辑。
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
String telephone = (String) authenticationToken.getPrincipal();// 获取凭证也就是用户的手机号
// 根据手机号查询用户信息UserDetails
UserDetails userDetails = userDetailsService.loadUserByUsername(telephone);
if (StringUtil.isEmpty(userDetails)) {
throw new InternalAuthenticationServiceException("用户不存在");
}
// 鉴权成功,返回一个拥有鉴权的 AbstractAuthenticationToken
SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());
smsAuthenticationToken.setDetails(authenticationToken.getDetails());
return smsAuthenticationToken;
}
/**
* 重写supports方法,指定此 AuthenticationProvider 仅支持短信验证码身份验证。
*
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
4、SecurityConfig
这里既使用用户名密码登录也使用手机短信登录,所以UserDetailsService自定义的手机短信实现类加个@Qualifier
注解防止注入失败,UserDetailsService实现类分别指定别名按别名注入:
// 分别去你的实现类里配置
// 账号密码登录
@Service("userDetailsServiceImpl")
public class UserDetailsServiceImpl implements UserDetailsService {}
@Service("smsUserDetailsService")
// 自定义的手机短信登录
public class SmsUserDetailsService implements UserDetailsService {}
在配置文件中添加自定义的手机短信认证,并放行登录接口。其他配置已省略。
/**
* spring security配置
*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义用户认证逻辑
*/
@Resource
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
/**
* 自定义手机短信登录
*/
@Resource
@Qualifier("smsUserDetailsService")
private UserDetailsService smsUserDetailsService;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// 添加手机号短信登录
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/sms-login").anonymous()
// 。。。。。。
}
/**
* 身份认证接口,添加自定义的手机短信认证
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new SmsAuthenticationProvider(smsUserDetailsService));
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
5、验证接口
既然是手机短信登录,这里申请aliyun短信服务或别的企业的短信服务就忽略了,直接使用固定短信验证码+redis的方式。
写一个接口跟据手机号发送验证码,并将验证码存入redis,再响应给前端:
/**
* 短信发送
*
* @param phone
* @return
*/
@GetMapping("/sendCode/{phone}")
public AjaxResult sendCode(@PathVariable("phone") String phone) {
SmsCode smsCode = aliyunSmsService.sendCode(phone);
return AjaxResult.success(smsCode.getCode());
}
此处aliyunSmsService.sendCode(phone)
逻辑可以自行百度aliyun短信服务。
登录接口:
/**
* 手机验证码登录方法
*
* @param smsLoginBody
* @return 结果
*/
@PostMapping("/sms-login")
public AjaxResult smsLogin(@RequestBody SmsLoginBody smsLoginBody) {
// 生成令牌
log.info("手机验证码登录:{}",smsLoginBody.getTelephone());
String token = loginService.smsLogin(smsLoginBody.getTelephone(), smsLoginBody.getCode());
return AjaxResult.success().put(Constants.TOKEN, token);
}
方法实现:
/**
* 手机验证码登录
*
* @param telephone
* @param code
* @return
*/
public String smsLogin(String telephone, String code) {
// 未携带手机号或验证码
if (StringUtil.isEmpty(telephone)) {
throw new TelePhoneException();
}
if (StringUtil.isEmpty(code)) {
throw new CaptchaException();
}
// 获取手机验证码
String verifyKey = CacheConstants.ALIYUN_SMS_KEY + telephone;
String phoneCode = redisTemplate.opsForValue().get(verifyKey);
if (StringUtil.isEmpty(phoneCode)) {
throw new SmsException("验证码已失效");
}
if (!phoneCode.equals(code)) {
throw new SmsException("验证码错误");
}
// 删除key
redisTemplate.delete(verifyKey);
// 通过手机号获取用户
SysUser userByTelephone = userService.getUserByTelephone(telephone);
if (StringUtil.isEmpty(userByTelephone)) {
throw new TelePhoneException();
}
// 用户验证
Authentication authentication = null;
String username = userByTelephone.getUserName();
try {
SmsAuthenticationToken authenticationToken = new SmsAuthenticationToken(telephone);
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用 SmsUserDetailsService.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
} catch (Exception e) {
if (e instanceof BadCredentialsException) {
// 异步记录日志
AsyncManager.me().execute(AsyncFactory.recordLogininfor(telephone, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
} else {
AsyncManager.me().execute(AsyncFactory.recordLogininfor(telephone, Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage());
}
} finally {
AuthenticationContextHolder.clearContext();
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(telephone, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 记录登录信息,修改用户表,添加登录IP、登录时间
recordLoginInfo(loginUser.getUserId());
// 生成token
return tokenService.createToken(loginUser);
}
如果是若依系统,记得所有用户要有role角色。