Spring boot整合Spring Security实现验证码登陆
验证码登陆在日常使用软件中是很常见的,甚至可以说超过了密码登陆。
如何通过Spring Security框架实现验证码登陆,并且登陆成功之后也同样返回和密码登陆类似的token?
- 先看一张Spring Security拦截请求的流程图
可以发现Spring Security默认有用户名密码登陆拦截器,查看 UsernamePasswordAuthenticationFilter 实现了 AbstractAuthenticationProcessingFilter类 。根据UsernamePasswordAuthenticationFilter的设计模式可以在Spring security的基础上拓展自己的拦截器,实现相应的功能。
- 新增一个 SmsCodeAuthenticationToken 实体类,需要继承IrhAuthenticationToken,而IrhAuthenticationToken继承了 AbstractAuthenticationToken 用来封装验证码登陆时需要的信息,并且自定义一个AuthenticationToken的父类的作用就是用来在管理在具有多种登陆方式的系统中,能记录最核心的两个信息,一个是用户身份,一个是登陆口令;并且在后期能直接向上转型而不报错。
package top.imuster.auth.config; import org.springframework.security.core.GrantedAuthority; import java.util.Collection; /** * @ClassName: SmsCodeAuthenticationToken * @Description: 验证码登录验证信息封装类 * @author: hmr * @date: 2020/4/30 13:59 */ public class SmsCodeAuthenticationToken extends IrhAuthenticationToken { public SmsCodeAuthenticationToken(Object principal, Object credentials) { super(principal, credentials); } public SmsCodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(principal, credentials, authorities); } }
package top.imuster.auth.config; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import java.util.Collection; /** * @Author hmr * @Description 自定义AbstractAuthenticationToken * @Date: 2020/5/1 13:51 * @param irh平台的认证实体类 可以通过继承该类来实现不同的登录逻辑 * @reture: **/ public class IrhAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 110L; //用户信息 protected final Object principal; //密码或者邮箱验证码 protected Object credentials; /** * This constructor can be safely used by any code that wishes to create a * <code>UsernamePasswordAuthenticationToken</code>, as the {@link * #isAuthenticated()} will return <code>false</code>. * */ public IrhAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; this.setAuthenticated(false); } /** * This constructor should only be used by <code>AuthenticationManager</code> or <code>AuthenticationProvider</code> * implementations that are satisfied with producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>) * token token. * * @param principal * @param credentials * @param authorities */ public IrhAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } @Override public Object getCredentials() { return this.credentials; } @Override public Object getPrincipal() { return this.principal; } public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { if(isAuthenticated) { throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); } else { super.setAuthenticated(false); } } public void eraseCredentials() { super.eraseCredentials(); this.credentials = null; } }
- 继承AbstractAuthenticationProcessingFilter之后就可以将自定义的Filter假如到过滤器链中。所以自定义一个 SmsAuthenticationFilter 类并且继承 AbstractAuthenticationProcessingFilter 类,并且该Filter中只校验输入参数是否正确,如果不完整,则抛出 AuthenticationServiceException异常,如果抛出自定义异常,会被Security框架处理。用原生异常可以设置异常信息,直接返回给前端。
package top.imuster.auth.component.login; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import top.imuster.auth.config.SecurityConstants; import top.imuster.auth.config.SmsCodeAuthenticationToken; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @ClassName: SmsCodeAuthenticationFilter * @Description: 自定义拦截器,拦截登录请求中的登录类型 * @author: hmr * @date: 2020/4/30 12:16 */ public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private static final Logger log = LoggerFactory.getLogger(SmsCodeAuthenticationFilter.class); private static final String POST = "post"; private boolean postOnly = true; public SmsCodeAuthenticationFilter(){ super(new AntPathRequestMatcher("/emailCodeLogin", "POST")); } @Autowired AuthenticationManager authenticationManager; @Override @Autowired public void setAuthenticationManager(AuthenticationManager authenticationManager) { super.setAuthenticationManager(authenticationManager); } @Override public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException { if(postOnly && !POST.equalsIgnoreCase(httpServletRequest.getMethod())){ throw new AuthenticationServiceException("不允许{}这种的请求方式: " + httpServletRequest.getMethod()); } //邮箱地址 String loginName = obtainParameter(httpServletRequest, SecurityConstants.LOGIN_PARAM_NAME); //验证码 String credentials = obtainParameter(httpServletRequest, SecurityConstants.EMAIL_VERIFY_CODE); loginName = loginName.trim(); if(StringUtils.isBlank(loginName)) throw new AuthenticationServiceException("登录名不能为空"); if(StringUtils.isBlank(credentials)) throw new AuthenticationServiceException("验证码不能为空"); SmsCodeAuthenticationToken authenticationToken = new SmsCodeAuthenticationToken(loginName, credentials); //将输入的信息封装成一个SmsCodeAuthenicationToken对象,并向后传递 setDetails(httpServletRequest, authenticationToken); return authenticationManager.authenticate(authenticationToken); } private void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } /** * @Author hmr * @Description 从request中获得参数 * @Date: 2020/4/30 12:22 * @param request * @reture: java.lang.String **/ protected String obtainParameter(HttpServletRequest request, String type){ return request.getParameter(type); } }
- 设置了Filter拦截到登陆请求之后,还需要一个具体的校验验证码是否正确的类 SmsAuthenticationProvider,该类才是具体的业务代码
package top.imuster.auth.component.login; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import top.imuster.auth.config.SmsCodeAuthenticationToken; import top.imuster.common.core.utils.RedisUtil; /** * @ClassName: IrhAuthenticationProvider * @Description: IrhAuthenticationProvider * @author: hmr * @date: 2020/4/30 14:36 */ public class SmsAuthenticationProvider implements AuthenticationProvider { private final Logger log = LoggerFactory.getLogger(this.getClass()); @Autowired RedisTemplate redisTemplate; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; //先将authentication强转成SmsCodeAuthenticationToken //登录名 String loginName = authenticationToken.getPrincipal() == null?"NONE_PROVIDED":authentication.getName(); //验证码 String verify = (String)authenticationToken.getCredentials(); String redisCode = (String)redisTemplate.opsForValue().get(RedisUtil.getConsumerLoginByEmail(loginName)); //从redis中获得申请到的验证码 if(StringUtils.isEmpty(redisCode) || !verify.equalsIgnoreCase(redisCode)){ throw new AuthenticationServiceException("验证码失效或者错误"); } return authentication; } @Override public boolean supports(Class<?> aClass) { return (SmsCodeAuthenticationToken.class.isAssignableFrom(aClass)); } }
并且在一个继承了 WebSecurityConfigurerAdapter的类中将Filter和Provide声明成Bean
package top.imuster.auth.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import top.imuster.auth.component.login.*; import top.imuster.auth.service.Impl.UsernameUserDetailsServiceImpl; import java.util.Arrays; @Configuration @EnableWebSecurity @Order(2147483636) class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public SmsCodeAuthenticationFilter smsCodeAuthenticationFilter(){
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
try {
smsCodeAuthenticationFilter.setAuthenticationManager(this.authenticationManager());
} catch (Exception e) {
e.printStackTrace();
}
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(irhAuthenticationSuccessHandler());
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(irhAuthenticationFailHandler());
return smsCodeAuthenticationFilter;
}
@Bean
public SmsAuthenticationProvider smsAuthenticationProvider(){
SmsAuthenticationProvider provider = new SmsAuthenticationProvider();
// 设置userDetailsService
// 禁止隐藏用户未找到异常
return provider;
}
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
ProviderManager authenticationManager = new ProviderManager(Arrays.asList(smsAuthenticationProvider()));
return authenticationManager;
}
}
说明:Filter是用来拦截Request请求,并且从request请求中将指定的信息提取出来,判断一些必要信息是否存在,不对信息进行校验,校验信息合法之后将其封装到token中,向后传递给provide如;provider则
将filter中得到的信息进行校验,在provider中,主要要注意的就是需要重写 supports(Class<?> aClass) 方法,该方法的作用就是假如在系统中有多个filter,并且向后传递了多个不同的token,那么对应的token只能传递到对应的provider中,所以该方法返回到是一个boolean类型,用来给Spring Security决策是否需要进入该provider。
- 获取验证码
/** * @Author hmr * @Description 发送email验证码 * @Date: 2020/4/30 10:12 * @param email 接受code的邮箱 * @param type 1-注册 2-登录 3-忘记密码 * @reture: top.imuster.common.base.wrapper.Message<java.lang.String> **/ @ApiOperation(value = "发送email验证码",httpMethod = "GET") @Idempotent(submitTotal = 5, timeTotal = 30, timeUnit = TimeUnit.MINUTES) @GetMapping("/sendCode/{type}/{email}") public Message<String> getCode(@ApiParam("邮箱地址") @PathVariable("email") String email, @PathVariable("type") Integer type) throws Exception { if(type != 1 && type != 2 && type != 3 && type != 4){ return Message.createByError("参数异常,请刷新后重试"); } userLoginService.getCode(email, type); return Message.createBySuccess(); }
具体代码我已经开源到GitHub上,仓库地址为https://github.com/HMingR/irh,该项目中还有利用Spring security实现微信小程序登陆,用户名密码登陆等。