1. 程式人生 > >【SpringSecurity系列】SpringBoot整合SpringSecurity新增驗證碼登入

【SpringSecurity系列】SpringBoot整合SpringSecurity新增驗證碼登入

上一篇博文已經介紹過了SpringSecurity的表單登入,這裡我們基於上一篇的基礎上,新增一個驗證碼進行登入,登入頁面效果圖,如圖所示:

首先我們需要建立驗證碼的生成規則,首先建立一個驗證碼的實體:

public class ImageCode {

    /** 驗證碼 */
    private String code;

    /** 判斷過期時間 */
    private LocalDateTime expireTime;

    /** 生成的圖片驗證碼 */
    private BufferedImage image;


    public ImageCode(String code, int expireIn, BufferedImage image) {
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
        this.image = image;
    }

    //判斷驗證碼是否過期
    public boolean isExpried() {
        return LocalDateTime.now().isAfter(expireTime);
    }

    //省略get/set方法
}

在定義一個controller用來處理我們驗證碼生成的流程:

@RestController
public class ValidateCodeController {

    //定義存入session的key
    public static final String SESSION_KEY = "SESSION_IMAGE_CODE";

    /** 處理session */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ImageCode imageCode = createImageCode(new ServletWebRequest(request));
        sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
    }

    private ImageCode createImageCode(ServletWebRequest servletWebRequest) {
        int width = 67;
        int height = 23;
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        Graphics g = image.getGraphics();

        Random random = new Random();

        g.setColor(getRandColor(200,250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        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);
        }

        String sRand = "";
        for(int i =0; i < 4; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);

        }
        g.dispose();
        return new ImageCode(sRand, 60, image);
    }

    /**
     *  生成隨機背景條紋
     * @param fc
     * @param bc
     * @return
     */
    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);
    }
}

然後我們需要定義一個驗證碼的攔截器來判斷我們驗證碼的流程:

/**
 * 定義一個驗證碼的攔截器
 * @author hdd
 */
public class ValidateCodeFilter extends OncePerRequestFilter {
    
    private DemoAuthenticationFailureHandler demoAuthenticationFailureHandler;


    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        if (StringUtils.equals("/authentication/form", request.getRequestURI()) &&
                StringUtils.endsWithIgnoreCase(request.getMethod(), "post")) {
            try {
                validate(new ServletWebRequest(request));
            } catch (ValidateCodeException e) {
                demoAuthenticationFailureHandler.onAuthenticationFailure(request,response,e);
                return;
            }
        }
        filterChain.doFilter(request,response);
    }

    //具體的驗證流程
    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
        String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");

        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("驗證碼的值不能為空");
        }
        if (codeInSession == null) {
            throw new ValidateCodeException("驗證碼不存在");
        }
        if (codeInSession.isExpried()) {
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
            throw new ValidateCodeException("驗證碼已過期");
        }
        if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("驗證碼不匹配");
        }
        sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
    }

    public DemoAuthenticationFailureHandler getDemoAuthenticationFailureHandler() {
        return demoAuthenticationFailureHandler;
    }

    public void setDemoAuthenticationFailureHandler(DemoAuthenticationFailureHandler demoAuthenticationFailureHandler) {
        this.demoAuthenticationFailureHandler = demoAuthenticationFailureHandler;
    }
}

然我我們需要定義一個驗證碼的異常讓SpringSecurity可以捕獲到:

/**
 * 用於丟擲驗證碼錯誤的異常,整合AuthenticationException可被SpringSecurity捕獲到
 * @author hdd
 * @date 2018/12/11 0011 14:55
 * @param
 * @return
 */
public class ValidateCodeException extends AuthenticationException {

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

最後將我們定義的攔截器注入到SpringSecurity的攔截器鏈中:

  protected void configure(HttpSecurity http) throws Exception {
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setDemoAuthenticationFailureHandler(demoAuthenticationFailureHandler);

        http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class)//在UsernamePasswordAuthenticationFilter新增新新增的攔截器
             .formLogin()//表示使用form表單提交
            .loginPage("/login.html")//我們定義的登入頁
            .loginProcessingUrl("/authentication/form")//因為SpringSecurity預設是login請求為登入請求,所以需要配置自己的請求路徑
            .successHandler(demoAuthenticationSuccessHandler)//登入成功的操作
            .failureHandler(demoAuthenticationFailureHandler)//登入失敗的操作
            .and()
            .authorizeRequests()//對請求進行授權
            .antMatchers("/login.html","/code/image").permitAll()//表示login.html路徑不會被攔截
            .anyRequest()//表示所有請求
            .authenticated()//需要許可權認證
            .and()
            .csrf().disable();//這是SpringSecurity的安全控制,我們這裡先關掉
    }

最後在頁面中新增驗證碼:

<h3>表單登入</h3>
<form action="/authentication/form" method="post">
    <table>
        <tr>
            <td>使用者名稱:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密碼:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td>圖形驗證碼:</td>
            <td>
                <input type="text" name="imageCode">
                <img src="/code/image">
            </td>
        </tr>
        <tr>
            <td colspan="2"><button type="submit">登入</button></td>
        </tr>
    </table>
</form>

當我們填寫錯誤的驗證碼是,提示我們驗證碼錯誤:

完整專案程式碼請從git上拉取git地址