代碼地址
在上文中基於過濾器實現了圖形驗證碼的操作,這次我們深入研究一下自定義登錄認證,並基於自定義登錄認證來完成圖形驗證碼的驗證操作。
Authentication
在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
接口
AuthenticationProvider
上面的Authentication 在各個AuthenticationProvider之間傳輸進行驗證
public interface AuthenticationProvider {
// 驗證方法,驗證完畢返回Authentication
Authentication authenticate(Authentication var1) throws AuthenticationException;
// 是否支持當前的Authentication 類型
boolean supports(Class<?> var1);
}
ProviderManager
AuthenticationProvider一般爲很多個,由ProviderManager來進行管理並進行驗證,其中保存了AuthenticationProvider的列表,在authenticate方法中會對對列表中的AuthenticationProvider進行迭代處理
private List<AuthenticationProvider> providers;
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
......
}
AbstractUserDetailsAuthenticationProvider
在該類中實現了基本的認證流程,不過我們這裏並不是要繼承它,而是繼承之後的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
單獨放在一個其他的配置文件中注入容器,否則會引起循環依賴
重啓項目測試即可