SpringSecurity入門4---自定義登錄認證實現圖形驗證碼

代碼地址
在上文中基於過濾器實現了圖形驗證碼的操作,這次我們深入研究一下自定義登錄認證,並基於自定義登錄認證來完成圖形驗證碼的驗證操作。

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 {
			......
	}

一般情況下繼承並重寫retrieveUseradditionalAuthenticationChecks就可以完成自定義認證的流程

獲取驗證碼

上篇裏面我們的驗證碼是從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單獨放在一個其他的配置文件中注入容器,否則會引起循環依賴

重啓項目測試即可

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章