述
上文中,实现了一个简单的图形验证码,但是这个是写死的, 图片的长宽,验证码的位数都是写死的,然后我们希望做成一个可配置的,就是由调用方去决定这些可变的参数
还有验证码拦截的接口,我们现在是只拦截登录请求,这个也可以做成一个可配置的,由调用方去决定,拦截哪些请求
最后就是把验证码的生成逻辑也做成可配置的,由我们的项目提供一个默认的生成逻辑,然后调用方可以去覆盖的这种
验证码参数可配置
跟之前配置过的登录页一样,首先我们的安全模块里面先给一个默认的配置, 然后调用安全模块的项目可以覆盖掉这个配置,最后,验证码的长和宽可以通过前端发的参数覆盖掉应用级的配置(不同的页面可能长宽不一样),如下图:
跟之前的登录页配置是一样的 ,这里我们需要一个放图形验证码的配置的配置类 ImageCodeProperties
,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22@Data
public class ImageCodeProperties {
/**
* 图片的宽度
*/
private int width = 160;
/**
* 图片的高度
*/
private int height = 40;
/**
* 验证码字符个数
*/
private int codeCount = 4;
/**
* 过期秒数
*/
private int expireIn = 60;
}
然后把这个类再包一层,新建一个 ValidateCodeProperties
,因为后面可能还有别的验证码,所以这里再包一层,代码如下:1
2
3
4
5
6
7
8
9
10@Data
public class ValidateCodeProperties {
/**
* 图片验证码配置
*/
private ImageCodeProperties imageCode = new ImageCodeProperties();
}
最后放到总的配置 SecurityProperties
中去,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14@Data
@ConfigurationProperties(prefix = "core.security")
public class SecurityProperties {
/**
* 浏览器配置
*/
private BrowserProperties browser = new BrowserProperties();
/**
* 验证码配置
*/
private ValidateCodeProperties code = new ValidateCodeProperties();
}
这样在调用方,也就是demo项目中的 application.yml
,就可以配置验证码的参数了, 如下:1
2
3
4
5core:
security:
code:
imageCode:
codeCount: 6
这里就是设置了验证码的长度是6位,我们默认的是4位
最后就是请求级的配置了,修改 ValidateCodeController
,修改后的代码如下: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@RestController
@Slf4j
public class ValidateCodeController {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("code/image")
public void getImageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 根据随机数生成数字
ImageCode imageCode = createImageCode(request);
// 将随机数存到缓存中
String redisKey = Constants.IMAGE_CODE_KEY_PREFIX + request.getSession().getId();
log.info("将验证码放到缓存中,redisKey:{}", request.getSession().getId());
redisTemplate.opsForValue().set(redisKey, imageCode);
// 将生成的图片写到接口的响应中
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
private ImageCode createImageCode(HttpServletRequest request) {
// 长宽先从请求中取
int width = ServletRequestUtils.getIntParameter(request, "width", securityProperties.getCode().getImageCode().getWidth());
int height = ServletRequestUtils.getIntParameter(request, "height", securityProperties.getCode().getImageCode().getHeight());
// 过期时间和长度不能通过请求指定
int codeCount = securityProperties.getCode().getImageCode().getCodeCount();
int expire = securityProperties.getCode().getImageCode().getExpireIn();
ValidateCode code = new ValidateCode(width, height, codeCount);
return new ImageCode(code.getBuffImg(), code.getCode(), expire);
}
}
这里主要是修改了 createImageCode()
方法,长和宽先从request中去取, 取不到的话,就从配置中取,就是我们之前写死的参数,都通过配置去拿
测试
到这儿,验证码的基本参数可配置就修改完成了,最后可以在页面中拿验证码的请求中加上参数,如下:1
<img src="/code/image?width=200">
这里请求中指定了验证码的长度是200, 然后上面 application.yml
中指定了验证码的长度是6, 然后启动项目,看下效果,如图:
可以看到,不论是请求的配置还是应用级的配置都生效了
验证码拦截接口可配置
在我们之前的 ValidateCodeFilter
中,是只拦截了一个 /authentication/from
这个登录的请求,我们在其他请求中,也可能需要用到图片验证码,所以这里的拦截可以做成一个可配置的拦截,下面来看一下具体的实现
首先在 ImageCodeProperties
增加一个属性1
2
3
4/**
* 要拦截的请求 逗号隔开
*/
private String urls;
这样的话,在应用中的 application.yml
中,就可以配置了, 如下:1
2
3
4
5core:
security:
code:
imageCode:
urls: /user/**,/test/*
这里我们使用了通配符去匹配
然后修改过滤器 ValidateCodeFilter
中的判断, 部分修改后的代码如下: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@Data
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
private AuthenticationFailureHandler authenticationFailureHandler;
private RedisTemplate redisTemplate;
private SecurityProperties securityProperties;
/**
* 要拦截的URL
*/
private Set<String> urls = new HashSet<>();
/**
* 用来匹配url
*/
private AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
// 登录是必须拦截的,直接加进去
urls.add("/authentication/from");
// 如果没有配置的话,return掉
if (StringUtils.isEmpty(securityProperties.getCode().getImageCode().getUrls())){
return;
}
String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImageCode().getUrls(), ",");
for (String configUrl : configUrls) {
urls.add(configUrl);
}
}
@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;
}
}
if (action) {
try {
// 这里做验证
validate(request);
} catch (ValidateCodeException exception) {
// 验证失败调用验证失败的处理
authenticationFailureHandler.onAuthenticationFailure(request, response, exception);
// 直接返回 不走下一个过滤器了
return;
}
}
doFilter(request, response, filterChain);
}
// ...省略 validate() 方法
}
这里首先是实现了一个 InitializingBean
目的是为了在其他的参数都组装完毕之后,初始化urls的值
然后重写了 afterPropertiesSet()
方法,里面把配置中的urls取出来,加到set集合中去
后面修改了doFilterInternal()
方法中的判断逻辑, 因为我们使用了通配符的这种,所以用到了 AntPathMatcher
去做验证
最后需要在配置类 BrowserSecurityConfig
中修改配置, 如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 @Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
validateCodeFilter.setRedisTemplate(redisTemplate);
// 本章加入的两个配置
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet();
// formLogin 表示表单认证
http
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
// ... 省略其他配置
}
就是把过滤器里面需要用到的 securityProperties
传了进去,然后调用了初始化属性的方法
测试
上面的配置都ok之后,就可以启动项目看下效果了, 我们在 application.yml
中配置的是 /user/**,/test/*
首先是 /user/me
接口,如下:
然后是 /test/1
接口,如下:
可覆盖的图形验证码逻辑
我们上面生成的图形验证码,逻辑是写死的,只能按照我们写死的逻辑来,现在我们想实现的就是,我们这个生成图片验证码的逻辑作为一个默认的方式,然后调用方可以自己去重写一个逻辑来覆盖我们的这个逻辑,下面看一下如何实现:
首先需要声明一个接口 ValidateCodeGenerator
,代码如下:1
2
3
4
5
6
7
8
9
10public interface ValidateCodeGenerator {
/**
* 生成图形验证码
* @param request
* @return
*/
ImageCode generate(HttpServletRequest request);
}
然后需要一个实现类,把实现图形验证码的代码都放在里面,新建 ImageCodeGenerator
, 代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20@Data
public class ImageCodeGenerator implements ValidateCodeGenerator {
private SecurityProperties securityProperties;
@Override
public ImageCode generate(HttpServletRequest request) {
// 长宽先从请求中取
int width = ServletRequestUtils.getIntParameter(request, "width", securityProperties.getCode().getImageCode().getWidth());
int height = ServletRequestUtils.getIntParameter(request, "height", securityProperties.getCode().getImageCode().getHeight());
// 过期时间和长度不能通过请求指定
int codeCount = securityProperties.getCode().getImageCode().getCodeCount();
int expire = securityProperties.getCode().getImageCode().getExpireIn();
ValidateCode code = new ValidateCode(width, height, codeCount);
return new ImageCode(code.getBuffImg(), code.getCode(), expire);
}
}
这里就是把controller中的逻辑都复制过来了,然后controller中的代码需要修改一下,如下: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@RestController
@Slf4j
public class ValidateCodeController {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ValidateCodeGenerator imageCodeGenerator;
@GetMapping("code/image")
public void getImageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 根据随机数生成数字
ImageCode imageCode = imageCodeGenerator.generate(request);
// 将随机数存到缓存中
String redisKey = Constants.IMAGE_CODE_KEY_PREFIX + request.getSession().getId();
log.info("将验证码放到缓存中,redisKey:{}", request.getSession().getId());
redisTemplate.opsForValue().set(redisKey, imageCode);
// 将生成的图片写到接口的响应中
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
}
就是把我们刚刚创建的生成器注入进来,这里调用就ok
到这儿,我们已经把验证码的生成逻辑放到一个接口的实现里面去了, 那如何把这个接口的实现改成可配置的,这里还需要加一个配置类,新建 ValidateCodeBeanConfig
,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14@Configuration
public class ValidateCodeBeanConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
@ConditionalOnMissingBean(name = "imageCodeGenerator")
public ValidateCodeGenerator imageCodeGenerator(){
ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
}
这里就是把我们的 ImageCodeGenerator
交给 Spring 去管理, 这里的重点是 @ConditionalOnMissingBean(name = "imageCodeGenerator")
这个注解的意思是,首先先会在容器中找一个名字是 imageCodeGenerator
的bean,找到的话就用找到的,找不到的话,再用我们下面方法中实例化的那个bean
有了这个注解就可以实现应用层的重写覆盖了
到这里就可以先看一下效果了, 因为我们并没有在应用层重写生成的逻辑,所以还是用的默认的逻辑,如下:
应用层覆盖实现逻辑
最后我们在应用层,也就是 demo
项目中,重新写一个图片验证码的生成器,新建一个类,实现我们的 ValidateCodeGenerator
接口,代码如下:1
2
3
4
5
6
7
8
9
10@Slf4j
@Component("imageCodeGenerator")
public class MyImageCodeGenerator implements ValidateCodeGenerator {
@Override
public ImageCode generate(HttpServletRequest request) {
log.info("这里实现应用层的验证码生成....");
// 此处省略
return null;
}
}
具体的生成逻辑这里就不写了,主要是@Component("imageCodeGenerator")
这个注解,这里的名字要和上面配置的 @ConditionalOnMissingBean(name = "imageCodeGenerator")
名字对应, 然后启动项目看下效果
控制台输出如下:
这里已经进入到我们的应用层中的逻辑了,因为这儿返回的是空所以会报空指针
总结
这里,我们的一个可配置的图形验证码的逻辑就实现了, 主要是以下几点
- 实现
InitializingBean
接口,重写afterPropertiesSet()
方法可以在其他的参数都组装完毕之后,做一些初始化的逻辑 - 使用
@Bean
注入一个bean的时候可以@ConditionalOnMissingBean
来做一个条件,如果条件不满足的情况下,才使用@Bean
注入