图形验证码接口及其重构思想

开发生成图形验证码接口

图片实体 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
在这里插入图片描述

主要是设计思想

以增量的方式去适应变化
当需求逻辑发生变化时,我们不是改变原来得代码,而是加一段代码:很重要

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