上一篇:https://lawsssscat.blog.csdn.net/article/details/105316362
实现图形验证码功能,三步:
- 开发生成图形验证码接口
- 在认证流程中加入图形认证码校验
- 重构代码
开发生成图形验证码接口
生成图形验证码
- 根据随机数生成图片
- 将随机数存到Session中
- 在将生成的图片写到接口的响应中
<mark>因为验证码不管是在浏览器或者是手机中都会用到,所以我们还是会写到 v-security-core 中</mark>
封装 ImageCode 类
包含 验证码图片的缓冲区(BufferedImage)、验证码(code)、过期时间(expire)
package cn.vshop.security.core.validate.code;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
/** * @author alan smith * @version 1.0 * @date 2020/4/5 1:17 */
@Getter
@Setter
@AllArgsConstructor
public class ImageCode {
/** * 图片,展示给用户看的 */
private BufferedImage image;
/** * 随机数,需要存在session中,作为验证的依据 */
private String code;
/** * 过期时间 * <p> * LocalDateTime 只存储如期,时间,不存储时区,JDK8后用以替代Date * 当时间不需要跟其他应用交互,建议使用。 */
private LocalDateTime expireTime;
/** * 构造函数 * * @param image 图片 * @param code 验证码 * @param expireIn 期望多长时间后过期(seconds) */
public ImageCode(BufferedImage image, String code, int expireIn) {
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
}
编写接口 GET /code/image
通过这个接口,前端可以获取二维码图片
<mark>在重构时候,会把这里写死的值抽离成参数</mark>
package cn.vshop.security.core.validate.code;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
/** * @author alan smith * @version 1.0 * @date 2020/4/5 1:33 */
@RestController
public class ValidateCodeController {
/** * 校验码信息存入session中的key */
private static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
/** * Spring 的工具类,用以操作session */
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 生成随机的验证码,并封装为 ImageCode
ImageCode imageCode = createImageCode(request);
// 获取session,并把键值对存入session
sessionStrategy.setAttribute(
// ServletWebRequest 是一个适配器(Adapter),把servlet封装成spring的WebRequest(继承了RequestAttributes)
// 通过把请求传进来,sessionStrategy会从请求中获取session
new ServletWebRequest(request),
// 存入session中的key
SESSION_KEY,
// 存入session中的值
imageCode);
// javax的io工具包
// 将BufferedImage以指定格式写入输出流中
ImageIO.write(
// 以BufferedImage类型的图片
imageCode.getImage(),
// 输出的图片格式
"JPEG",
// 输出流,输出到响应体中
response.getOutputStream());
}
private Random random = new Random();
/** * 生成验证码图片的逻辑代码 * * @param request 请求 * @return 将验证码图片封装为 ImageCode 返回 */
private ImageCode createImageCode(HttpServletRequest request) {
int width = 67;
int height = 23;
// 图片的缓冲区(buffer)
// BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 图片的画笔
// 产生Image对象的Graphics对象,该对象可以在图像上进行各种绘制操作
Graphics graphics = image.getGraphics();
/** * 背景颜色 */
// 设置画笔的颜色
graphics.setColor(getRandColor(200, 250));
// 用画笔填充图片
graphics.fillRect(0, 0, width, height);
// 绘制干扰线
// 设置画笔的颜色
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, xl, yl);
}
// 设置画笔的字体、粗细、大小
graphics.setFont(new Font("Times New Roman", Font.ITALIC, 20));
String sRand = "";
for (int i = 0; i < 4; 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();
// 将图片、字符串、过期时间封装成ImageCode
return new ImageCode(image, sRand, 60);
}
/** * 生成随机颜色 * * @param fc frontcolor * @param bc backcolor * @return 颜色实体 */
private Color getRandColor(int fc, int bc) {
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc - 16);
int g = fc + random.nextInt(bc - fc - 14);
int b = fc + random.nextInt(bc - fc - 18);
return new Color(r, g, b);
}
}
修改 browser 配置,使其不拦截 “/code/image” 接口
修改登录界面
使其请求接口,显示图片
访问页面
http://localhost:8080/login2.html
在认证流程中加入图形验证码校验
如何添加图形验证码校验?
最直接的方式是在 用户名密码验证过滤器前添加图形校验码校验过滤器
编写 ValidateCodeFilter
package cn.vshop.security.core.validate.code;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.method.P;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
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 javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/** * @author alan smith * @version 1.0 * @date 2020/4/5 11:39 */
public class ValidateCodeFilter
// SpringMVC 提供的工具类,能确保此Filter每次请求只被调用一次
extends OncePerRequestFilter {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
// 我们前面自定义的验证失败处理器
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 如果请求的是执行登录URL
if (StringUtils.equals("/authentication/form", request.getRequestURI())
// 并且是POST请求
&& StringUtils.equalsIgnoreCase(request.getMethod(), "post")) {
// 尝试校验
try {
// 封装成spring的request,方便后续操作
validate(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
// 捕获到自定义的验证码校验异常
// 用我们之前自定义的错误处理器,进行校验失败的处理
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return ;
}
}
// 校验通过or不是登录请求
filterChain.doFilter(request, response);
}
/** * 校验的逻辑 * * @param request */
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
// 从session中获取封装好的ImageCode
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
// 从request中获取请求参数imageCode
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码的值不能为空");
}
if (codeInSession == null) {
throw new ValidateCodeException("验证码不存在");
}
if (codeInSession.isExpired()) {
// 如果过期了,就移除验证码
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 void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
}
ImageCode 涉及到的过期判断
自定义验证码校验异常
package cn.vshop.security.core.validate.code;
import org.springframework.security.core.AuthenticationException;
/** * 验证码校验异常 * * @author alan smith * @version 1.0 * @date 2020/4/5 11:54 */
public class ValidateCodeException
// Spring Security所有校验异常的基类
extends AuthenticationException {
public ValidateCodeException(String msg) {
super(msg);
}
}
在 BrowserSecurityConfig 中注册过滤器
测试
来到登录页面
http://localhost:8080/login2.html
登录失败
直接点登录,会报错,并且能看到错误信息
输入正确验证码
去到了UsernamePasswordAuthenticationFilter
正常登陆
访问成功
如果错误时,返回的不是json
可能是没有把返回的类型改为JSON
如果返回的信息过多
可以用自定义的 SimpleResponse 只封装想返回的错误信息
重构代码
下面重构代码,使代码更加灵活(能被引用)
- 验证码基本参数可配置
如:
- 验证码拦截的接口可配置
除了说接口可变外,还要可以让不同服务能选择性的使用验证码
如:
- 验证码的生成逻辑可配置
有的情况可能需要更复杂的验证码,(如能使用不同字符,限定校验次数等)
# 图形验证码基本参数配置
- 默认配置
即如果使用安全模块,不做其他配置,默认使用写在 core 里面的配置 - 应用及的配置
如果使用了安全模块的demo做了一些配置,可以覆盖上面的默认配置 - 请求级配置
配置值在请求接口中传递,可以在调用请求接口时候覆盖上面的配置
<mark>这也是做通用代码的通用逻辑,能在不同级别中做不同的配置的覆盖</mark>
编写 ValidateCodeProperties 和 ImageCodeProperties
ValidateCodeProperties 包含 ImageCodeProperties
前者包含 手机验证码和图形验证码的配置(即包含后者)
后者包含 图形验证码配置
package cn.vshop.security.core.properties;
import lombok.Getter;
import lombok.Setter;
/** * 图形验证码 * * @author alan smith * @version 1.0 * @date 2020/4/5 14:47 */
@Getter
@Setter
public class ImageCodeProperties {
/** * 验证码宽 */
private int width = 67;
/** * 验证码高 */
private int height = 23;
/** * 验证码个数 */
private int length = 4;
/** * 验证按失效时间 */
private int expireIn = 60;
}
package cn.vshop.security.core.properties;
import lombok.Getter;
import lombok.Setter;
/** * 验证码配置 * 包含: * 1. 图形验证码 * 2. 短信验证码 * * @author alan smith * @version 1.0 * @date 2020/4/5 14:54 */
@Getter
@Setter
public class ValidateCodeProperties {
private ImageCodeProperties image = new ImageCodeProperties();
}
修改 application.yml 配置
为了能看出效果,我们把图形验证码的长度改为 6
v:
security:
code:
image:
length: 6
修改 ValidateCodeController
在ValidateCodeController中使用上面写好的配置信息
package cn.vshop.security.core.validate.code;
import cn.vshop.security.core.properties.SecurityProperties;
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.ServletRequestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
/** * @author alan smith * @version 1.0 * @date 2020/4/5 1:33 */
@RestController
public class ValidateCodeController {
@Autowired
private SecurityProperties securityProperties;
/** * 校验码信息存入session中的key */
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
/** * Spring 的工具类,用以操作session */
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 生成随机的验证码,并封装为 ImageCode
ImageCode imageCode = createImageCode(request);
// 获取session,并把键值对存入session
sessionStrategy.setAttribute(
// ServletWebRequest 是一个适配器(Adapter),把servlet封装成spring的WebRequest(继承了RequestAttributes)
// 通过把请求传进来,sessionStrategy会从请求中获取session
new ServletWebRequest(request),
// 存入session中的key
SESSION_KEY,
// 存入session中的值
imageCode);
// javax的io工具包
// 将BufferedImage以指定格式写入输出流中
ImageIO.write(
// 以BufferedImage类型的图片
imageCode.getImage(),
// 输出的图片格式
"JPEG",
// 输出流,输出到响应体中
response.getOutputStream());
}
private Random random = new Random();
/** * 生成验证码图片的逻辑代码 * * @param request 请求 * @return 将验证码图片封装为 ImageCode 返回 */
private ImageCode createImageCode(HttpServletRequest request) {
// 验证码图片宽度
// 借助工具类,中request中获取
int width = ServletRequestUtils.getIntParameter(
// 从请求中分析width参数,从而获取width值
request, "width",
// 如果请求中没有值,就从配置中获取
securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(
request, "height",
securityProperties.getCode().getImage().getHeight());
// 验证码长度
int length = securityProperties.getCode().getImage().getLength();
// 验证码有效时间
int expiredIn = securityProperties.getCode().getImage().getExpireIn();
// 图片的缓冲区(buffer)
// BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 图片的画笔
// 产生Image对象的Graphics对象,该对象可以在图像上进行各种绘制操作
Graphics graphics = image.getGraphics();
/** * 背景颜色 */
// 设置画笔的颜色
graphics.setColor(getRandColor(200, 250));
// 用画笔填充图片
graphics.fillRect(0, 0, width, height);
// 绘制干扰线
// 设置画笔的颜色
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, xl, yl);
}
// 设置画笔的字体、粗细、大小
graphics.setFont(new Font("Times New Roman", Font.ITALIC, 20));
String sRand = "";
for (int i = 0; i < length; 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();
// 将图片、字符串、过期时间封装成ImageCode
return new ImageCode(image, sRand, expiredIn);
}
/** * 生成随机颜色 * * @param fc frontcolor * @param bc backcolor * @return 颜色实体 */
private Color getRandColor(int fc, int bc) {
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc - 16);
int g = fc + random.nextInt(bc - fc - 14);
int b = fc + random.nextInt(bc - fc - 18);
return new Color(r, g, b);
}
}
修改登录页
最后,修改图片请求,看请求长度是否有效果
测试
来到登录页面
http://localhost:8080/login2.html
# 验证码拦截的接口可配置
<mark>我们希望能指定哪些接口需要验证码校验,哪些接口不需要验证码校验</mark>
修改 application.yml
希望下面的配置生效,即***会对下面指定的接口进行拦截和验证码的校验
v:
security:
code:
image:
# 指定要进行验证码验证的url
url:
- /user
- /user/*
在 ImageCodeProperties 中添加
/** * 要进行图形验证码校验的url */
private String[] urls ;
修改 ValidateCodeFilter
-
添加接口 InitializingBean : 可以在其他配置注入完成后,通过afterPropertiesSet方法进行初始化
(因为这个Filter不在Spring容器中,所以InitializingBean 实际不生效,但我们可以手动调用它的方法实现) -
添加属性:urls 指定要过滤哪些url
-
添加属性:securityProperties 把配置中指定的urls注入(需要手动注入,因为该Filter不在Spring容器中)
-
通过修改方法doFilterInternal,修改过滤条件。(通过Spring的工具类AntPathMatcher)
package cn.vshop.security.core.validate.code;
import cn.vshop.security.core.properties.SecurityProperties;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.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 javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
/** * 图形校验码的过滤器 * * @author alan smith * @version 1.0 * @date 2020/4/5 11:39 */
@Slf4j
@Setter
public class ValidateCodeFilter
// SpringMVC 提供的工具类,能确保此Filter每次请求只被调用一次
extends OncePerRequestFilter
// 在其他参数都加载完毕时,组装URLs的值
implements InitializingBean {
/** * 要拦截的url */
private Set<String> urls = new HashSet<>();
private SecurityProperties securityProperties;
/** * spring的工具类,用来匹配Ant风格路径,如:“/user/*” */
private AntPathMatcher pathMatcher = new AntPathMatcher();
/** * 操作session的工具类 */
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
// 我们前面自定义的验证失败处理器
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
public void afterPropertiesSet() throws ServletException {
// 父类实现了
super.afterPropertiesSet();
String[] configUrls = securityProperties.getCode().getImage().getUrls();
for (String url : configUrls) {
urls.add(url);
}
urls.add("/authentication/form");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 循环判断是否执行过滤
boolean action = false;
for (String url : urls) {
if (pathMatcher.match(url, request.getRequestURI())) {
action = true;
break;
}
}
// 如果请求的是执行登录URL
if (action) {
// 尝试校验
try {
// 封装成spring的request,方便后续操作
validate(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
// 捕获到自定义的验证码校验异常
// 用我们之前自定义的错误处理器,进行校验失败的处理
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
// 校验通过or不是登录请求
filterChain.doFilter(request, response);
}
/** * 校验的逻辑 * * @param request */
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
// 从session中获取封装好的ImageCode
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
// 从request中获取请求参数imageCode
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码的值不能为空");
}
if (codeInSession == null) {
throw new ValidateCodeException("验证码不存在");
}
if (codeInSession.isExpired()) {
// 如果过期了,就移除验证码
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
// 然后再抛异常
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
}
}
修改 BrowserSecurityConfig 配置
测试
当我们把配置中的路径注释掉
等看到(下图),说明配置生效了
# 验证码的生成逻辑可配置
<mark>要把一段逻辑做成可以配置的,我们需要把这段代码封装到一个接口的后面</mark>
就像我们前面实现 Spring Security 的一些逻辑(如登录成功、失败逻辑)一样,我们需要实现 Spring Security 的一些接口
因此,我们需要生成一个接口,让别人能覆盖我们的逻辑,同时给出一个默认的实现
<mark>其实就是把之前写在Controller里面的代码,抽离出来,作为一个接口的实现</mark>
生成接口 ValidateCodeGenerator
package cn.vshop.security.core.validate.code;
import javax.servlet.http.HttpServletRequest;
/** * 校验码生成器 * * @author alan smith * @version 1.0 * @date 2020/4/5 17:25 */
public interface ValidateCodeGenerator {
/** * 生成验证码图片的逻辑代码 * * @param request 请求 * @return 将验证码图片封装为 ImageCode 返回 */
ImageCode createImageCode(HttpServletRequest request);
}
给出默认实现 ImageCodeGenerator
把Controller 里面相关的代码挪进来
package cn.vshop.security.core.validate.code;
import cn.vshop.security.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.ServletRequestUtils;
import javax.servlet.http.HttpServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
/** * @author alan smith * @version 1.0 * @date 2020/4/5 17:30 */
public class ImageCodeGenerator implements ValidateCodeGenerator {
@Autowired
private SecurityProperties securityProperties;
private Random random = new Random();
@Override
public ImageCode createImageCode(HttpServletRequest request) {
// 验证码图片宽度
// 借助工具类,中request中获取
int width = ServletRequestUtils.getIntParameter(
// 从请求中分析width参数,从而获取width值
request, "width",
// 如果请求中没有值,就从配置中获取
securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(
request, "height",
securityProperties.getCode().getImage().getHeight());
// 验证码长度
int length = securityProperties.getCode().getImage().getLength();
// 验证码有效时间
int expiredIn = securityProperties.getCode().getImage().getExpireIn();
// 图片的缓冲区(buffer)
// BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 图片的画笔
// 产生Image对象的Graphics对象,该对象可以在图像上进行各种绘制操作
Graphics graphics = image.getGraphics();
/** * 背景颜色 */
// 设置画笔的颜色
graphics.setColor(getRandColor(200, 250));
// 用画笔填充图片
graphics.fillRect(0, 0, width, height);
// 绘制干扰线
// 设置画笔的颜色
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, xl, yl);
}
// 设置画笔的字体、粗细、大小
graphics.setFont(new Font("Times New Roman", Font.ITALIC, 20));
String sRand = "";
for (int i = 0; i < length; 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();
// 将图片、字符串、过期时间封装成ImageCode
return new ImageCode(image, sRand, expiredIn);
}
/** * 生成随机颜色 * * @param fc frontcolor * @param bc backcolor * @return 颜色实体 */
private Color getRandColor(int fc, int bc) {
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc - 16);
int g = fc + random.nextInt(bc - fc - 14);
int b = fc + random.nextInt(bc - fc - 18);
return new Color(r, g, b);
}
}
修改 ValidateCodeController
将Controller原本的代码抽离到 接口实现中后,注入那个接口
package cn.vshop.security.core.validate.code;
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.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/** * @author alan smith * @version 1.0 * @date 2020/4/5 1:33 */
@RestController
public class ValidateCodeController {
/** * 校验码信息存入session中的key */
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
/** * 注入接口,接口的实现完成验证码的生成和封装 */
@Autowired
private ValidateCodeGenerator validateCodeGenerator ;
/** * Spring 的工具类,用以操作session */
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 生成随机的验证码,并封装为 ImageCode
ImageCode imageCode = validateCodeGenerator.createImageCode(request);
// 获取session,并把键值对存入session
sessionStrategy.setAttribute(
// ServletWebRequest 是一个适配器(Adapter),把servlet封装成spring的WebRequest(继承了RequestAttributes)
// 通过把请求传进来,sessionStrategy会从请求中获取session
new ServletWebRequest(request),
// 存入session中的key
SESSION_KEY,
// 存入session中的值
imageCode);
// javax的io工具包
// 将BufferedImage以指定格式写入输出流中
ImageIO.write(
// 以BufferedImage类型的图片
imageCode.getImage(),
// 输出的图片格式
"JPEG",
// 输出流,输出到响应体中
response.getOutputStream());
}
}
编写配置类 ValidateCodeBeanConfig
通过配置类,使接口的实现注入到spring容器中,从而实现controller中的注入
package cn.vshop.security.core.validate.code;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/** * 校验码生成逻辑的配置类 * * @author alan smith * @version 1.0 * @date 2020/4/5 17:39 */
@Configuration
public class ValidateCodeBeanConfig {
/** * 如果用户没有注入名字为imageCodeGenerator的bean,那么就使用默认的 * * @return 默认的 ValidateCodeGenerator 实现 */
@Bean("imageCodeGenerator")
@ConditionalOnMissingBean(name = "imageCodeGenerator")
public ValidateCodeGenerator imageCodeGenerator() {
return new ImageCodeGenerator();
}
}
<mark>至此,就实现了验证码的生成可配置</mark>
下面,就尝试覆盖原有的生成逻辑
其他形式验证码,参考:SpringBoot——登录验证码实现
这里就不展示了
package cn.vshop.security.core.validate.code;
import cn.vshop.security.core.properties.SecurityProperties;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/** * @author alan smith * @version 1.0 * @date 2020/4/5 19:44 */
// 名字必须为 imageCodeGenerator。
// 当然,也可以自定义覆盖规则
@Component("imageCodeGenerator")
public class DemoImageCodeGenerator implements ValidateCodeGenerator, InitializingBean {
@Autowired
private SecurityProperties securityProperties;
private ImageCodeGenerator imageCodeGenerator;
@Override
public void afterPropertiesSet() throws Exception {
imageCodeGenerator = new ImageCodeGenerator();
imageCodeGenerator.setSecurityProperties(securityProperties);
}
@Override
public ImageCode createImageCode(HttpServletRequest request) {
System.out.println("更高级的图形验证码生成代码");
return imageCodeGenerator.createImageCode(request);
}
}
<mark>这里体现一种代码思想,即以增量的方式来适应变化。</mark>
当有代码有新的需求,不是重写原有代码,而是借助提供好的接口,在原有代码上添加新增的业务逻辑
done~