代码地址
在上文中基于过滤器实现了图形验证码的操作,这次我们深入研究一下自定义登录认证,并基于自定义登录认证来完成图形验证码的验证操作。
在SpringSecurity中将用户权限、其他系统和设备等包装成为了一个接口
public interface Authentication extends Principal, Serializable {
// 权限列表
Collection<? extends GrantedAuthority> getAuthorities();
// 主体凭据,一般为密码
Object getCredentials();
// 主体携带的其他详细信息(等会验证码信息就会保存在这里)
Object getDetails();
// 主体,一般为用户名
Object getPrincipal();
// 是否验证成功
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
在表单登录的过程中,使用的UsernamePasswordAuthenticationToken
,查看该类的继承关系,可以看到其父类AbstractAuthenticationToken
实现了Authentication
接口
上面的Authentication 在各个AuthenticationProvider之间传输进行验证
public interface AuthenticationProvider {
// 验证方法,验证完毕返回Authentication
Authentication authenticate(Authentication var1) throws AuthenticationException;
// 是否支持当前的Authentication 类型
boolean supports(Class<?> var1);
}
AuthenticationProvider一般为很多个,由ProviderManager来进行管理并进行验证,其中保存了AuthenticationProvider的列表,在authenticate方法中会对对列表中的AuthenticationProvider进行迭代处理
private List<AuthenticationProvider> providers;
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
......
}
在该类中实现了基本的认证流程,不过我们这里并不是要继承它,而是继承之后的DaoAuthenticationProvider
(相较之多了密码加密的过程),这里主要介绍一下各个方法的用途
// 额外的认证,我们主要是实现这个方法来校验验证码
protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
// 检索用户
protected abstract UserDetails retrieveUser(String var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
// 验证用户信息
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
......
}
一般情况下继承并重写retrieveUser
和additionalAuthenticationChecks
就可以完成自定义认证的流程
上篇里面我们的验证码是从request中获取的,但是在additionalAuthenticationChecks
方法中并没有HttpServletRequest的参数,在Authentication中有一个getDetails()
的方法,我们可以将验证码信息放在Details中。所以要找到注入Authentication的时机,同时添加我们的验证码信息到Details中;在UsernamePasswordAuthenticationFilter
中会调用ProviderManager,Authentication也是来源于这个过滤器,在这个过滤器中有一个setDetails
的方法,可以看到入参就有HttpServletRequest 了
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
但是这个方法使用的是authenticationDetailsSource.buildDetails
,然而后进入方法,发现AuthenticationDetailsSource
只是一个接口,所以我们需要自己实现一个AuthenticationDetailsSource,将我们的验证码放进去
上面已经分析完毕,接下来就来实现吧。
我们需要一个自定义的Details,实现WebAuthenticationDetails
public class MyWebAuthenticationDetails extends WebAuthenticationDetails {
// 在构造器就初始化验证码是否正确,用于在Provider判断
private boolean isCodeRight;
private String imageCode;
public boolean isCodeRight() {
return isCodeRight;
}
public MyWebAuthenticationDetails(HttpServletRequest request) {
super(request);
String requestCode = request.getParameter("captcha");
HttpSession session = request.getSession();
String vertificationCode = (String) session.getAttribute("captcha");
// 不论校验成功还是失败,要保证session的验证码被删除
session.removeAttribute("captcha");
if(StringUtils.isEmpty(requestCode) || StringUtils.isEmpty(vertificationCode)
|| !requestCode.equals(vertificationCode)){
this.isCodeRight = false;
}else {
this.isCodeRight = true;
}
}
}
创建自己的AuthenticationDetailSource
,用于setDetails调用并注入我们的MyWebAuthenticationDetails
@Component
public class MyAuthenticationDetailSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
return new MyWebAuthenticationDetails(request);
}
}
接下来就可以实现我们的自己的AuthenticationProvider了
@Component
public class MyAuthenticationProvider extends DaoAuthenticationProvider{
// 由Spring注入UserDetailService和PasswordEncoder
public MyAuthenticationProvider(@Qualifier("myUserDetailService") UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
this.setUserDetailsService(userDetailsService);
this.setPasswordEncoder(passwordEncoder);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// 获取details
MyWebAuthenticationDetails details = (MyWebAuthenticationDetails) authentication.getDetails();
// 如果验证码错误抛出异常
if(!details.isCodeRight()){
throw new VerificationCodeException();
}
super.additionalAuthenticationChecks(userDetails, authentication);
}
}
最后在配置文件中进行配置即可
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MySuccessHandler successHandler;
@Autowired
private MyFailureHandler failureHandler;
@Autowired
private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> myWebAuthenticationDetailsSource;
@Autowired
private AuthenticationProvider authenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/css/**", "/img/**", "/js/**", "/bootstrap/**","/captcha.jpg").permitAll()
.antMatchers("/app/api/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/myLogin.html")
.loginProcessingUrl("/login")
.successHandler(successHandler)
.failureHandler(failureHandler)
// 注入自定义authenticationDetailsSource
.authenticationDetailsSource(myWebAuthenticationDetailsSource)
.permitAll()
// 使登录页不受限
.and()
.csrf().disable();
// 在验证用户名密码之前验证验证码信息
//.addFilterBefore(new VerificationCodeFilter(),
//UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 添加自定义Provider
auth.authenticationProvider(authenticationProvider);
}
@Bean
// 配置验证码工具
public Producer captcha(){
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width","150");
properties.setProperty("kaptcha.image.height","50");
// 字符集
properties.setProperty("kaptcha.textproducer.char.string","0123456789");
// 字符长度
properties.setProperty("kaptcha.textproducer.char.length","4");
Config config = new Config(properties);
// 使用默认图形验证码
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
需要注意的是,之前的PasswordEncoder
我在WebSecurityConfig
中进行配置的,由于我们配置了自定义的Provider,使用构造器注入的PasswordEncoder
,而WebSecurityConfig
文件中又注入了Provider,我们需要将PasswordEncoder
单独放在一个其他的配置文件中注入容器,否则会引起循环依赖
重启项目测试即可