SpringBoot+Spring Security添加圖形驗(yàn)證碼

Spring Security添加圖形驗(yàn)證碼

添加驗(yàn)證碼大致可以分為三個(gè)步驟:根據(jù)隨機(jī)數(shù)生成驗(yàn)證碼圖片;將驗(yàn)證碼圖片顯示到登錄頁(yè)面;認(rèn)證流程中加入驗(yàn)證碼校驗(yàn)。Spring Security的認(rèn)證校驗(yàn)是由UsernamePasswordAuthenticationFilter過(guò)濾器完成的,所以我們的驗(yàn)證碼校驗(yàn)邏輯應(yīng)該在這個(gè)過(guò)濾器之前。下面一起學(xué)習(xí)下如何加入驗(yàn)證碼校驗(yàn)功能。

生成圖形驗(yàn)證碼

驗(yàn)證碼功能需要用到以下依賴:

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

這個(gè)工具類的用戶可以參見(jiàn)該工具的官方文檔

接著定義一個(gè)ValidateCodeController,用于處理生成驗(yàn)證碼請(qǐng)求:

@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 {
        //設(shè)置response響應(yīng)
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Pragma", "No-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);
        response.setContentType("image/jpeg");

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

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

    }
}

使用hutool的CaptchaUtil.createCircleCaptcha方法生成驗(yàn)證碼對(duì)象,將生成的驗(yàn)證碼對(duì)象存儲(chǔ)到Session中,并通過(guò)IO流將生成的圖片輸出到登錄頁(yè)面上。

改造登錄頁(yè)

在登錄頁(yè)面加上如下代碼:

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

標(biāo)簽的src屬性對(duì)應(yīng)ValidateController的createCode方法。

要使生成驗(yàn)證碼的請(qǐng)求不被攔截,需要在SecurityConfig的configure方法中配置免攔截:

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

重啟項(xiàng)目,訪問(wèn)http://localhost:8080/loginPage

認(rèn)證流程添加驗(yàn)證碼校驗(yàn)

在校驗(yàn)驗(yàn)證碼的過(guò)程中,可能會(huì)拋出各種驗(yàn)證碼類型的異常,比如“驗(yàn)證碼錯(cuò)誤”、“驗(yàn)證碼已過(guò)期”等,所以我們定義一個(gè)驗(yàn)證碼類型的異常類:

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

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

我們都知道,Spring Security實(shí)際上是由許多過(guò)濾器組成的過(guò)濾器鏈,處理用戶登錄邏輯的過(guò)濾器為UsernamePasswordAuthenticationFilter,而驗(yàn)證碼校驗(yàn)過(guò)程應(yīng)該是在這個(gè)過(guò)濾器之前的,即只有驗(yàn)證碼校驗(yàn)通過(guò)后采去校驗(yàn)用戶名和密碼。由于Spring Security并沒(méi)有直接提供驗(yàn)證碼校驗(yàn)相關(guān)的過(guò)濾器接口,所以我們需要自己定義一個(gè)驗(yàn)證碼校驗(yàn)的過(guò)濾器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("驗(yàn)證碼不能為空!");
        }
        if (codeInSession == null) {
            throw new ValidateCodeException("驗(yàn)證碼不存在!");
        }
        if (!codeInRequest.equalsIgnoreCase(codeInSession)) {
            throw new ValidateCodeException("驗(yàn)證碼不正確!");
        }
        session.removeAttribute(ValidateController.SESSION_KEY_IMAGE_CODE);

    }

}

ValidateCodeFilter繼承了org.springframework.web.filter.OncePerRequestFilter,該過(guò)濾器只會(huì)執(zhí)行一次。

在doFilterInternal方法中我們判斷了請(qǐng)求URL是否為/form,該路徑對(duì)應(yīng)登錄form表單的action路徑,請(qǐng)求的方法是否為POST,是的話進(jìn)行驗(yàn)證碼校驗(yàn)邏輯,否則直接執(zhí)行filterChain.doFilter讓代碼往下走。當(dāng)在驗(yàn)證碼校驗(yàn)的過(guò)程中捕獲到異常時(shí),調(diào)用Spring Security的校驗(yàn)失敗處理器AuthenticationFailureHandler進(jìn)行處理。

validateCode的校驗(yàn)邏輯是validateCode方法

我們分別從Session中獲取了ImageCode對(duì)象和請(qǐng)求參數(shù)imageCode(對(duì)應(yīng)登錄頁(yè)面的驗(yàn)證碼<input>框name屬性),然后進(jìn)行了各種判斷并拋出相應(yīng)的異常。當(dāng)驗(yàn)證碼過(guò)期或者驗(yàn)證碼校驗(yàn)通過(guò)時(shí),我們便可以刪除Session中的ImageCode屬性了。

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

@Autowired
private ValidateCodeFilter validateCodeFilter;

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

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

大功告成,重啟項(xiàng)目,訪問(wèn)http://localhost:8080/loginPage,當(dāng)不輸入驗(yàn)證碼時(shí)點(diǎn)擊登錄,
當(dāng)輸入錯(cuò)誤的驗(yàn)證碼時(shí)點(diǎn)擊登錄,
當(dāng)驗(yàn)證碼通過(guò),并且用戶名密碼正確時(shí),頁(yè)面顯示如下:

image.png

參考文檔

Spring Security添加圖形驗(yàn)證碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,024評(píng)論 3 413
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,285評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,548評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,328評(píng)論 6 404
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,878評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,098評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,616評(píng)論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,554評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,725評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,971評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,361評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,613評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,339評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,695評(píng)論 2 370