SpringSecurity之图形验证码

图形验证码使用:

  • 用户名密码登录
    防止用户重复输入用户名密码强行破解登录
  • 短信发送
    某些时候短信API的限量是无效的,此时需要在短信发送接口前进行图形验证码校验,防止短信盗刷(APP模块开发)
  • 类似于12306,利用图形验证码限流

此处图形验证码按照视频教程做,笔记不完整,因为后面开发到APP模块之后会对图形验证码进行重构(不使用session,图形验证码放入Redis缓存,APP场景适用于当前浏览器模式的开发)

验证码类关系图:
在这里插入图片描述
ImageCode.java:

package com.cong.security.core.code.image;
import com.cong.security.core.code.ValidateCode;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
public class ImageCode extends ValidateCode {
	private BufferedImage BufferImage;
	/*过期时间*/
	private LocalDateTime expireTime;
	/**
	 * 设置在多少秒之后过期
	 * @param bufferImage
	 *            图片流
	 * @param code
	 *            验证码
	 * @param expireIn
	 *            秒数
	 */
	public ImageCode(BufferedImage bufferImage, String code, int expireIn) {
		super(code);
		BufferImage = bufferImage;
		// 当前时间+过期时间长度(使用缓存之后此配置即不需要)
		this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
	}
	public BufferedImage getBufferImage() {
		return BufferImage;
	}
	public void setBufferImage(BufferedImage bufferImage) {
		BufferImage = bufferImage;
	}
	public boolean isExpried() {
		return LocalDateTime.now().isAfter(expireTime);
	}
}

图形验证码后续会进行修改,不会存储在session中。
ValidateCode只有一个code属性(存储验证码实际内容)。
图形验证码生成实现类ImageCodeGenerator:
代码有点长,当做一个图形验证码生成工具类即可。

package com.cong.security.core.code.image;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.util.Random;
import com.cong.security.core.code.ValidateCodeGenerator;
import com.cong.security.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

	private static char code[] = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789".toCharArray();

	@Autowired
	private SecurityProperties securityProperties;

	@Override
	public ImageCode generate() {
		// 在内存中创建图象
		BufferedImage image = new BufferedImage(securityProperties.getCode().getImage().getWidth(),
				securityProperties.getCode().getImage().getHeight(), BufferedImage.TYPE_INT_RGB);
		// 获取图形上下文
		Graphics g = image.getGraphics();
		// 生成随机类
		Random random = new Random();
		// 设定背景色
		g.setColor(getRandColor(200, 250));
		g.fillRect(0, 0, securityProperties.getCode().getImage().getWidth(),
				securityProperties.getCode().getImage().getHeight());
		// 设定字体
		g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
		// 设置颜色
		g.setColor(getRandColor(160, 200));
		// 随机产生100条干扰线,使图象中的认证码不易被其它程序探测到
		for (int i = 0; i < 100; i++) {
			int x = random.nextInt(securityProperties.getCode().getImage().getWidth());
			int y = random.nextInt(securityProperties.getCode().getImage().getHeight());
			int xl = random.nextInt(12);
			int yl = random.nextInt(12);
			g.drawLine(x, y, x + xl, y + yl);
		}
		// 取随机产生的认证码(4位数字)
		String sRand = "";
		for (int i = 0; i < securityProperties.getCode().getImage().getLength(); i++) {
			String rand = String.valueOf(code[random.nextInt(code.length)]);
			sRand += rand;
			// 将认证码显示到图象中
			g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
			// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
			g.drawString(rand, 13 * i + random.nextInt(6), random.nextInt(7) + 15);
		}
		// shear(g, securityProperties.getCode().getImage().getWidth(),
		// securityProperties.getCode().getImage().getHeight(), getRandColor(200, 200));// 使图片扭曲
		// 赋值验证码
		// 图象生效
		g.dispose();
		ImageCode imageCode = new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());
		return imageCode;
	}

	/*
	 * 给定范围获得随机颜色
	 */
	private static 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);
	}

	/**
	 * 使图形扭曲
	 * 
	 * @param g
	 * @param w1
	 * @param h1
	 * @param color
	 * @author single-聪
	 * @date 2019年11月12日
	 * @version 1.0.1
	 */
	private static void shear(Graphics g, int w1, int h1, Color color) {
		shearX(g, w1, h1, color);
		shearY(g, w1, h1, color);
	}

	/**
	 * 扭曲
	 * 
	 * @param g
	 * @param w1
	 * @param h1
	 * @param color
	 * @author single-聪
	 * @date 2019年11月12日
	 * @version 1.0.1
	 */
	private static void shearX(Graphics g, int w1, int h1, Color color) {
		Random random = new Random();
		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);
			}
		}
	}

	/**
	 * 扭曲
	 * 
	 * @param g
	 * @param w1
	 * @param h1
	 * @param color
	 * @author single-聪
	 * @date 2019年11月12日
	 * @version 1.0.1
	 */
	private static void shearY(Graphics g, int w1, int h1, Color color) {
		Random random = new Random();
		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);
			}
		}
	}
	public SecurityProperties getSecurityProperties() {
		return securityProperties;
	}
	public void setSecurityProperties(SecurityProperties securityProperties) {
		this.securityProperties = securityProperties;
	}
}

短信验证码生成实现类SmsCodeGenerator:

package com.cong.security.core.code.sms;

import com.cong.security.core.code.ValidateCode;
import com.cong.security.core.code.ValidateCodeGenerator;
import com.cong.security.core.properties.SecurityProperties;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component("smsCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator {

	@Autowired
	private SecurityProperties securityProperties;

	@Override
	public ValidateCode generate() {
		// 随机数字
		String code = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLength());
		ValidateCode validateCode = new ValidateCode(code);
		return validateCode;
	}
}

只是生成一个配置文件中指定长度的数字,过期时间等的设置需要在短信发送接口中设置(需要调用第三方短信接口,存在发送失败情况)

验证码调用接口CodeController(目前仅写图形验证码接口,后续在此控制器类中添加短信验证码接口):

package com.cong.security.core.code;

import java.io.IOException;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.cong.security.core.code.image.ImageCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

/**
 * 验证码接口,图形验证码,短信验证码之类
 * 
 * @author single-聪
 *
 */
@RestController
@RequestMapping("code")
public class CodeController {

	public static String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

	// 生成验证码接口
	@Autowired
	private ValidateCodeGenerator imageCodeGenerator;
	// 短信验证码接口
	@Autowired
	private ValidateCodeGenerator smsCodeGenerator;

	private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

	/**
	 * @Description 图形验证码生成
	 * @Param [request, response]
	 * @Author single-聪
	 * @Date 20:12 2020/1/10
	 * @Version 1.0.1
	 * @return void
	 **/
	@RequestMapping(value = "image")
	public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
		// 强转成imageCode
		ImageCode imageCode = (ImageCode) imageCodeGenerator.generate();
		sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
		// 图片写入IO响应中
		ImageIO.write(imageCode.getBufferImage(), "JPEG", response.getOutputStream());
	}
}

在安全配置中将该接口权限放开,允许所有人访问。
在这里插入图片描述
此处因为后续还会在此类中添加短信接口,所以放开以/code开始的所有接口,需要相应权限才能访问的接口应避免以/code开头。
编写HTML页面调用:

<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>
            <button type="submit">登录</button>
        </td>
    </tr>
</table>

运行项目即可看到图形验证码
在这里插入图片描述
开始使用验证码。
图形验证码过滤器ValidateCodeFilter.java:

package com.cong.security.core.code;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.cong.security.core.code.image.ImageCode;
import com.cong.security.core.properties.SecurityProperties;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import lombok.extern.slf4j.Slf4j;

/**
 * 继承OncePerRequestFilter,保证该过滤器只会被调用一次
 * 
 * @author single-聪
 *
 */
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {

	/* 失败处理器,成功直接进入下一过滤器,所以不需要成功处理器 */
	private AuthenticationFailureHandler myAuthenticationFailureHandler;

	/* 存放需要拦截的Url */
	private Set<String> urls = new HashSet<>();

	private SecurityProperties securityProperties;

	public void setMyAuthenticationFailureHandler(AuthenticationFailureHandler myAuthenticationFailureHandler) {
		this.myAuthenticationFailureHandler = myAuthenticationFailureHandler;
	}

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

	private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

	private AntPathMatcher antPathMatcher = new AntPathMatcher();

	@Override
	public void afterPropertiesSet() throws ServletException {
		super.afterPropertiesSet();
		log.info("配置图形验证码拦截路径[{}] ", securityProperties.getCode().getImage().getUrl());
		// 放入配置文件中配置
		String[] configUrls = StringUtils
				.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(), ",");
		for (String string : configUrls) {
			urls.add(string);
		}
	}

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		// 登录请求才起作用且必须是post请求
		log.info("图形验证码请求路径[{}] ", request.getRequestURI());
		// 判断用户登录请求是否需要先进行验证码校验
		boolean match = false;
		// 循环判断当前请求是否需要进行图形验证码校验
		for (String url : urls) {
			if (antPathMatcher.match(url, request.getRequestURI())) {
				match = true;
			}
		}
		// 如果需要进行验证码校验
		if (match) {
			try {
				log.info("开始校验图形验证码");
				validate(new ServletWebRequest(request));
			} catch (CodeException e) {
				// 图形验证码校验出现问题,走失败处理器(验证码成功是敲门砖,验证码失败不会走用户名密码登录,一方面降低数据库请求次数,一方面防止别人拿到登陆请求恶意攻击,造成数据库压力过大)
				myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
				return;
			}
		}
		filterChain.doFilter(request, response);
	}

	private void validate(ServletWebRequest request) throws ServletRequestBindingException {
		// 获取图形验证码信息,APP中的验证码值会从缓存中获取,完全抛弃session
		ImageCode imageCode = (ImageCode) sessionStrategy.getAttribute(request, CodeController.SESSION_KEY);
		// 
		String code = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
		log.info("imageCode值为[{}] ", imageCode);
		// 用户未输入值
		if (StringUtils.isBlank(code)) {
			throw new CodeException("图形验证码的值不能为空");
		}
		// session中不存在数据
		if (imageCode == null) {
			throw new CodeException("图形验证码不存在");
		}
		// 验证码超时过期,缓存中不存在也是过期
		if (imageCode.isExpried()) {
			sessionStrategy.removeAttribute(request, CodeController.SESSION_KEY);
			throw new CodeException("图形验证码已过期");
		}
		// 验证码是否匹配
		if (!StringUtils.equalsIgnoreCase(imageCode.getCode(), code)) {
			throw new CodeException("图形验证码不匹配");
		}
		// 验证成功,将验证码从session中移除
		sessionStrategy.removeAttribute(request, CodeController.SESSION_KEY);
	}
}

将ValidateCodeFilter放入过滤器链中,并且在UsernamePasswordAuthenticationFilter之后。
在这里插入图片描述
配置拦截请求:my.security.code.image.url:/login,以,分割即可拦截多个请求
此时调用登录页面输入错误的用户名密码,错误的图形验证码,返回值为图形验证码比配错误,并没有进入用户名密码校验模块,目的达成。
在这里插入图片描述

发布了43 篇原创文章 · 获赞 25 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/single_cong/article/details/103928281