闲来无事,断断续续看了半个月的spring security。寻思着怎么在登录的时候验证更多的用户数据,例如:验证码。
在spring security官方文档中的第十章节,阐述了spring security验证的步骤。
在spring security中默认的验证方法都是通过调用ProviderManager,从而轮训authenticationProviders来一个个验证,达到验证的目的。但是不支持默认验证以外的验证。于是就有了以下的思路。
我们也自定义个Filter,把他添加到spring security中的filterChain中去,按照spring secutiry的验证结构来扩展一个验证机制。
笔者以下代码参考了spring security中的UsernamePasswordAuthenticationFilter来实现的。
- 具体步骤如下
- 自定义一个验证码token类
用来存放验证码验证数据
public class CodeAuthenticationToken extends AbstractAuthenticationToken{
/**
* 验证码
*/
private Object captcha;
/**
* 验证码验证标识
* true:通过
* false:错误
*/
private boolean flag;
public CodeAuthenticationToken() {
super(null);
}
public CodeAuthenticationToken(Object captcha) {
super(null);
this.captcha = captcha;
this.flag = false;
setAuthenticated(false);
}
public CodeAuthenticationToken(Collection extends GrantedAuthority> authorities, Object captcha) {
super(authorities);
this.captcha = captcha;
this.flag = false;
super.setAuthenticated(true); // must use super, as we override
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return null;
}
public Object getCaptcha() {
return captcha;
}
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");
}
super.setAuthenticated(false);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
- 自定义一个认证失败异常类
注意,这个类必须继承AuthenticationException 类
public class CodeAuthenticationException extends AuthenticationException {
public CodeAuthenticationException(String msg, Throwable t) {
super(msg, t);
}
public CodeAuthenticationException(String msg) {
super(msg);
}
}
- 提供一个验证码验证器
public class CodeAuthenticationProvider implements AuthenticationProvider {
/** Logger available to subclasses */
protected final Log logger = LogFactory.getLog(getClass());
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
logger.debug("custom captcha authentication");
Assert.isInstanceOf(CodeAuthenticationToken.class, authentication,"错误的类");
CodeAuthenticationToken custCaptchaToken = (CodeAuthenticationToken) authentication;
String captcha = custCaptchaToken.getCaptcha().toString();
if(captcha.equals("")){
logger.debug("验证码为空");
throw new CodeAuthenticationException("验证码错误!");
}
//写死一个验证码,具体可以自己修改
if(!captcha.equals("1000")){
logger.debug("验证码错误");
throw new CodeAuthenticationException("验证码错误!");
}
//返回验证成功对象
custCaptchaToken.setFlag(true);
return custCaptchaToken;
}
@Override
public boolean supports(Class> authentication) {
return (CodeAuthenticationToken.class.isAssignableFrom(authentication));
}
}
- 自定义一个认证管理器
这里管理器可以省略,这里因为是按照spring security的结构来实现的,所以没有省略。
public class CodeAuthenticationManager implements AuthenticationManager {
/**
* 自己实现的验证码认证器
*/
private AuthenticationProvider provider;
public CodeAuthenticationManager(AuthenticationProvider provider) {
Assert.notNull(provider, "provider cannot be null");
this.provider = provider;
}
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
return this.provider.authenticate(authentication);
}
}
- 我们自己的filter
public class CodeFilter extends GenericFilterBean {
/** Logger available to subclasses */
protected final Log logger = LogFactory.getLog(getClass());
//验证码拦截路径
private static final String CODE_ANT_URL = "/login";
private static final String SPRING_SECURITY_FORM_CAPTCHA_KEY = "code";
private String captchaParameter = SPRING_SECURITY_FORM_CAPTCHA_KEY;
private boolean postOnly = true;
//请求路径匹配
private RequestMatcher requiresAuthenticationRequestMatcher;
private RememberMeServices rememberMeServices = new NullRememberMeServices();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(CODE_ANT_URL); //设置验证失败重定向路径
public CodeFilter() {
this.requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(CODE_ANT_URL, "POST");
}
public CodeFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
Assert.notNull(requiresAuthenticationRequestMatcher,"requiresAuthenticationRequestMatcher cannot be null");
this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 不是需要过滤的路径,执行下一个过滤器
if (!requiresAuthentication(request, response)) {
filterChain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
logger.error("Authentication is null!");
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
} catch (InternalAuthenticationServiceException failed) {
logger.error("An internal error occurred while trying to authenticate the user.",failed);
return;
} catch (AuthenticationException failed) {
logger.error("Authentication failed.", failed);
//Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
//认证成功,执行下个过滤器
filterChain.doFilter(request, response);
}
private Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
//获取验证码
String captcha = request.getParameter(captchaParameter);
if (captcha == null) {
captcha = "";
}
captcha = captcha.trim();
CodeAuthenticationToken authRequest = new CodeAuthenticationToken(captcha);
//这里可以直接省略掉,用provider直接验证
CodeAuthenticationManager manager = new CodeAuthenticationManager(new CodeAuthenticationProvider());
return manager.authenticate(authRequest);
}
/**
* 比较需要过滤的请求路径
*
* @param request
* @param response
* @return
*/
protected boolean requiresAuthentication(HttpServletRequest request,
HttpServletResponse response) {
return requiresAuthenticationRequestMatcher.matches(request);
}
/**
* 处理验证码认证失败
* 参考 {@link org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter}
* Default behaviour for unsuccessful authentication.
*
* - Clears the {@link SecurityContextHolder}
* - Stores the exception in the session (if it exists or
* allowSesssionCreation is set to true)
* - Informs the configured RememberMeServices of the failed login
* - Delegates additional behaviour to the {@link AuthenticationFailureHandler}.
*
*/
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (logger.isDebugEnabled()) {
logger.debug("Authentication request failed: " + failed.toString(), failed);
logger.debug("Updated SecurityContextHolder to contain null Authentication");
logger.debug("Delegating to authentication failure handler " + failureHandler);
}
rememberMeServices.loginFail(request, response);
failureHandler.onAuthenticationFailure(request, response, failed);
}
}
以上是需要自己实现的。下面笔者在spring boot集成spring security中的demo中稍作修改,来实现验证码登录。
步骤如下:
- 将上面自己实现五个类添加到工程中去
- 修改WebSecurityConfig
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(getFilter(), UsernamePasswordAuthenticationFilter.class) //在认证用户名之前认证验证码
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//基于内存来验证用户
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
//注入自定义的验证码过滤器
@Bean
public CodeFilter getFilter(){
return new CodeFilter();
}
}
- 修改登录视图控制器
注释掉registry.addViewController("/login").setViewName("login");
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
//registry.addViewController("/login").setViewName("login");
}
}
- 新建一个登录视图控制器
@Controller
@RequestMapping("/")
public class LoginController {
//认证失败抛出来的异常
private static final String LAST_EXCEPTION_KEY = "SPRING_SECURITY_LAST_EXCEPTION";
@RequestMapping("/login")
public String login(HttpServletRequest request, HttpSession session){
//spring security默认会把异常存到session中。
AuthenticationException authenticationException = (AuthenticationException) session.getAttribute(LAST_EXCEPTION_KEY);
//判断异常是否是我们自定义的验证码认证异常
if(authenticationException != null && authenticationException instanceof CodeAuthenticationException){
CodeAuthenticationException c = (CodeAuthenticationException) authenticationException;
//验证码认证错误标识,存入request中只针对本次请求。不影响整个会话
request.setAttribute("codeError",true);
}
return "login";
}
}
- 最后在登录界面添加一个验证码输入框
Spring Security Example
Invalid username and password.
You have been logged out.
验证码错误!
本文内容基于spring boot 1.5.10和spring security 4.2.4来实现的spring security 添加登录验证码验证,在原有验证机制的情况下,添加了验证码验证机制。
测试数据 用户名为:user 密码:password 验证码:1000
例子比较简单,验证码直接写死了。感兴趣的可以自行修改
本例子源码:https://gitee.com/longguiyunjosh/spring-security-demo/tree/master
不足之处,欢迎补充
如需转载,请注明出处