圖形驗證碼接口及其重構思想

開發生成圖形驗證碼接口

圖片實體 ImageCode

在這裏插入圖片描述

public class ImageCode  {
    /**
     * 一個圖片驗證碼包含三個信息
     */

    private BufferedImage image; //圖片

    private String code;//code是一個隨機數,圖片根據這個隨機數生成,這個隨機數是要存入到session中的

    private LocalDateTime expireTime;//驗證碼圖片過期時間

    /**
     *
     * @param image
     * @param code
     * @param expireIn 多少秒過期
     */
    public ImageCode(BufferedImage image, String code, int expireIn) {
        this.image = image;
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }

    public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
        this.image = image;
        this.code = code;
        this.expireTime = expireTime;
    }
    public boolean isExpried() {
        return LocalDateTime.now().isAfter(expireTime);
    }

圖片接口 ValidateCodeController

根據隨機數生成圖片
將隨機數存到session中
將生成的圖片寫到接口的響應中

在這裏插入圖片描述

@RestController
public class ValidateCodeController implements Serializable {

    private static  final  String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key

    //spring 操作session的工具類
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @RequestMapping("/code/image")
    private void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //1根據請求中的隨機數生成圖片
        ImageCode imageCode = createImageCode(request);
        //2將隨機數放到session中
        sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
        //3將生成的圖片寫到接口的響應中
        ImageIO.write(imageCode.getImage(),"jpeg",response.getOutputStream());
    }

    private ImageCode createImageCode(HttpServletRequest request) {
        //生成一個圖片對象
        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(image, sRand, 60);



    }

在認證流程中加入圖形驗證碼校驗

登錄頁面

在這裏插入圖片描述

  <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>

安全認證配置不攔截圖片路徑

在這裏插入圖片描述
在這裏插入圖片描述

測試 圖片路徑無法訪問404及解決

圖片路徑無法訪問404,發現對應的model在mave中爲灰色
解決
https://blog.csdn.net/L359389556/article/details/82852244
在這裏插入圖片描述

訪問

在這裏插入圖片描述

自定義過濾器 ValidateCodeFilter 校驗登錄驗證碼

在這裏插入圖片描述

/**
 * 繼承spring中的OncePerRequestFilter,確保每次請求調用一次過濾器
 */
public class ValidateCodeFilter extends OncePerRequestFilter {

    private AuthenticationFailureHandler authenticationFailureHandler;

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

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        if(StringUtils.equals("/authentication/form",httpServletRequest.getRequestURI())
                && StringUtils.equalsAnyIgnoreCase(httpServletRequest.getMethod(),"post")){

            try {
                validate(new ServletWebRequest(httpServletRequest));

            }catch (ValidateCodeException e){
                authenticationFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
               return;//失敗後直接返回,不再走下面的過濾器
            } 

        }

        //如果不是登錄請求,直接放行
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }

    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);

    }


}

自定義異常 ValidateCodeException

/**
 * AuthenticationException 是 springframework.security提供的,登錄過程中所有異常的基類
 *
 */
public class ValidateCodeException extends AuthenticationException {

	private static final long serialVersionUID = -7285211528095468156L;

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

}

配置指定位置中加入此攔截器

 protected void configure(HttpSecurity http) throws Exception {
        //http.formLogin()   //指定身份認證的方式爲表單登錄
        //http.httpBasic()

        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(whaleAuthenctiationFailureHandler);//設置錯誤過濾器

        http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class)
                .formLogin()
//                .loginPage("/signIn.html") //指定登錄頁面的url
//                .loginPage("/anthentication/require") //指定登錄頁面的url
                .loginPage(securityProperties.getBrowser().getLoginPage()) //指定登錄頁面的url
                .loginProcessingUrl("/authentication/form")
                .successHandler(whaleAuthenticationSuccessHandler)
                .failureHandler(whaleAuthenctiationFailureHandler)
                .permitAll()
                .and()
                .authorizeRequests() //對請求授權
//                .antMatchers("/signIn.html","/code/image").permitAll() //加一個匹配器 對匹配的路徑不進行身份認證
                .antMatchers(securityProperties.getBrowser().getLoginPage(),"/code/image").permitAll() //加一個匹配器 對匹配的路徑不進行身份認證
                .anyRequest()        //任何請求
                .authenticated()    //安全認證
                .and()
                .cors().disable().csrf().disable();// 禁用跨站攻擊
                // 默認都會產生一個hiden標籤 裏面有安全相關的驗證 防止請求僞造 這邊我們暫時不需要 可禁用掉
                //任何請求都必須經過表單驗證才能進行訪問

       /* http.csrf().disable().cors().disable().headers().disable()
                .authorizeRequests()
                .antMatchers("/signIn.html").permitAll() // 配置不需要身份認證的請求地址
                .anyRequest().authenticated() // 其他所有訪問路徑需要身份認證
                .and()
                .formLogin()
                .loginPage("/signIn.html") // 指定登錄請求地址
                .loginProcessingUrl("/authentication/form")
                .permitAll();
        */


    }

測試ok

簡化認證失敗處理信息

	public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException authenticationException ) throws IOException, ServletException {
		logger.info("登錄失敗");
		if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
			httpServletResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
			httpServletResponse.setContentType("application/json;charset=UTF-8");
//			httpServletResponse.getWriter().write(objectMapper.writeValueAsString(authenticationException));//打印的信息太多 簡化如下
			httpServletResponse.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse(authenticationException.getMessage())));
		}else {
			super.onAuthenticationFailure(httpServletRequest,httpServletResponse,authenticationException);
		}

	}

在這裏插入圖片描述

重構圖形驗證碼接口

驗證碼基本參數可配置

在這裏插入圖片描述

默認配置

ImageCodeProperties

public class ImageCodeProperties  {

	private int width = 67;
	private int height = 23;
	private int length = 4;
	private int expireIn = 60;

再封裝一層ValidateCodeProperties

public class ValidateCodeProperties {
	
	private ImageCodeProperties image = new ImageCodeProperties();


	public ImageCodeProperties getImage() {
		return image;
	}

	public void setImage(ImageCodeProperties image) {
		this.image = image;
	}
}

SecurityProperties加入ValidateCodeProperties

@ConfigurationProperties(prefix = "whale.security") //這個類會讀取以whale.security開頭的配置項
public class SecurityProperties {
	//瀏覽器配置
	private BrowserProperties browser = new BrowserProperties();

	//驗證碼配置
	private ValidateCodeProperties code = new ValidateCodeProperties();

應用級配置

demo application

whale.security.code.image.length = 6#驗證碼長度、
whale.security.code.image.width = 200

請求級配置

在這裏插入圖片描述

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

攔截器引用配置

@RestController
public class ValidateCodeController implements Serializable {

    public static  final  String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key

    //spring 操作session的工具類
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    @RequestMapping("/code/image")
    private void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //1根據請求中的隨機數生成圖片
//        ImageCode imageCode = createImageCode(request);
        ImageCode imageCode = createImageCode(new ServletWebRequest(request));
        //2將隨機數放到session中
        sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
        //3將生成的圖片寫到接口的響應中
        ImageIO.write(imageCode.getImage(),"jpeg",response.getOutputStream());
    }

    /**
     *
     * @param  request(HttpServletRequest)
     * @return
     */
    private ImageCode createImageCode(ServletWebRequest request) {
        //生成一個圖片對象
//        int width = 67;
        int width = ServletRequestUtils.getIntParameter(request.getRequest(),"width",securityProperties.getCode().getImage().getWidth());
//        int height =23;
        int height = ServletRequestUtils.getIntParameter(request.getRequest(),"height",securityProperties.getCode().getImage().getHeight());

        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++) {
//        驗證碼的長度不應該在請求中配置
        for (int i = 0; i <securityProperties.getCode().getImage().getLength(); 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(image, sRand, 60);
        return new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());



    }

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

測試
在這裏插入圖片描述

驗證碼攔截的接口可配置

ImageCodeProperties加屬性url

private String url;

驗證碼匹配路徑以逗號隔開

demo application 中配置路徑

whale.security.code.image.url = /user,/user/*

攔截器處理


/**
 * 繼承spring中的OncePerRequestFilter,確保每次請求調用一次過濾器
 */
//InitializingBean 實現此接口中的 afterPropertiesSet 初始化方法在其中初始化圖片驗證碼攔截路徑
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {

    private AuthenticationFailureHandler authenticationFailureHandler;

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

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    //驗證碼路徑,需要初始化
    private Set<String> urls = new HashSet<>();

    //配置
    private SecurityProperties securityProperties;

    //路徑正則匹配工具
    private AntPathMatcher pathMatcher = new AntPathMatcher();

    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();

        String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(),",");
        for (String url : configUrls) {
            urls.add(url);
        }
        //這個路徑是默認的
        urls.add("/authentication/form");

    }

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

        System.out.println(httpServletRequest.getRequestURI());
        System.out.println(httpServletRequest.getRequestURL());
        
        //如果請求路徑滿足匹配模式 則需要驗證碼
        boolean action = false;
        for (String url : urls) {
            if(pathMatcher.match(url,httpServletRequest.getRequestURI())){
                action = true;
            }

        }

//        if(StringUtils.equals("/authentication/form",httpServletRequest.getRequestURI())
//                && StringUtils.equalsAnyIgnoreCase(httpServletRequest.getMethod(),"post")){
        if(action){

            try {
                validate(new ServletWebRequest(httpServletRequest));

            }catch (ValidateCodeException e){
                authenticationFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);

                return;//失敗後直接返回,不再走下面的過濾器
            }

        }

        //如果不是登錄請求,直接放行
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }

    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 SecurityProperties getSecurityProperties() {
        return securityProperties;
    }

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


}

BrowserSecurityConfig中配置攔截器

················
 @Override
    protected void configure(HttpSecurity http) throws Exception {
        //http.formLogin()   //指定身份認證的方式爲表單登錄
        //http.httpBasic()

        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(whaleAuthenctiationFailureHandler);//設置錯誤過濾器

        validateCodeFilter.setSecurityProperties(securityProperties);
        validateCodeFilter.afterPropertiesSet();
        ············

測試
在這裏插入圖片描述

驗證碼的生成邏輯可配置

創建驗證碼生成器接口及實現類

把com.whale.security.core.validate.ValidateCodeController#createImageCode中生成圖片驗證碼的邏輯搬到驗證碼生成器接口及實現類中
如下
在這裏插入圖片描述
ValidateCodeGenerator

public interface ValidateCodeGenerator {

	ImageCode generate(ServletWebRequest request);
	
}

ImageCodeGenerator

public class ImageCodeGenerator implements ValidateCodeGenerator {

	/**
	 * 系統配置
	 */
	@Autowired
	private SecurityProperties securityProperties;
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.imooc.security.core.validate.code.ValidateCodeGenerator#generate(org.
	 * springframework.web.context.request.ServletWebRequest)
	 */
	@Override
	public ImageCode generate(ServletWebRequest request) {

ValidateCodeController中調用驗證碼生成器接口

@RestController
public class ValidateCodeController implements Serializable {

    public static  final  String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key

    //spring 操作session的工具類
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private ValidateCodeGenerator imageCodeGenerator; 

    @RequestMapping("/code/image")
    private void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //1根據請求中的隨機數生成圖片
//        ImageCode imageCode = createImageCode(request);
//        ImageCode imageCode = createImageCode(new ServletWebRequest(request));
        ImageCode imageCode = imageCodeGenerator.generate(new ServletWebRequest(request));
        //2將隨機數放到session中
        sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
        //3將生成的圖片寫到接口的響應中
        ImageIO.write(imageCode.getImage(),"jpeg",response.getOutputStream());
    }

imageCodeGenerator 圖片生成器接口實現類的初始化和可配置

在這裏插入圖片描述

imageCodeGenerator是如何注入進去的呢
如下
在這裏插入圖片描述

@Configuration
public class ValidateCodeBeanConfig {
	
	@Autowired
	private SecurityProperties securityProperties;
	
	@Bean
	@ConditionalOnMissingBean(name = "imageCodeGenerator")
	//爲啥這樣配置
	//@ConditionalOnMissingBean(name = "imageCodeGenerator")spring初始化這個類之前會判斷容器中是否有名字爲imageCodeGenerator的bean,
	//若果有就用已經初始化的bean,沒有的話才初始化當前bean
	//這樣 這個接口就可被用戶覆蓋
	public ValidateCodeGenerator imageCodeGenerator() { //方法的名字就是spring容器中bean的名字
		ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
		
		codeGenerator.setSecurityProperties(securityProperties);
		return codeGenerator;
	}

}

測試demo模塊中覆蓋驗證碼生成器

在這裏插入圖片描述

@Component("imageCodeGenerator")
public class DemoImageCodeGenerator implements ValidateCodeGenerator {

	@Override
	public ImageCode generate(ServletWebRequest request) {
		System.out.println("更高級的圖形驗證碼生成代碼");
		return null;
	}
}

訪問報錯 ok
在這裏插入圖片描述

主要是設計思想

以增量的方式去適應變化
當需求邏輯發生變化時,我們不是改變原來得代碼,而是加一段代碼:很重要

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章