SpringBoot集成SpringSecurity - 表單登錄添加驗(yàn)證碼(四)

源碼地址:https://github.com/springsecuritydemo/microservice-auth-center04

通過(guò)之前文章的學(xué)習(xí),我們已經(jīng)基本上掌握了SpringSecurity的基本流程楚午。你會(huì)發(fā)現(xiàn)夕晓,真正的login請(qǐng)求時(shí)有SpringSecurity幫我們處理的菱蔬,那么我們?nèi)绾螌?shí)現(xiàn)自定義表單登錄呢,必須添加一個(gè)驗(yàn)證碼等。

一宠页、添加驗(yàn)證碼

我們這里為了方便,直接從百度找了個(gè)生成驗(yàn)證碼的代碼寇仓,你也可以使用自己項(xiàng)目中的驗(yàn)證碼生成工具举户。

1.1 生成驗(yàn)證碼工具類(lèi)

public class VerifyCodeUtils {

    //使用到Algerian字體,系統(tǒng)里沒(méi)有的話需要安裝字體遍烦,字體只顯示大寫(xiě)俭嘁,去掉了1,0,i,o幾個(gè)容易混淆的字符
    public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
    private static Random random = new Random();


    /**
     * 使用系統(tǒng)默認(rèn)字符源生成驗(yàn)證碼
     * @param verifySize    驗(yàn)證碼長(zhǎng)度
     * @return
     */
    public static String generateVerifyCode(int verifySize){
        return generateVerifyCode(verifySize, VERIFY_CODES);
    }
    /**
     * 使用指定源生成驗(yàn)證碼
     * @param verifySize    驗(yàn)證碼長(zhǎng)度
     * @param sources   驗(yàn)證碼字符源
     * @return
     */
    public static String generateVerifyCode(int verifySize, String sources){
        if(sources == null || sources.length() == 0){
            sources = VERIFY_CODES;
        }
        int codesLen = sources.length();
        Random rand = new Random(System.currentTimeMillis());
        StringBuilder verifyCode = new StringBuilder(verifySize);
        for(int i = 0; i < verifySize; i++){
            verifyCode.append(sources.charAt(rand.nextInt(codesLen-1)));
        }
        return verifyCode.toString();
    }

    /**
     * 輸出指定驗(yàn)證碼圖片流
     * @param w
     * @param h
     * @param os
     * @param code
     * @throws IOException
     */
    public static void outputImage(int w, int h, OutputStream os, String code) throws IOException{
        int verifySize = code.length();
        BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Random rand = new Random();
        Graphics2D g2 = image.createGraphics();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
        Color[] colors = new Color[5];
        Color[] colorSpaces = new Color[] { Color.WHITE, Color.CYAN,
                Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
                Color.PINK, Color.YELLOW };
        float[] fractions = new float[colors.length];
        for(int i = 0; i < colors.length; i++){
            colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
            fractions[i] = rand.nextFloat();
        }
        Arrays.sort(fractions);

        g2.setColor(Color.GRAY);// 設(shè)置邊框色
        g2.fillRect(0, 0, w, h);

        Color c = getRandColor(200, 250);
        g2.setColor(c);// 設(shè)置背景色
        g2.fillRect(0, 2, w, h-4);

        //繪制干擾線
        Random random = new Random();
        g2.setColor(getRandColor(160, 200));// 設(shè)置線條的顏色
        for (int i = 0; i < 20; i++) {
            int x = random.nextInt(w - 1);
            int y = random.nextInt(h - 1);
            int xl = random.nextInt(6) + 1;
            int yl = random.nextInt(12) + 1;
            g2.drawLine(x, y, x + xl + 40, y + yl + 20);
        }

        // 添加噪點(diǎn)
        float yawpRate = 0.05f;// 噪聲率
        int area = (int) (yawpRate * w * h);
        for (int i = 0; i < area; i++) {
            int x = random.nextInt(w);
            int y = random.nextInt(h);
            int rgb = getRandomIntColor();
            image.setRGB(x, y, rgb);
        }

        shear(g2, w, h, c);// 使圖片扭曲

        g2.setColor(getRandColor(100, 160));
        int fontSize = h-4;
        Font font = new Font("Algerian", Font.ITALIC, fontSize);
        g2.setFont(font);
        char[] chars = code.toCharArray();
        for(int i = 0; i < verifySize; i++){
            AffineTransform affine = new AffineTransform();
            affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize/2, h/2);
            g2.setTransform(affine);
            g2.drawChars(chars, i, 1, ((w-10) / verifySize) * i + 5, h/2 + fontSize/2 - 10);
        }

        g2.dispose();
        ImageIO.write(image, "jpg", os);
    }

    private static Color getRandColor(int fc, int bc) {
        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);
    }

    private static int getRandomIntColor() {
        int[] rgb = getRandomRgb();
        int color = 0;
        for (int c : rgb) {
            color = color << 8;
            color = color | c;
        }
        return color;
    }

    private static int[] getRandomRgb() {
        int[] rgb = new int[3];
        for (int i = 0; i < 3; i++) {
            rgb[i] = random.nextInt(255);
        }
        return rgb;
    }

    private static void shear(Graphics g, int w1, int h1, Color color) {
        shearX(g, w1, h1, color);
        shearY(g, w1, h1, color);
    }

    private static void shearX(Graphics g, int w1, int h1, Color color) {

        int period = random.nextInt(2);

        boolean borderGap = true;
        int frames = 1;
        int phase = random.nextInt(2);

        for (int i = 0; i < h1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(0, i, w1, 1, (int) d, 0);
            if (borderGap) {
                g.setColor(color);
                g.drawLine((int) d, i, 0, i);
                g.drawLine((int) d + w1, i, w1, i);
            }
        }

    }

    private static void shearY(Graphics g, int w1, int h1, Color color) {

        int period = random.nextInt(40) + 10; // 50;

        boolean borderGap = true;
        int frames = 20;
        int phase = 7;
        for (int i = 0; i < w1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(i, 0, 1, h1, 0, (int) d);
            if (borderGap) {
                g.setColor(color);
                g.drawLine(i, (int) d, i, 0);
                g.drawLine(i, (int) d + h1, i, h1);
            }
        }
    }
}

1.2 編寫(xiě)Redis配置、封裝結(jié)果集和生成圖片接口

我們將生成的驗(yàn)證碼存入到服務(wù)器的 Session 對(duì)象中服猪,但如果你的項(xiàng)目是分布式項(xiàng)目或者是App項(xiàng)目供填,這里就不能存入到Session中,可以考慮使用 Redis 存儲(chǔ)罢猪。我們采用Redis 存儲(chǔ)方案近她。
添加redis依賴包:

        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

添加redis和圖片驗(yàn)證碼失效時(shí)間:

...
  redis:
    #數(shù)據(jù)庫(kù)索引
    database: 0
    host: 127.0.0.1
    port: 6379
    password:
    #連接超時(shí)時(shí)間
    timeout: 5000
loginCode:
  expiration: 1 #登錄驗(yàn)證碼過(guò)期時(shí)間,單位 分鐘
  prefix: login_code #驗(yàn)證碼redis的key值前綴

編寫(xiě)圖片結(jié)果集:

@Data
@AllArgsConstructor
public class ImgResult {
    private String img;
    private String uuid;
}

編寫(xiě)獲取驗(yàn)證碼接口:

    // 登錄驗(yàn)證碼過(guò)期時(shí)間:?jiǎn)挝?分鐘
    @Value("${loginCode.expiration}")
    private Long expiration;

    @Value("${loginCode.prefix}")
    private String prefix;
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * 獲取驗(yàn)證碼
     */
    @GetMapping("/vCode")
    @ResponseBody
    public ImgResult getCode() throws IOException {

        // 生成隨機(jī)字串
        String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
        String uuid = UUID.randomUUID().toString();
        // 存入redis
        redisTemplate.opsForValue().set(prefix + uuid,verifyCode, expiration, TimeUnit.MINUTES);
        // 生成圖片
        int w = 111, h = 36;
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        VerifyCodeUtils.outputImage(w, h, stream, verifyCode);
        try {
            return new ImgResult(Base64.encode(stream.toByteArray()),uuid);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            stream.close();
        }
    }

這里采用 Base64格式的圖片返回膳帕,使用Hutool依賴包完成Base64轉(zhuǎn)換:

        <!--工具包-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.5.11</version>
        </dependency>

1.3 修改login.html

在原來(lái)的 login 頁(yè)面集成上加入 驗(yàn)證碼字段:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陸</title>
    <script src="js/jquery-3.4.1.min.js"></script>
</head>
<body>
<h1>登陸</h1>
<form method="post" action="/login">
    <div>
        用戶名:<input type="text" name="username">
    </div>
    <div>
        密 碼:<input type="password" name="password">
    </div>
    <div>
        驗(yàn)證碼:<input type="text" class="form-control" name="verifyCode" required="required" placeholder="驗(yàn)證碼">
        <input id="uuid" type="hidden" name="uuid" />
        <img  id="vCode" title="看不清粘捎,請(qǐng)點(diǎn)我" onclick="getVerifyCode()" onmouseover="mouseover(this)" />
    </div>
    <div>
        <label><input type="checkbox" name="remember-me"/>自動(dòng)登錄</label>
    </div>
    <div>
        <button type="submit">立即登陸</button>
    </div>
</form>

<script>
    $(function() {
        getVerifyCode();
    })

    function getVerifyCode() {
        var url = "/vCode?" + Math.random();
        $.ajax({
            //請(qǐng)求方式
            type : "GET",
            //請(qǐng)求的媒體類(lèi)型
            contentType: "application/json;charset=UTF-8",
            //請(qǐng)求地址
            url : url,
            //請(qǐng)求成功
            success : function(result) {
                console.log(result);
                $("#uuid").val(result.uuid);
                $("#vCode").attr("src","data:image/png;base64," + result.img);
            },
            //請(qǐng)求失敗,包含具體的錯(cuò)誤信息
            error : function(e){
                console.log(e.status);
                console.log(e.responseText);
            }
        });
    }

    function mouseover(obj) {
        obj.style.cursor = "pointer";
    }
</script>

</body>
</html>

1.4 添加匿名訪問(wèn) URL(放行 驗(yàn)證碼請(qǐng)求)

WebSecurityConfig 中允許 驗(yàn)證碼請(qǐng)求匿名訪問(wèn)危彩,不然沒(méi)有登錄就沒(méi)辦法獲取驗(yàn)證碼(死循環(huán)了)攒磨。

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Value("${loginCode.prefix}")
    private String prefix;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 如果有允許匿名的url,填在下面
                .antMatchers("/vCode").permitAll()
                .anyRequest().authenticated()
                .and()
                // 設(shè)置登陸頁(yè)
                .formLogin().loginPage("/login")
                // 設(shè)置登陸成功url
                .defaultSuccessUrl("/").permitAll()
                // 設(shè)置登錄失敗url
                .failureUrl("/login/error")
                // 自定義登陸用戶名和密碼參數(shù)恬砂,默認(rèn)為username和password
//                .usernameParameter("username")
//                .passwordParameter("password")
                .and()
                // 添加圖片驗(yàn)證碼過(guò)濾器
                .addFilterBefore(new VerifyFilter(redisTemplate, prefix), UsernamePasswordAuthenticationFilter.class)
                .logout().permitAll()
                // 自動(dòng)登錄
                .and().rememberMe()
                .tokenRepository(persistentTokenRepository())
                // 有效時(shí)間咧纠,單位:s
                .tokenValiditySeconds(60)
                .userDetailsService(userDetailsService);

        // 關(guān)閉CSRF跨域
        http.csrf().disable();
    }

這樣驗(yàn)證碼就加好了。

1.5 運(yùn)行程序

二泻骤、驗(yàn)證碼驗(yàn)證

驗(yàn)證方式:

  • AJAX 驗(yàn)證
  • 過(guò)濾器驗(yàn)證
  • Spring Security 驗(yàn)證
    接下來(lái)我們分別針對(duì)這幾種驗(yàn)證方式做講解漆羔。

2.1 AJAX驗(yàn)證

使用 AJAX 方式驗(yàn)證和我們 Spring Security 框架就沒(méi)有任何關(guān)系了,其實(shí)就是表單提交前先發(fā)個(gè) HTTP 請(qǐng)求驗(yàn)證驗(yàn)證碼狱掂,本篇不再贅述演痒。有興趣的同學(xué)可以自己實(shí)現(xiàn)。

2.2 過(guò)濾器驗(yàn)證

使用過(guò)濾器驗(yàn)證的思路: 在SpringSecurity 處理登錄驗(yàn)證請(qǐng)求前趋惨,先驗(yàn)證驗(yàn)證碼鸟顺,如果正確,放行;如果不正確讯嫂,拋出異常蹦锋。

具體實(shí)現(xiàn)步驟如下:

第一步:編寫(xiě)自定義驗(yàn)證碼異常,繼承AuthenticationException抽象類(lèi)

public class VerifyCodeException extends AuthenticationException {

    public VerifyCodeException(String msg) {
        super(msg);
    }

    public VerifyCodeException(String msg, Throwable t) {
        super(msg, t);
    }
}

第二步:編寫(xiě)驗(yàn)證碼過(guò)濾器

自定義一個(gè)過(guò)濾器欧芽,實(shí)現(xiàn) OncePerRequestFilter(用于防止多次執(zhí)行Filter的莉掂;也就是說(shuō)一次請(qǐng)求只會(huì)走一次攔截器鏈) ,在 isProtectedUrl() 方法中攔截 POST 方式的/login 請(qǐng)求千扔。

在邏輯處理中從 request 中取出驗(yàn)證碼憎妙,并進(jìn)行驗(yàn)證,如果驗(yàn)證成功曲楚,放行厘唾;驗(yàn)證失敗,手動(dòng)拋出異常龙誊。

public class VerifyFilter extends OncePerRequestFilter{

    private static final PathMatcher pathMatcher = new AntPathMatcher();

    private StringRedisTemplate stringRedisTemplate;

    private String prefix;

    public VerifyFilter() {}

    public VerifyFilter(StringRedisTemplate stringRedisTemplate, String prefix) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.prefix = prefix;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 攔截 /login的POST請(qǐng)求
        if(isProtectedUrl(request)) {
            String uuid = request.getParameter("uuid"); // 圖片驗(yàn)證碼的 key
            String vCode = request.getParameter("verifyCode");// 圖片驗(yàn)證碼的 value

            if(!validateVerify(request, uuid, vCode)) {
                //手動(dòng)設(shè)置異常
                request.getSession().setAttribute("SPRING_SECURITY_LAST_EXCEPTION",new VerifyCodeException("驗(yàn)證碼輸入錯(cuò)誤"));
                // 轉(zhuǎn)發(fā)到錯(cuò)誤Url
                request.getRequestDispatcher("/login/error").forward(request,response);
            } else {
                filterChain.doFilter(request,response);
            }
        } else {
            filterChain.doFilter(request,response);
        }
    }

    /**
     * 驗(yàn)證驗(yàn)證碼合法性
     * @param uuid  驗(yàn)證key
     * @param vCode 驗(yàn)證值
     * @return
     */
    private boolean validateVerify(HttpServletRequest request, String uuid, String vCode) {
        // 查詢驗(yàn)證碼
        String code = stringRedisTemplate.opsForValue().get(prefix + uuid);

        // 清除驗(yàn)證碼
        stringRedisTemplate.delete(prefix + uuid);

        if (StringUtils.isBlank(code)) {
            //手動(dòng)設(shè)置異常
            request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, new VerifyCodeException("驗(yàn)證碼已過(guò)期"));
            return false;
        }
        if (StringUtils.isBlank(vCode) || !vCode.equalsIgnoreCase(code)) {
            request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, new VerifyCodeException("驗(yàn)證碼錯(cuò)誤"));
            return false;
        }

        logger.info("驗(yàn)證碼:" + code + "用戶輸入:" + vCode);
        return true;
    }

    // 攔截 /login的POST請(qǐng)求
    private boolean isProtectedUrl(HttpServletRequest request) {
        return "POST".equals(request.getMethod()) && pathMatcher.match("/login", request.getServletPath());
    }

}

第三步:修改loginError()方法抚垃,添加 圖片驗(yàn)證碼異常處理 :

特別注意:這里不要指定請(qǐng)求方式,而使用:@RequestMapping("/login/error")载迄,這里之前我做這個(gè)測(cè)試讯柔,發(fā)現(xiàn)SpringSecurity 默認(rèn)錯(cuò)誤調(diào)整使用的是 GET 方式,這里我們手動(dòng)通過(guò) request.getDispatcher("/login/error").forward()使用的是 post方式护昧。所以這里一定記得修改注解方式為 @RequestMapping()魂迄,不然你會(huì)發(fā)現(xiàn)怎么也不成功。

    @RequestMapping("/login/error")
    @ResponseBody
    public Result loginError(HttpServletRequest request) {
        AuthenticationException authenticationException = (AuthenticationException) request.getSession().getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
        log.info("authenticationException={}", authenticationException);
        Result result = new Result();
        result.setCode(201);

        // 圖片驗(yàn)證碼校驗(yàn)
        if(authenticationException instanceof VerifyCodeException) {
            result.setMsg(authenticationException.getMessage());
        } else if (authenticationException instanceof UsernameNotFoundException || authenticationException instanceof BadCredentialsException) {
            result.setMsg("用戶名或密碼錯(cuò)誤");
        } else if (authenticationException instanceof DisabledException) {
            result.setMsg("用戶已被禁用");
        } else if (authenticationException instanceof LockedException) {
            result.setMsg("賬戶被鎖定");
        } else if (authenticationException instanceof AccountExpiredException) {
            result.setMsg("賬戶過(guò)期");
        } else if (authenticationException instanceof CredentialsExpiredException) {
            result.setMsg("證書(shū)過(guò)期");
        } else {
            result.setMsg("登錄失敗");
        }
        return result;
    }

第四步:注入過(guò)濾器

修改 WebSecurityConfigconfigure() 方法惋耙,添加一個(gè) addFilterBefore()捣炬,具有兩個(gè)參數(shù),作用是在參數(shù)二之前執(zhí)行參數(shù)一指定的過(guò)濾器绽榛。

SpringSecurity 對(duì)于用戶名/密碼登錄方式是通過(guò) UsernamePasswordAuthenticationFilter 處理的湿酸,所以我們?cè)谒皥?zhí)行自定義驗(yàn)證碼過(guò)濾器即可。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 如果有允許匿名的url灭美,填在下面
                .antMatchers("/vCode").permitAll()
                .anyRequest().authenticated()
                .and()
                // 設(shè)置登陸頁(yè)
                .formLogin().loginPage("/login")
                // 設(shè)置登陸成功url
                .defaultSuccessUrl("/").permitAll()
                // 設(shè)置登錄失敗url
                .failureUrl("/login/error")
                // 自定義登陸用戶名和密碼參數(shù)推溃,默認(rèn)為username和password
//                .usernameParameter("username")
//                .passwordParameter("password")
                .and()
                // 添加圖片驗(yàn)證碼過(guò)濾器
                .addFilterBefore(new VerifyFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)
                .logout().permitAll()
                // 自動(dòng)登錄
                .and().rememberMe()
                .tokenRepository(persistentTokenRepository())
                // 有效時(shí)間,單位:s
                .tokenValiditySeconds(60)
                .userDetailsService(userDetailsService);

        // 關(guān)閉CSRF跨域
        http.csrf().disable();

第五步:運(yùn)行程序

上面我們使用過(guò)濾器實(shí)現(xiàn)了驗(yàn)證功能届腐,但是其他它和AJAX 驗(yàn)證差別不大铁坎。

  • AJAZ 驗(yàn)證是在登錄提交前發(fā)送一個(gè)異步請(qǐng)求,請(qǐng)求返回成功就提交登錄犁苏;失敗就不提交登錄硬萍。
  • 過(guò)濾器是先驗(yàn)證驗(yàn)證碼,驗(yàn)證成功就讓 SpringSecurity 驗(yàn)證用戶名和密碼围详;驗(yàn)證失敗則拋出異常朴乖。

如果我們要做的需求是用戶登錄時(shí)需要多個(gè)驗(yàn)證字段,不單單是用戶名和密碼,那么使用過(guò)濾器會(huì)讓邏輯變得復(fù)雜买羞,而這里我們通過(guò)另外一種方式來(lái)完整驗(yàn)證邏輯袁勺。

2.3 SpringSecurity驗(yàn)證

第一步:自定義 WebAuthenticationDetails 類(lèi):

我們知道SpringSecurity 默認(rèn)情況下只會(huì)處理用戶名和密碼信息。

WebAuthenticationDetails: 該類(lèi)提供了獲取用戶登錄時(shí)攜帶的額外信息的功能畜普,默認(rèn)提供了 remoteAddress 與 sessionId 信息魁兼。

public class WebAuthenticationDetails implements Serializable {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // ~ Instance fields
    // ================================================================================================

    private final String remoteAddress;
    private final String sessionId;
    ...

這時(shí)候我們就要自定義 CustomWebAuthenticationDetails類(lèi),并在其中加入我們的驗(yàn)證碼字段:

package com.thtf.auth.security;

import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

import javax.servlet.http.HttpServletRequest;

/**
 * ========================
 * 獲取用戶登錄時(shí)攜帶的額外信息
 * Created with IntelliJ IDEA.
 * User:pyy
 * Date:2019/7/24 16:50
 * Version: v1.0
 * ========================
 */
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final String uuid;          //驗(yàn)證碼key
    private final String verifyCode;    //驗(yàn)證碼value


    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        this.uuid = request.getParameter("uuid");
        this.verifyCode = request.getParameter("verifyCode");
    }

    public String getUuid() {
        return uuid;
    }

    public String getVerifyCode() {
        return verifyCode;
    }
}

在這個(gè)類(lèi)我們?cè)黾觾蓚€(gè)屬性:uuid 和 verifyCode漠嵌。

第二步:配置 AuthenticationDetailsSource

自定義了 WebAuthenticationDetails,我們需要將其放入到 AuthenticationDetailsSource 中替換原來(lái)的 WebAuthenticationDetails 對(duì)象盖呼,所以我們還得實(shí)現(xiàn)自定義 AuthenticationDetailsSource

package com.thtf.auth.security;

import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

import javax.servlet.http.HttpServletRequest;

/**
 * ========================
 * 該接口用于在Spring Security登錄過(guò)程中對(duì)用戶的登錄信息的詳細(xì)信息進(jìn)行填充
 * Created with IntelliJ IDEA.
 * User:pyy
 * Date:2019/7/24 17:06
 * Version: v1.0
 * ========================
 */
@Component("authenticationDetailsSource")
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {

    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
        return new CustomWebAuthenticationDetails(request);
    }
}

第三步:將 CustomAuthenticationDetailsSource 注入到SpringSecurity中儒鹿。

修改 WebSecurityConfig,在 configure() 方法中使用 authenticationDetailsSource(authenticationDetailsSource)方法來(lái)指定它几晤,替換默認(rèn)的AuthenticationDetailsSource對(duì)象约炎。

    @Autowired
    private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 如果有允許匿名的url,填在下面
                .antMatchers("/vCode").permitAll()
                .anyRequest().authenticated()
                .and()
                // 設(shè)置登陸頁(yè)
                .formLogin().loginPage("/login")
                // 設(shè)置登陸成功url
                .defaultSuccessUrl("/").permitAll()
                // 設(shè)置登錄失敗url
                .failureUrl("/login/error")
                // 自定義登陸用戶名和密碼參數(shù)蟹瘾,默認(rèn)為username和password
//                .usernameParameter("username")
//                .passwordParameter("password")
                // 指定authenticationDetailsSource
                .authenticationDetailsSource(authenticationDetailsSource)
                .and()
                // 添加圖片驗(yàn)證碼過(guò)濾器
                //.addFilterBefore(new VerifyFilter(redisTemplate, prefix), UsernamePasswordAuthenticationFilter.class)
                .logout().permitAll()
                // 自動(dòng)登錄
                .and().rememberMe()
                .tokenRepository(persistentTokenRepository())
                // 有效時(shí)間圾浅,單位:s
                .tokenValiditySeconds(60)
                .userDetailsService(userDetailsService);

        // 關(guān)閉CSRF跨域
        http.csrf().disable();
    }

第四步:自定義 AuthenticationProvider

上面我們通過(guò)自定義 WebAuthenticationDetailsAuthenticationDetailsSource將驗(yàn)證碼key、驗(yàn)證碼值和用戶名憾朴、密碼一起帶入了Spring Security中狸捕,下面我們需要將它取出來(lái)。

這里需要我們自定義AuthenticationProvider众雷,需要注意:如果是我們自己實(shí)現(xiàn)AuthenticationProvider灸拍,那么我們就需要自己做密碼校驗(yàn)了

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    private StringRedisTemplate redisTemplate;


    @Value("${loginCode.prefix}")
    private String prefix;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 獲取用戶輸入的用戶名和密碼
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();

        String uuid = details.getUuid();
        String vCode = details.getVerifyCode();

        // 查詢驗(yàn)證碼
        String code = redisTemplate.opsForValue().get(prefix + uuid);

        // 清除驗(yàn)證碼
        redisTemplate.delete(prefix + uuid);

        if (StringUtils.isBlank(code)) {
            throw new VerifyCodeException("驗(yàn)證碼已過(guò)期");
        }
        if (StringUtils.isBlank(vCode) || !vCode.equalsIgnoreCase(code)) {
            throw new VerifyCodeException("驗(yàn)證碼錯(cuò)誤");
        }


        // userDetails為數(shù)據(jù)庫(kù)中查詢到的用戶信息
        UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);

        // 如果是自定義AuthenticationProvider砾省,需要手動(dòng)密碼校驗(yàn)
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        if(!bCryptPasswordEncoder.matches(password, userDetails.getPassword())) {
            throw new BadCredentialsException("密碼錯(cuò)誤");
        }

        return new UsernamePasswordAuthenticationToken(username, password, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 這里不要忘記鸡岗,和UsernamePasswordAuthenticationToken比較
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

第五步: 在 WebSecurityConfig 中注入 CustomAuthenticationProvider:

@Autowired
    private CustomAuthenticationProvider customAuthenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
        auth.authenticationProvider(customAuthenticationProvider);
    }

第六步:運(yùn)行程序

是不是更復(fù)雜了O(∩_∩)O哈哈~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市编兄,隨后出現(xiàn)的幾起案子轩性,更是在濱河造成了極大的恐慌,老刑警劉巖狠鸳,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件揣苏,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡碰煌,警方通過(guò)查閱死者的電腦和手機(jī)舒岸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)芦圾,“玉大人蛾派,你說(shuō)我怎么就攤上這事。” “怎么了洪乍?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵眯杏,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我壳澳,道長(zhǎng)岂贩,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任巷波,我火速辦了婚禮萎津,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘抹镊。我一直安慰自己锉屈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布垮耳。 她就那樣靜靜地躺著颈渊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪终佛。 梳的紋絲不亂的頭發(fā)上俊嗽,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音铃彰,去河邊找鬼绍豁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛牙捉,可吹牛的內(nèi)容都是我干的妹田。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼鹃共,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼鬼佣!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起霜浴,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤晶衷,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后阴孟,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體晌纫,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年永丝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了锹漱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡慕嚷,死狀恐怖哥牍,靈堂內(nèi)的尸體忽然破棺而出毕泌,到底是詐尸還是另有隱情,我是刑警寧澤嗅辣,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布撼泛,位于F島的核電站,受9級(jí)特大地震影響澡谭,放射性物質(zhì)發(fā)生泄漏愿题。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一蛙奖、第九天 我趴在偏房一處隱蔽的房頂上張望潘酗。 院中可真熱鬧,春花似錦雁仲、人聲如沸崎脉。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至骆膝,卻和暖如春祭衩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背阅签。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工掐暮, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人政钟。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓路克,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親养交。 傳聞我的和親對(duì)象是個(gè)殘疾皇子精算,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348