三、Spring Security表單驗證碼

表單驗證碼登錄

表單登錄驗證碼驗證喷好,一般在用戶名翔横、密碼提交登錄前,添加過濾器梗搅,先驗證驗證碼的有效性(開發(fā)中一般用的這種)禾唁,然后再提交用戶名、密碼些膨。文章下面還會使用另一種方法:驗證碼和用戶名蟀俊、密碼一起同時提交登錄。

Spring Security中订雾,兩種實現(xiàn)方式為:

  • 使用自定義過濾器(Filter)肢预,在提交用戶名、密碼前洼哎,先驗證驗證碼的有效性
  • 驗證碼和用戶名烫映、密碼一起在Spring Security中進(jìn)行驗證

一、驗證碼生成

新建一個包validateCode放置所有驗證碼相關(guān)的類噩峦。

1.1锭沟、驗證碼實體對象

@Data
public class ValidateCode {
    private BufferedImage image;
    private String code;
    private LocalDateTime expireTime;

    /**
     * @param expirtSecond 設(shè)置過期時間,單位秒
     */
    public ValidateCode(BufferedImage image, String code, int expirtSecond){
        this.image = image;
        this.code = code;
        // expireSecond秒后的時間
        this.expireTime = LocalDateTime.now().plusSeconds(expirtSecond);
    }
    /**
     * 驗證碼是否過期
     */
    public boolean isExpired(){
        return LocalDateTime.now().isAfter(expireTime);
    }
}

1.2识补、生成驗證碼:

@Service
public class ValidateCodeCreateService {
    public ValidateCode createImageCode() {
        // 寬度
        // 從請求參數(shù)中獲取數(shù)據(jù)族淮,否則,讀取配置文件配置值
        int width = 80;
        // 高度
        int height = 30;
        // 認(rèn)證碼長度
        int charLength = 4;
        // 過期時間(秒)
        int expireTime = 60;
        BufferedImage image = new BufferedImage(width, height,
                BufferedImage.TYPE_INT_RGB);
        // 獲取圖形上下文
        Graphics g = image.getGraphics();
        // 生成隨機類
        Random random = new Random();
        // 設(shè)定背景色
        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        // 設(shè)定字體
        g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
        // 隨機產(chǎn)生155條干擾線,使圖象中的認(rèn)證碼不易被其它程序探測到
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }
        // 取隨機產(chǎn)生的認(rèn)證碼
        String sRand = "";
        for (int i = 0; i < charLength; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            // 將認(rèn)證碼顯示到圖象中
            g.setColor(new Color(20 + random.nextInt(110), 20 + random
                    .nextInt(110), 20 + random.nextInt(110)));
            // 調(diào)用函數(shù)出來的顏色相同祝辣,可能是因為種子太接近贴妻,所以只能直接生成
            g.drawString(rand, 13 * i + 6, 16);
        }
        // 圖象生效
        g.dispose();
        return new ValidateCode(image, sRand, expireTime);
    }

    /**
     * 給定范圍獲得隨機顏色
     */
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
}

驗證碼圖片生成接口

@RestController
public class ValidateCodeController {
    @Autowired
    private ValidateCodeCreateService validateCodeCreateService;

    @GetMapping("/get-validate-code")
    public void getImageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 創(chuàng)建驗證碼
        ValidateCode validateCode = validateCodeCreateService.createImageCode();
        // 將驗證碼放到session中(也可放在Redis中,可設(shè)置過期時間)
        request.getSession().setAttribute("validate-code", validateCode);
        // 返回驗證碼給前端
        ImageIO.write(validateCode.getImage(), "JPEG", response.getOutputStream());
    }
}

二蝙斜、登錄頁面配置

修改resources/templates下登錄頁面名惩,添加驗證碼選項:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <meta charset="UTF-8">
    <title>登錄頁面</title>
</head>
<body>

<form th:action="@{/my-login}" method="post">
    <div><label> 用戶名 : <input type="text" name="username"/> </label></div>
    <div><label> 密碼: <input type="password" name="password"/> </label></div>
    <div>驗證碼:
        <input type="text" class="form-control" name="validateCode" required="required" placeholder="驗證碼">
        <img src="get-validate-code" title="看不清,請點我" onclick="refresh(this)" />
    </div>
    <button type="submit" class="btn">登錄</button>
</form>
<script>
    function refresh(obj) { obj.src = "get-validate-code"; }
</script>
</body>
</html>

WebSecurityConfig配置:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                // 獲取驗證碼允許匿名訪問
                .antMatchers("/get-validate-code").permitAll()
                .anyRequest().authenticated()
            .and()
                .formLogin()
                .loginPage("/user-login").permitAll()
                .loginProcessingUrl("/my-login")
    // ...
    }
}

正常項目已經(jīng)配置好孕荠,啟動項目娩鹉,訪問localhost:8080/hello跳轉(zhuǎn)到自定義的登錄頁面:

圖片

隨便輸入內(nèi)容提交,登錄失敗稚伍,返回:

圖片

輸入正確的用戶名弯予、密碼,驗證碼隨意輸入登錄个曙,登錄成功熙涤,返回:

圖片

可以看到,這里Spring Security默認(rèn)只驗證用戶名困檩、密碼,沒有驗證驗證碼是否正確那槽。所以下面開始實現(xiàn)登錄驗證碼驗證悼沿,有以下兩種種實現(xiàn)方式:

  1. 使用自定義過濾器(Filter),在校驗用戶名骚灸、密碼前判斷驗證碼合法性糟趾,驗證通過后,通過用戶名和密碼登錄
  2. 驗證碼和用戶名甚牲、密碼一起提交到后臺登錄

三义郑、過濾器驗證

原理:在 Spring Security 處理登錄請求前,先驗證驗證碼丈钙,如果正確非驮,放行去登錄;如果不正確雏赦,返回失敗處理劫笙。

2.1、驗證碼過濾器

自定義一個過濾器星岗,OncePerRequestFilter(該Filter保證每次請求只過濾一次):

public class ValidateCodeFilter extends OncePerRequestFilter {
    // URL正則匹配
    private static final PathMatcher pathMatcher = new AntPathMatcher();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 只有登錄請求‘/authentication/form’,并且為'post'請求時填大,才校驗
        if ("POST".equals(request.getMethod())
                && pathMatcher.match("/anthentication/form", request.getServletPath())) {
            try {
                codeValidate(request);
            } catch (ValidateCodeException e) {
               // 驗證碼不通過,跳到錯誤處理器處理
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().append(
                    new ObjectMapper().createObjectNode()
                        .put("status", "500")
                        .put("msg", e.getMessage())
                        .toString());
                // 異常后俏橘,不執(zhí)行后面
                return;
            }
        }
        doFilter(request, response, filterChain);
    }

    private void codeValidate(HttpServletRequest request) throws JsonProcessingException {
        // 獲取到傳入的驗證碼
        String codeInRequest = request.getParameter("validateCode");
        ValidateCode codeInSession = (ValidateCode) request.getSession(false).getAttribute("validate-code");

        // 校驗驗證碼是否正確
        if (StringUtils.isEmpty(codeInRequest)) {
            throw new ValidateCodeException("驗證碼的值不能為空");
        }
        if (codeInSession == null) {
            throw new ValidateCodeException("驗證碼不存在");
        }
        if (codeInSession.isExpired()) {
            throw new ValidateCodeException("驗證碼已過期");
        }
        if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("驗證碼不匹配");
        }

        // 校驗正確后允华,移除session中驗證碼
        request.getSession(false).removeAttribute("validate-code");
    }
}

class ValidateCodeException extends AuthenticationException {
    public ValidateCodeException(String message) {
        super(message);
    }
}

2.2、配置過濾器

Spring Security 對于用戶名/密碼登錄驗證是通過 UsernamePasswordAuthenticationFilter 處理的,只要在它之前執(zhí)行驗證碼過濾器即可:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 驗證碼過濾器在用戶名靴寂、密碼校驗前
                .addFilterBefore(new ValidateCodeFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers("/get-validate-code").permitAll()
                .anyRequest().authenticated()
            .and()
                .formLogin()
                .loginPage("/user-login").permitAll()
                .loginProcessingUrl("/my-login")
    }
}

2.4磷蜀、運行程序

啟動項目,訪問localhost:8080/login到登錄頁榨汤,隨機輸入內(nèi)容登錄:

圖片

點擊登錄后蠕搜,后臺驗證驗證碼錯誤,顯示如下:

圖片

輸入正確的驗證碼收壕,而用戶名妓灌、密碼錯誤:

圖片

全部正確時,返回用戶信息:

圖片

四蜜宪、和用戶名虫埂、密碼同時驗證

上面使用過濾器實現(xiàn)了驗證碼功能,該過濾器是先驗證驗證碼圃验,驗證成功就讓 Spring Security 驗證用戶名和密碼掉伏。

如果用戶登錄是需要多個登錄字段,不單單是用戶名和密碼澳窑,這時候可以考慮自定義 Spring Security 的驗證邏輯斧散。

3.1、WebAuthenticationDetails

Spring security 默認(rèn)只會處理用戶名和密碼信息摊聋,如果我們需要增加驗證碼字段驗證鸡捐,則需要拿到驗證碼。而WebAuthenticationDetails類提供了獲取用戶登錄時攜帶的額外信息的功能麻裁,可以通過該類拿到驗證碼箍镜。所以我們需要自定義類繼承該類拿到驗證碼:

public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
    @Getter // 設(shè)置getter方法,以便拿到驗證碼
    private final String validateCode;
    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        // 拿頁面?zhèn)鱽淼尿炞C碼
        validateCode = request.getParameter("validateCode");
    }
}

3.2煎源、AuthenticationDetailSource

把自定義CustomWebAuthenticationDetails色迂,放入 AuthenticationDetailsSource 中來替換原本的 WebAuthenticationDetails ,因此還得實現(xiàn)自定義 CustomAuthenticationDetailsSource 手销,設(shè)置為我們自定義的 CustomWebAuthenticationDetails

@Component("authenticationDetailsSource")
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest httpRequest) {
        return new CustomWebAuthenticationDetails(httpRequest);
    }
}

3.3歇僧、Spring Security配置

CustomAuthenticationDetailsSource 注入Spring Security中,替換掉默認(rèn)的 AuthenticationDetailsSource锋拖。

修改 WebSecurityConfig馏慨,將其注入,然后在config()中使用 authenticationDetailsSource(authenticationDetailsSource)方法來指定它姑隅。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 
    
    // 省略其他
    
    @Autowired
    private AuthenticationDetailsSource authenticationDetailsSource;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/get-validate-code").permitAll()
                .anyRequest().authenticated()
              .and()
                .formLogin()
                .loginPage("/user-login").permitAll()
                .loginProcessingUrl("/my-login")
                .authenticationDetailsSource(authenticationDetailsSource);
        http.csrf().disable();
    }
}

3.4写隶、AuthenticationProvider

通過自定義CustomWebAuthenticationDetailsCustomAuthenticationDetailsSource將驗證碼和用戶名、密碼一起加入了Spring Security中讲仰,但默認(rèn)的認(rèn)證中還不會對驗證碼進(jìn)行校驗慕趴,需要重寫UserDetailsAuthenticationProvider進(jìn)行校驗。

@Component
public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    @Autowired
    private CustomUserDetailsService userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // 獲取登錄提交的用戶名和密碼
        String inputPassword = (String) authentication.getCredentials();

        // 獲取登錄提交的驗證碼
        CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();
        String validateCode = details.getValidateCode();

        // 驗證碼校驗
        checkValidateCode(validateCode);

        // 驗證用戶名
        if (!passwordEncoder.matches(inputPassword, userDetails.getPassword())) {
            throw new BadCredentialsException("密碼錯誤");
        }
    }
    
    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
        return userDetailsService.loadUserByUsername(username);
    }
    
    private void checkValidateCode(String validateCode) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        ValidateCode codeInSession = (ValidateCode) request.getSession(false).getAttribute("validate-code");
        if (StringUtils.isEmpty(validateCode)) {
            throw new ValidateCodeException("驗證碼的值不能為空");
        }
        if (codeInSession == null) {
            throw new ValidateCodeException("驗證碼不存在");
        }
        if (codeInSession.isExpired()) {
            // 移除session中驗證碼
            request.getSession(false).removeAttribute("validate-code");
            throw new ValidateCodeException("驗證碼已過期");
        }
        if (!StringUtils.equals(codeInSession.getCode(), validateCode)) {
            throw new ValidateCodeException("驗證碼不匹配");
        }
        // 移除session中驗證碼
        request.getSession(false).removeAttribute("validate-code");
    }
}
class ValidateCodeException extends AuthenticationException {
    ValidateCodeException(String message) {
        super(message);
    }
}

WebSecurityConfig 中將其注入,并在 configure(AuthenticationManagerBuilder auth) 方法中通過 auth.authenticationProvider() 指定使用

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 
    @Autowired
    private CustomAuthenticationProvider authenticationProvider;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // auth.userDetailsService(userDetailsService);
        auth.authenticationProvider(authenticationProvider);
    }
}

啟動程序測試即可冕房。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末躏啰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子耙册,更是在濱河造成了極大的恐慌给僵,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件详拙,死亡現(xiàn)場離奇詭異帝际,居然都是意外死亡,警方通過查閱死者的電腦和手機饶辙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進(jìn)店門蹲诀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人弃揽,你說我怎么就攤上這事脯爪。” “怎么了矿微?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵痕慢,是天一觀的道長。 經(jīng)常有香客問我涌矢,道長守屉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任蒿辙,我火速辦了婚禮,結(jié)果婚禮上滨巴,老公的妹妹穿的比我還像新娘思灌。我一直安慰自己,他們只是感情好恭取,可當(dāng)我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布泰偿。 她就那樣靜靜地躺著,像睡著了一般蜈垮。 火紅的嫁衣襯著肌膚如雪耗跛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天攒发,我揣著相機與錄音调塌,去河邊找鬼。 笑死惠猿,一個胖子當(dāng)著我的面吹牛羔砾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼姜凄,長吁一口氣:“原來是場噩夢啊……” “哼政溃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起态秧,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤董虱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后申鱼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體愤诱,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年润讥,在試婚紗的時候發(fā)現(xiàn)自己被綠了转锈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡楚殿,死狀恐怖撮慨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情脆粥,我是刑警寧澤砌溺,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站变隔,受9級特大地震影響规伐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜匣缘,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一猖闪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧肌厨,春花似錦培慌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至表鳍,卻和暖如春馅而,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背譬圣。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工瓮恭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人厘熟。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓偎血,卻偏偏與公主長得像诸衔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子颇玷,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,573評論 2 359