1. 程式人生 > >重構Spring Security實現圖形驗證碼的功能

重構Spring Security實現圖形驗證碼的功能

不單要寫完功能,而是要把它變的可以配置,供其他的應用可以使用
優化要點

  • 驗證碼的基本引數可配置(寬/高/驗證碼數字的長度/驗證碼的有效時間等)
  • 驗證碼的攔截介面可配置(url地址)
  • 驗證碼的生成邏輯可配置(更復雜的驗證碼生成邏輯)

1.驗證碼的基本引數可配置
重構Spring Security實現圖形驗證碼的功能

在呼叫方 呼叫驗證碼的時候,沒有做任何配置,則使用預設的驗證碼生成規則,如果有則覆蓋掉預設配置。
預設配置

//生成二維碼預設配置
public class ImageCodeProperties {

    private int width = 67;    //圖片長度
    private int height = 23;   //圖片高度
    private int length = 4;    //驗證碼長度
    private int expireIn = 60; //失效時間
    public int getWidth() {
        return width;
    }
    public void setWidth(int width) {
        this.width = width;
    }
    public int getHeight() {
        return height;
    }
    public void setHeight(int height) {
        this.height = height;
    }
    public int getLength() {
        return length;
    }
    public void setLength(int length) {
        this.length = length;
    }
    public int getExpireIn() {
        return expireIn;
    }
    public void setExpireIn(int expireIn) {
        this.expireIn = expireIn;
    }

}

//再此基礎上,再封裝一層。
public class ValidateCodeProperties {

    private ImageCodeProperties image = new ImageCodeProperties();

    public ImageCodeProperties getImage() {
        return image;
    }

    public void setImage(ImageCodeProperties image) {
        this.image = image;
    }
}
//之後,再把ValidateCodeProperties放置在SecurityProperties中

重構Spring Security實現圖形驗證碼的功能

再呼叫方則需要在配置檔案中配置即可。

#code length
core.security.code.image.length = 6
core.security.code.image.width = 100

完成程式碼如下:

@RestController
public class ValidateCodeController {

    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    //操作Session的類
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request,HttpServletResponse response) throws IOException {
        //1.根據隨機數生成數字
        ImageCode imageCode = createImageCode(new ServletWebRequest(request));
        //2.將隨機數存到Session中
        //把請求傳遞進ServletWebRequest,
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
        //3.將生成的圖片寫到介面的響應中
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());

    }

    //生成圖片
    private ImageCode createImageCode(ServletWebRequest request) {
        //寬和高需要從request來取,如果沒有傳遞,再從配置的值來取
        //驗證碼寬和高
        int width = ServletRequestUtils.getIntParameter(request.getRequest(), "width", securityProperties.getCode().getImage().getWidth());
        int height = ServletRequestUtils.getIntParameter(request.getRequest(), "height", securityProperties.getCode().getImage().getHeight());
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics graphics = image.getGraphics();
        Random random = new Random();

        graphics.setColor(getRandColor(200,250));
        graphics.fillRect(0, 0, width, height);
        graphics.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        graphics.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);
            graphics.drawLine(x, y, x+xl, y+yl);
        }
        String sRand = "";
        //驗證碼長度
        for (int i = 0; i < securityProperties.getCode().getImage().getLength(); i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand +=rand;
            graphics.setColor(new Color(20, random.nextInt(110), 20+random.nextInt(110),20+random.nextInt(110)));
            graphics.drawString(rand, 13*i+6, 16);
        }
        graphics.dispose();
        //過期時間
        return new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());
    }

    //隨機生成背景條紋
    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);
    }
}

<tr>
        <td>圖形驗證碼:</td>
        <td>
            <input type="text" name="imageCode">
            <img src="/code/image?width=200">
        </td>
</tr>

在配置檔案裡配置了驗證碼的長度和寬度,也在驗證碼的請求裡增加了width引數,這個時候請求我們的頁面;width=200會覆蓋掉core.security.code.image.width = 100這個屬性,
core.security.code.image.length = 6會覆蓋掉我們預設的4位長度驗證碼屬性。
重構Spring Security實現圖形驗證碼的功能

2.驗證碼的攔截介面可配置
ImageCodeProperties增加url引數,用來配置哪些url請求需要驗證碼。

//生成二維碼預設配置
public class ImageCodeProperties {

    private int width = 67;    //圖片長度
    private int height = 23;   //圖片高度
    private int length = 4;    //驗證碼長度
    private int expireIn = 60; //失效時間

    private String url;        //多個請求需要驗證;逗號隔開

    public int getWidth() {
        return width;
    }
    public void setWidth(int width) {
        this.width = width;
    }
    public int getHeight() {
        return height;
    }
    public void setHeight(int height) {
        this.height = height;
    }
    public int getLength() {
        return length;
    }
    public void setLength(int length) {
        this.length = length;
    }
    public int getExpireIn() {
        return expireIn;
    }
    public void setExpireIn(int expireIn) {
        this.expireIn = expireIn;
    }
    public String getUrl() {
        return url;
    }
    public void setUrl(String url) {
        this.url = url;
    }
}

//application.properties中配置需要攔截的url
core.security.code.image.url = /user,/user/*

//更改ValidateCodeFilter過濾中的doFilterInternal方法
//OncePerRequestFilter保證每次只被呼叫一次
//實現InitializingBean介面的目的是:其他的引數都組裝完畢之後,初始化urls的值
@Component
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean{

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    //儲存需要攔截的url
    private Set<String> urls = new HashSet<>();

  @Autowired
    private SecurityProperties securityProperties;

    private AntPathMatcher pathMatcher = new AntPathMatcher();

    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        //做urls處理
        String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(),",");
        for (String configUrl : configUrls) {
            urls.add(configUrl);
        }
        //登入的請求一定要做驗證碼校驗的
        urls.add("/authentication/form");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        //1.判斷表單提交的請求(是否為登入請求)
        //因為請求中有/user,/user/*這種方式的請求,就不能使用equals這種方式來判斷,需要用到spring的工具類AntPathMatcher
        boolean action = false;
        for (String url : urls) {
            if (pathMatcher.match(url, request.getRequestURI())) {
                action = true;
            }
        }
        if (action) {
            try {
                validate(new ServletWebRequest(request));
                //為什麼要用自定義異常,因為這是還是屬於認證的過濾鏈中
            } catch (ValidateCodeException e) {
                authenticationFailureHandler.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);
        //從請求裡,拿到imageCode[來源於表單]
        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 AuthenticationFailureHandler getAuthenticationFailureHandler() {
        return authenticationFailureHandler;
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    public Set<String> getUrls() {
        return urls;
    }

    public void setUrls(Set<String> urls) {
        this.urls = urls;
    }

    public SecurityProperties getSecurityProperties() {
        return securityProperties;
    }

    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }

    public SessionStrategy getSessionStrategy() {
        return sessionStrategy;
    }
    public void setSessionStrategy(SessionStrategy sessionStrategy) {
        this.sessionStrategy = sessionStrategy;
    }
}

//最後需要配置BrowserSecurityConfig使其生效
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    private final static String loginPage = "/authentication/require";

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private MyAuthenticationFailHandler myAuthenticationFailHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailHandler);
        //傳遞引數
        validateCodeFilter.setSecurityProperties(securityProperties);
        validateCodeFilter.afterPropertiesSet();

        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
        .formLogin()  
        .loginPage(loginPage)
        .loginProcessingUrl("/authentication/form")
        .successHandler(myAuthenticationSuccessHandler)
        .failureHandler(myAuthenticationFailHandler)
        .and()
        .authorizeRequests()
        .antMatchers(loginPage).permitAll()
        .antMatchers(securityProperties.getBrowser().getLoginPage(),
                "/code/image").permitAll()
        .anyRequest().authenticated()
        .and()
        .csrf().disable();
    }
}

從我們的配置上來說,目前有三個請求需要驗證碼
分別是:登入的,/user以及/user/*的

驗證成功就是這些請求的時候,都會做驗證碼的非空/正確校驗。

3.驗證碼的生成邏輯可配置