Spring Security - 使用自定義AuthenticationProvider實(shí)現(xiàn)圖形驗(yàn)證碼
前面通過(guò)過(guò)濾器實(shí)現(xiàn)驗(yàn)證碼校驗(yàn),是從servlet層面實(shí)現(xiàn)的配置簡(jiǎn)單污朽,易于理解。Spring Security 還提供了另一種更為靈活的方法龙考。
通過(guò)自定義認(rèn)證同樣可以實(shí)現(xiàn)蟆肆。
一、自定義AuthenticationProvider
我們只是在常規(guī)的密碼校驗(yàn)前加了一層判斷圖形驗(yàn)證碼的認(rèn)證條件
所以可以通過(guò)繼承DaoAuthenticationProvider稍加修改即可實(shí)現(xiàn)需求
- 通過(guò)構(gòu)造方法注入自定義的MyUserDetailsService晦款、MyPasswordEncoder
- 重新additionalAuthenticationChecks()方法
- 添加實(shí)現(xiàn)圖形驗(yàn)證碼校驗(yàn)邏輯
@Component
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
//構(gòu)造方法注入MyUserDetailsService和MyPasswordEncoder
public MyAuthenticationProvider(MyUserDetailsService myUserDetailService, MyPasswordEncoder myPasswordEncoder) {
this.setUserDetailsService(myUserDetailService);
this.setPasswordEncoder(myPasswordEncoder);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//實(shí)現(xiàn)圖形驗(yàn)證碼邏輯
//驗(yàn)證碼錯(cuò)誤炎功,拋出異常
if (!details.getImageCodeIsRight()) {
throw new VerificationCodeException("驗(yàn)證碼錯(cuò)誤");
}
//調(diào)用父類完成密碼校驗(yàn)認(rèn)證
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("查詢數(shù)據(jù)庫(kù)");
//查詢用戶信息
User user = userMapper.findByUserName(username);
if (user==null){
throw new UsernameNotFoundException(username+"用戶不存在");
}
//重新填充roles
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
return user;
}
}
authentication封裝了用戶的登錄驗(yàn)證信息
但是圖形驗(yàn)證碼是存在session中的,我們需要將request請(qǐng)求一同封裝進(jìn)authentication中
這樣就可以在additionalAuthenticationChecks()中添加驗(yàn)證碼的校驗(yàn)邏輯了
其實(shí)我們要驗(yàn)證的所有信息可以當(dāng)成一個(gè)主體Principal缓溅,通過(guò)繼承實(shí)現(xiàn)Principal蛇损,經(jīng)過(guò)包裝,返回的Authentication認(rèn)證實(shí)體坛怪。
public interface Authentication extends Principal, Serializable {
//獲取主體權(quán)限列表
Collection<? extends GrantedAuthority> getAuthorities();
//獲取主題憑證淤齐,一般為密碼
Object getCredentials();
//獲取主體攜帶的詳細(xì)信息
Object getDetails();
//獲取主體,一般為一個(gè)用戶名
Object getPrincipal();
//主體是否驗(yàn)證成功
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
一次完整的認(rèn)證通常包含多個(gè)AuthenticationProvider
由ProviderManager管理
ProviderManager由UsernamePasswordAuthenticationFilter 調(diào)用
也就是說(shuō)袜匿,所有的 AuthenticationProvider包含的Authentication都來(lái)源于UsernamePasswordAuthenticationFilter
二更啄、自定義AuthenticationDetailsSource
UsernamePasswordAuthenticationFilter本身并沒(méi)有設(shè)置用戶詳細(xì)信息的流程,而且是通過(guò)標(biāo)準(zhǔn)接口 AuthenticationDetailsSource構(gòu)建的居灯,這意味著它是一個(gè)允許定制的特性锈死。
public interface AuthenticationDetailsSource<C, T> {
T buildDetails(C var1);
}
UsernamePasswordAuthenticationFilter中使用的AuthenticationDetailsSource是一個(gè)標(biāo)準(zhǔn)的Web認(rèn)證源贫堰,攜帶
的是用戶的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的,我們可以實(shí)現(xiàn)自己WebAuthenticationDetails待牵,并擴(kuò)展自己需要的信息
public class MyWebAuthenticationDetails extends WebAuthenticationDetails {
private boolean imageCodeIsRight;
public boolean getImageCodeIsRight(){
return this.imageCodeIsRight;
}
//補(bǔ)充用戶提交的驗(yàn)證碼和session保存的驗(yàn)證碼
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;
}
}
}
將他提供給一個(gè)自定義的AuthenticationDetailsSource
@Component
public class MyAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
return new MyWebAuthenticationDetails(request);
}
}
有了HttpServletRequest其屏,接下來(lái)再去實(shí)現(xiàn)我們的圖形驗(yàn)證碼驗(yàn)證邏輯
@Component
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
//構(gòu)造方法注入U(xiǎn)serDetailsService和PasswordEncoder
public MyAuthenticationProvider(MyUserDetailsService myUserDetailService, MyPasswordEncoder myPasswordEncoder) {
this.setUserDetailsService(myUserDetailService);
this.setPasswordEncoder(myPasswordEncoder);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//實(shí)現(xiàn)圖形驗(yàn)證碼邏輯
//獲取詳細(xì)信息
MyWebAuthenticationDetails details = (MyWebAuthenticationDetails) authentication.getDetails();
//驗(yàn)證碼錯(cuò)誤,拋出異常
if (!details.getImageCodeIsRight()) {
throw new VerificationCodeException("驗(yàn)證碼錯(cuò)誤");
}
//調(diào)用父類完成密碼校驗(yàn)認(rèn)證
super.additionalAuthenticationChecks(userDetails, authentication);
}
}
三缨该、應(yīng)用自定義認(rèn)證
最后修改WebSecurityConfig 使其應(yīng)用自定義的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 {
//應(yīng)用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")
// 指定處理登錄請(qǐng)求的路徑,修改請(qǐng)求的路徑,默認(rèn)為/login
.loginProcessingUrl("/mylogin").permitAll()
.failureHandler(new MyAuthenticationFailureHandler())
.and()
.csrf().disable();
}
@Bean
public Producer kaptcha() {
//配置圖形驗(yàn)證碼的基本參數(shù)
Properties properties = new Properties();
//圖片寬度
properties.setProperty("kaptcha.image.width", "150");
//圖片長(zhǎng)度
properties.setProperty("kaptcha.image.height", "50");
//字符集
properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
//字符長(zhǎng)度
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
//使用默認(rèn)的圖形驗(yàn)證碼實(shí)現(xiàn)贰拿,也可以自定義
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
四蛤袒、測(cè)試
啟動(dòng)項(xiàng)目
訪問(wèn)api:http://localhost:8080/user/api/hi
輸入正確用戶名密碼,正確驗(yàn)證碼
訪問(wèn)成功
頁(yè)面顯示hi,user.
重啟項(xiàng)目
輸入正確用戶名密碼膨更,錯(cuò)誤驗(yàn)證碼
訪問(wèn)失敗
返回失敗報(bào)文
{
"error_code": 401,
"error_name": "com.yang.springsecurity.exception.VerificationCodeException",
"message": "請(qǐng)求失敗,圖形驗(yàn)證碼校驗(yàn)異常"
}