Spring Security - 使用自定义AuthenticationProvider实现图形验证码
前面通过过滤器实现验证码校验,是从servlet层面实现的配置简单,易于理解。Spring Security 还提供了另一种更为灵活的方法。
通过自定义认证同样可以实现。
一、自定义AuthenticationProvider
我们只是在常规的密码校验前加了一层判断图形验证码的认证条件
所以可以通过继承DaoAuthenticationProvider稍加修改即可实现需求
- 通过构造方法注入自定义的MyUserDetailsService、MyPasswordEncoder
- 重新additionalAuthenticationChecks()方法
- 添加实现图形验证码校验逻辑
@Component
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
//构造方法注入MyUserDetailsService和MyPasswordEncoder
public MyAuthenticationProvider(MyUserDetailsService myUserDetailService, MyPasswordEncoder myPasswordEncoder) {
this.setUserDetailsService(myUserDetailService);
this.setPasswordEncoder(myPasswordEncoder);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//实现图形验证码逻辑
//验证码错误,抛出异常
if (!details.getImageCodeIsRight()) {
throw new VerificationCodeException("验证码错误");
}
//调用父类完成密码校验认证
super.additionalAuthenticationChecks(userDetails, authentication);
}
}
@Component
public class MyPasswordEncoder implements PasswordEncoder {
private static final PasswordEncoder INSTANCE = new MyPasswordEncoder();
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword);
}
public static PasswordEncoder getInstance() {
return INSTANCE;
}
private MyPasswordEncoder() {
}
}
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("查询数据库");
//查询用户信息
User user = userMapper.findByUserName(username);
if (user==null){
throw new UsernameNotFoundException(username+"用户不存在");
}
//重新填充roles
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
return user;
}
}
authentication封装了用户的登录验证信息
但是图形验证码是存在session中的,我们需要将request请求一同封装进authentication中
这样就可以在additionalAuthenticationChecks()中添加验证码的校验逻辑了
其实我们要验证的所有信息可以当成一个主体Principal,通过继承实现Principal,经过包装,返回的Authentication认证实体。
public interface Authentication extends Principal, Serializable {
//获取主体权限列表
Collection<? extends GrantedAuthority> getAuthorities();
//获取主题凭证,一般为密码
Object getCredentials();
//获取主体携带的详细信息
Object getDetails();
//获取主体,一般为一个用户名
Object getPrincipal();
//主体是否验证成功
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
一次完整的认证通常包含多个AuthenticationProvider
由ProviderManager管理
ProviderManager由UsernamePasswordAuthenticationFilter 调用
也就是说,所有的 AuthenticationProvider包含的Authentication都来源于UsernamePasswordAuthenticationFilter
二、自定义AuthenticationDetailsSource
UsernamePasswordAuthenticationFilter本身并没有设置用户详细信息的流程,而且是通过标准接口 AuthenticationDetailsSource构建的,这意味着它是一个允许定制的特性。
public interface AuthenticationDetailsSource<C, T> {
T buildDetails(C var1);
}
UsernamePasswordAuthenticationFilter中使用的AuthenticationDetailsSource是一个标准的Web认证源,携带
的是用户的sessionId和IP地址
public class WebAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
public WebAuthenticationDetailsSource() {
}
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new WebAuthenticationDetails(context);
}
}
public class WebAuthenticationDetails implements Serializable {
private static final long serialVersionUID = 530L;
private final String remoteAddress;
private final String sessionId;
public WebAuthenticationDetails(HttpServletRequest request) {
this.remoteAddress = request.getRemoteAddr();
HttpSession session = request.getSession(false);
this.sessionId = session != null ? session.getId() : null;
}
private WebAuthenticationDetails(String remoteAddress, String sessionId) {
this.remoteAddress = remoteAddress;
this.sessionId = sessionId;
}
可以看到我们是可以拿到HttpServletRequest的,我们可以实现自己WebAuthenticationDetails,并扩展自己需要的信息
public class MyWebAuthenticationDetails extends WebAuthenticationDetails {
private boolean imageCodeIsRight;
public boolean getImageCodeIsRight(){
return this.imageCodeIsRight;
}
//补充用户提交的验证码和session保存的验证码
public MyWebAuthenticationDetails(HttpServletRequest request) {
super(request);
String captcha = request.getParameter("captcha");
HttpSession session = request.getSession();
String saveCaptcha = (String) session.getAttribute("captcha");
if (StringUtils.isNotEmpty(saveCaptcha)){
session.removeAttribute("captcha");
}
if (StringUtils.isNotEmpty(captcha) && captcha.equals(saveCaptcha)){
this.imageCodeIsRight = true;
}
}
}
将他提供给一个自定义的AuthenticationDetailsSource
@Component
public class MyAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
return new MyWebAuthenticationDetails(request);
}
}
有了HttpServletRequest,接下来再去实现我们的图形验证码验证逻辑
@Component
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
//构造方法注入UserDetailsService和PasswordEncoder
public MyAuthenticationProvider(MyUserDetailsService myUserDetailService, MyPasswordEncoder myPasswordEncoder) {
this.setUserDetailsService(myUserDetailService);
this.setPasswordEncoder(myPasswordEncoder);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//实现图形验证码逻辑
//获取详细信息
MyWebAuthenticationDetails details = (MyWebAuthenticationDetails) authentication.getDetails();
//验证码错误,抛出异常
if (!details.getImageCodeIsRight()) {
throw new VerificationCodeException("验证码错误");
}
//调用父类完成密码校验认证
super.additionalAuthenticationChecks(userDetails, authentication);
}
}
三、应用自定义认证
最后修改WebSecurityConfig 使其应用自定义的MyAuthenticationDetailsSource、MyAuthenticationProvider
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private MyAuthenticationDetailsSource myWebAuthenticationDetailsSource;
@Autowired
private MyAuthenticationProvider myAuthenticationProvider;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
//应用MyAuthenticationProvider
auth.authenticationProvider(myAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**","/captcha.jpg").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
//AuthenticationDetailsSource
.authenticationDetailsSource(myWebAuthenticationDetailsSource)
.loginPage("/myLogin.html")
// 指定处理登录请求的路径,修改请求的路径,默认为/login
.loginProcessingUrl("/mylogin").permitAll()
.failureHandler(new MyAuthenticationFailureHandler())
.and()
.csrf().disable();
}
@Bean
public Producer kaptcha() {
//配置图形验证码的基本参数
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;
}
}
四、测试
启动项目
访问api:http://localhost:8080/user/api/hi
输入正确用户名密码,正确验证码
访问成功
页面显示hi,user.
重启项目
输入正确用户名密码,错误验证码
访问失败
返回失败报文
{
"error_code": 401,
"error_name": "com.yang.springsecurity.exception.VerificationCodeException",
"message": "请求失败,图形验证码校验异常"
}