SpringBoot 2整合SpringSecurity权限管理(三)增加验证码验证功能

概述

图片验证码是当今使用最广泛的用户验证方式之一,在之前实现了简单的表单登录的基础上,今天用Spring Security来实现一下图片验证码的实现过程。

Spring Security的本质是一系列的过滤器组成的过滤器链,我们只需要自定义一个过滤器并将其插入到Spring Security的过滤器链上,就可以执行我们自定义的认证逻辑了。

20180523203835339

一、新建验证码异常类,继承AuthenticationException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 自定义验证码异常类
* 声明一个验证码异常,用于抛出特定的验证码异常
*
* @author ZHANGCHAO
* @date 2020/3/13 11:16
* @since 1.0.0
*/
public class VerifyCodeException extends AuthenticationException {

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

二、新建验证码过滤器VerifyCodeFilter,继承OncePerRequestFilter,保证过滤器每次请求只会被调用一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 验证码过滤器
*
* @author ZHANGCHAO
* @date 2020/3/13 11:17
* @since 1.0.0
*/
@Slf4j
@Component
public class VerifyCodeFilter extends OncePerRequestFilter {

@Autowired
private LoginFailureHandler loginFailureHandler;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request.getRequestURI().equals("/login") && request.getMethod().equalsIgnoreCase("post")) {
try {
validate(request);
} catch (VerifyCodeException e) {
loginFailureHandler.onAuthenticationFailure(request,response,e);
return;
}
}
// 检验通过,放行!
filterChain.doFilter(request,response);
}

/**
* 验证保存在session的验证码和表单提交的验证码是否一致
*
* @Author ZHANGCHAO
* @Date 2020/3/13 11:30
* @param
* @param request
* @retrun void
**/
private void validate(HttpServletRequest request) throws ServletRequestBindingException {
String captcha = ServletRequestUtils.getStringParameter(request,"captcha");
String code = (String) request.getSession().getAttribute(request.getParameter("uuid"));
log.info("获取提交的captcha: "+captcha);
log.info("获取session保存的code: "+code);
if (!captcha.equalsIgnoreCase(code)){
throw new VerifyCodeException("验证码不正确!");
}
//清除session中的验证码
request.getSession().removeAttribute(request.getParameter("uuid"));
}
}

三、在SecurityConfig配置类中注入刚才新建的验证码过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //需要授权的请求
.antMatchers("/login","/home","/getCode").permitAll() //过滤不需要认证的路径
.anyRequest().authenticated() //对任何一个请求,都需要认证
.and() //完成上一个配置,进行下一步配置
//.httpBasic();
.formLogin() //配置表单登录
.loginPage("/login") //设置登录页面
.successHandler(successHandler) /* 设置成功处理器 */
.failureHandler(failureHandler) /* 设置失败处理器*/
.and()
.logout() //登出
.logoutSuccessUrl("/home"); //设置退出页面
// 添加验证码过滤器
http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
}

将定义好的过滤器插入到UsernamePasswordAuthenticationFilter前面,来验证图片验证码。

四、新增验证码工具类和VO类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/**
* 验证码工具类
*
* @author ZHANGCHAO
* @date 2020/3/13 11:45
* @since 1.0.0
*/
public class VerifyCodeUtil {

//使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
private static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static Random random = new Random();


/**
* 使用系统默认字符源生成验证码
* @param verifySize 验证码长度
* @return
*/
public static String generateVerifyCode(int verifySize){
return generateVerifyCode(verifySize, VERIFY_CODES);
}

/**
* 使用指定源生成验证码
* @param verifySize 验证码长度
* @param sources 验证码字符源
* @return
*/
public static String generateVerifyCode(int verifySize, String sources){
if(sources == null || sources.length() == 0){
sources = VERIFY_CODES;
}
int codesLen = sources.length();
Random rand = new Random(System.currentTimeMillis());
StringBuilder verifyCode = new StringBuilder(verifySize);
for(int i = 0; i < verifySize; i++){
verifyCode.append(sources.charAt(rand.nextInt(codesLen-1)));
}
return verifyCode.toString();
}

/**
* 输出指定验证码图片流
* @param w
* @param h
* @param os
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
int verifySize = code.length();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Random rand = new Random();
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
Color[] colors = new Color[5];
Color[] colorSpaces = new Color[] { Color.WHITE, Color.CYAN,
Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
Color.PINK, Color.YELLOW };
float[] fractions = new float[colors.length];
for(int i = 0; i < colors.length; i++){
colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
fractions[i] = rand.nextFloat();
}
Arrays.sort(fractions);

g2.setColor(Color.GRAY);// 设置边框色
g2.fillRect(0, 0, w, h);

Color c = getRandColor(200, 250);
g2.setColor(c);// 设置背景色
g2.fillRect(0, 2, w, h-4);

//绘制干扰线
Random random = new Random();
g2.setColor(getRandColor(160, 200));// 设置线条的颜色
for (int i = 0; i < 20; i++) {
int x = random.nextInt(w - 1);
int y = random.nextInt(h - 1);
int xl = random.nextInt(6) + 1;
int yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}

// 添加噪点
float yawpRate = 0.05f;// 噪声率
int area = (int) (yawpRate * w * h);
for (int i = 0; i < area; i++) {
int x = random.nextInt(w);
int y = random.nextInt(h);
int rgb = getRandomIntColor();
image.setRGB(x, y, rgb);
}

shear(g2, w, h, c);// 使图片扭曲

g2.setColor(getRandColor(100, 160));
int fontSize = h-4;
Font font = new Font("Algerian", Font.ITALIC, fontSize);
g2.setFont(font);
char[] chars = code.toCharArray();
for(int i = 0; i < verifySize; i++){
AffineTransform affine = new AffineTransform();
affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize/2, h/2);
g2.setTransform(affine);
g2.drawChars(chars, i, 1, ((w-10) / verifySize) * i + 5, h/2 + fontSize/2 - 10);
}

g2.dispose();
ImageIO.write(image, "jpg", os);
}

private static Color getRandColor(int fc, int bc) {
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);
}

private static int getRandomIntColor() {
int[] rgb = getRandomRgb();
int color = 0;
for (int c : rgb) {
color = color << 8;
color = color | c;
}
return color;
}

private static int[] getRandomRgb() {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
rgb[i] = random.nextInt(255);
}
return rgb;
}

private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}

private static void shearX(Graphics g, int w1, int h1, Color color) {
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);
}
}

}

private static void shearY(Graphics g, int w1, int h1, Color color) {
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);
}
}
}
}

五、在controller中增加方法,在登录页面显示验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@GetMapping("/getCode")
public Object getVerifyCode(HttpServletRequest request){
/* 生成验证码字符串 */
String verifyCode = VerifyCodeUtil.generateVerifyCode(4);
String uuid = IdUtil.fastSimpleUUID();
System.out.println("verifyCode:"+verifyCode);
System.out.println("uuid:"+uuid);
request.getSession().setAttribute(uuid,verifyCode);
int w = 111, h = 36;

try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
VerifyCodeUtil.outputImage(w,h,stream,verifyCode);
return new ResponseMessage(true, "200", "success", new ImgVO("data:image/gif;base64,"+ Base64Utils.encodeToString(stream.toByteArray()),uuid));
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author ZHANGCHAO
* @date 2020/3/13 13:18
* @since 1.0.0
*/
@Data
public class ImgVO {

private String img;
private String uuid;

public ImgVO(String img, String uuid) {
this.img = img;
this.uuid = uuid;
}
}

注意要在SecurityConfig配置类的antMatchers中过滤不需要认证的路径中加入“/getCode”

六、登录页面login加上验证码

TIM截图20200313155032

启动项目测试:

333

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2020 yak33
  • Powered by Hexo Theme Ayer
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信