SpringBoot+Spring Security添加圖形驗證碼

Spring Security添加圖形驗證碼

添加驗證碼大致可以分為三個步驟:根據隨機數生成驗證碼圖片;將驗證碼圖片顯示到登錄頁面;認證流程中加入驗證碼校驗。Spring Security的認證校驗是由UsernamePasswordAuthenticationFilter過濾器完成的,所以我們的驗證碼校驗邏輯應該在這個過濾器之前。下面一起學習下如何加入驗證碼校驗功能。

生成圖形驗證碼

驗證碼功能需要用到以下依賴:

 <dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-captcha</artifactId>
    <version>5.3.10</version>
</dependency>

這個工具類的用戶可以參見該工具的官方文檔

接著定義一個ValidateCodeController,用于處理生成驗證碼請求:

@Slf4j
@RestController
public class ValidateController {

    public final static String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //設置response響應
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Pragma", "No-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);
        response.setContentType("image/jpeg");

        //定義圖形驗證碼的長、寬、驗證碼字符數、干擾元素個數
        CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(100, 38, 4, 20);
        System.out.println(captcha.getCode());
        //將驗證碼放到HttpSession里面
        request.getSession().setAttribute(SESSION_KEY_IMAGE_CODE, captcha.getCode());
        log.info("本次生成的驗證碼為:" + captcha.getCode() + ",已存放到HttpSession中");

        //圖形驗證碼寫出,可以寫出到文件,也可以寫出到流
        //輸出瀏覽器
        OutputStream out=response.getOutputStream();
        captcha.write(out);
        out.flush();
        out.close();

    }
}

使用hutool的CaptchaUtil.createCircleCaptcha方法生成驗證碼對象,將生成的驗證碼對象存儲到Session中,并通過IO流將生成的圖片輸出到登錄頁面上。

改造登錄頁

在登錄頁面加上如下代碼:

<span style="display: inline">
    <input type="text" name="imageCode" placeholder="驗證碼" style="width: 50%;"/>
    <img src="/code/image"/>
</span>
<img>

標簽的src屬性對應ValidateController的createCode方法。

要使生成驗證碼的請求不被攔截,需要在SecurityConfig的configure方法中配置免攔截:

@Override
protected void configure(HttpSecurity http) throws Exception {
   ...
            .antMatchers("/code/image").permitAll() // 無需認證的請求路徑
            .anyRequest()  // 所有請求
            ...
}

重啟項目,訪問http://localhost:8080/loginPage

認證流程添加驗證碼校驗

在校驗驗證碼的過程中,可能會拋出各種驗證碼類型的異常,比如“驗證碼錯誤”、“驗證碼已過期”等,所以我們定義一個驗證碼類型的異常類:

public class ValidateCodeException extends AuthenticationException {
    private static final long serialVersionUID = 5022575393500654458L;
ValidateCodeException(String message) {
    super(message);
}

}
注意,這里繼承的是AuthenticationException而不是Exception。

我們都知道,Spring Security實際上是由許多過濾器組成的過濾器鏈,處理用戶登錄邏輯的過濾器為UsernamePasswordAuthenticationFilter,而驗證碼校驗過程應該是在這個過濾器之前的,即只有驗證碼校驗通過后采去校驗用戶名和密碼。由于Spring Security并沒有直接提供驗證碼校驗相關的過濾器接口,所以我們需要自己定義一個驗證碼校驗的過濾器ValidateCodeFilter:

@Component
public class ValidateCodeFilter extends OncePerRequestFilter {

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        if ("/form".equalsIgnoreCase(httpServletRequest.getRequestURI())
                && "post".equalsIgnoreCase(httpServletRequest.getMethod())) {
            try {
                HttpSession session = httpServletRequest.getSession();
                String codeInReq = httpServletRequest.getParameter("imageCode");
                validateCode(session,codeInReq);
            } catch (ValidateCodeException e) {
                authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
                return;
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    private void validateCode(HttpSession session,String codeInRequest) throws ServletRequestBindingException {
        String codeInSession = (String)session.getAttribute(ValidateController.SESSION_KEY_IMAGE_CODE);

        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("驗證碼不能為空!");
        }
        if (codeInSession == null) {
            throw new ValidateCodeException("驗證碼不存在!");
        }
        if (!codeInRequest.equalsIgnoreCase(codeInSession)) {
            throw new ValidateCodeException("驗證碼不正確!");
        }
        session.removeAttribute(ValidateController.SESSION_KEY_IMAGE_CODE);

    }

}

ValidateCodeFilter繼承了org.springframework.web.filter.OncePerRequestFilter,該過濾器只會執行一次。

在doFilterInternal方法中我們判斷了請求URL是否為/form,該路徑對應登錄form表單的action路徑,請求的方法是否為POST,是的話進行驗證碼校驗邏輯,否則直接執行filterChain.doFilter讓代碼往下走。當在驗證碼校驗的過程中捕獲到異常時,調用Spring Security的校驗失敗處理器AuthenticationFailureHandler進行處理。

validateCode的校驗邏輯是validateCode方法

我們分別從Session中獲取了ImageCode對象和請求參數imageCode(對應登錄頁面的驗證碼<input>框name屬性),然后進行了各種判斷并拋出相應的異常。當驗證碼過期或者驗證碼校驗通過時,我們便可以刪除Session中的ImageCode屬性了。

驗證碼校驗過濾器定義好了,怎么才能將其添加到UsernamePasswordAuthenticationFilter前面呢?很簡單,只需要在SecurityConfig的configure方法中添加些許配置即可:

@Autowired
private ValidateCodeFilter validateCodeFilter;

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加驗證碼校驗過濾器
            .formLogin() // 表單登錄
            // http.httpBasic() // HTTP Basic
            .loginPage("/authentication/require") // 登錄跳轉 URL
            .loginProcessingUrl("/login") // 處理表單登錄 URL
            .successHandler(authenticationSucessHandler) // 處理登錄成功
            .failureHandler(authenticationFailureHandler) // 處理登錄失敗
            .and()
            .authorizeRequests() // 授權配置
            .antMatchers("/authentication/require",
                    "/login.html",
                    "/code/image").permitAll() // 無需認證的請求路徑
            .anyRequest()  // 所有請求
            .authenticated() // 都需要認證
            .and().csrf().disable();
}

上面代碼中,我們注入了ValidateCodeFilter,然后通過addFilterBefore方法將ValidateCodeFilter驗證碼校驗過濾器添加到了UsernamePasswordAuthenticationFilter前面。

大功告成,重啟項目,訪問http://localhost:8080/loginPage,當不輸入驗證碼時點擊登錄,
當輸入錯誤的驗證碼時點擊登錄,
當驗證碼通過,并且用戶名密碼正確時,頁面顯示如下:

image.png

參考文檔

Spring Security添加圖形驗證碼

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。