Spring Security-12-短信登录配置及验证逻辑重构

上文中,已经把短信登录流程的代码写好了,但是这些都没有加到 Spring Security 中, 所以并没有生效, 下面看一下如何将我们自定义的这个登录流程配置到 Spring Security 中去

配置

这个登录方式,可能会在多个环境下用到,所以这里把配置类写到core项目里面去, 新建类 SmsAuthenticationSecurityConfig :

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
@Component
public class SmsAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;

@Autowired
private AuthenticationFailureHandler myAuthenticationFailureHandler;

@Autowired
private UserDetailsService userDetailsService;

@Override
public void configure(HttpSecurity http) throws Exception {
// 过滤器的配置
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
// 设置AuthenticationManager
smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// 设置登录成功失败的处理
smsAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
smsAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);

// Provider的配置
SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
smsAuthenticationProvider.setUserDetailsService(userDetailsService);

http.authenticationProvider(smsAuthenticationProvider)
.addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}

首先继承了 SecurityConfigurerAdapter ,然后重写 configure(HttpSecurity http) 方法,里面首先配置过滤器, 设置 AuthenticationManager, 还有登录成功失败的处理器

下面就是我们自己的 Provider 的配置, 最后是把这个 Provider 加到 AuthenticationManager 里面去, 再把我们的过滤器加到 UsernamePasswordAuthenticationFilter 的后面去

这个配置是在core里面的配置,web环境并没有, 然后我们需要把这段配置引到web环境中去, BrowserSecurityConfig 中, 修改如下:

1
2
3
4
5
6
7
8
9
@Autowired
private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;

@Override
protected void configure(HttpSecurity http) throws Exception {
// ... 省略其他diamante
.csrf().disable()
.apply(smsAuthenticationSecurityConfig);
}

这里重点就是 .apply() 方法,他可以把其他地方的配置也引过来,就相当于在这个方法里写一样

常量配置

我们系统中有一些常量,比如说 登录的url, 参数名, 等等. 这些常量都可以放在一个地方统一的管理. core项目里面 新建一个类 SecurityConstants

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
public class SecurityConstants {
/**
* 验证图片验证码时,http请求中默认的携带图片验证码信息的参数的名称
*/
public static final String DEFAULT_PARAMETER_NAME_CODE_IMAGE = "image";

/**
* 验证短信验证码时,http请求中默认的携带短信验证码信息的参数的名称
*/
public static final String DEFAULT_PARAMETER_NAME_CODE_SMS = "sms";

/**
* 默认的处理验证码的url前缀
*/
public static final String DEFAULT_VALIDATE_CODE_URL_PREFIX = "/code";

/**
* 当请求需要身份认证时,默认跳转的url
*/
public static final String DEFAULT_UNAUTHENTICATION_URL = "/authentication/require";

/**
* 默认的用户名密码登录请求处理url
*/
public static final String DEFAULT_LOGIN_PROCESSING_URL_FORM = "/authentication/form";

/**
* 默认的手机验证码登录请求处理url
*/
public static final String DEFAULT_LOGIN_PROCESSING_URL_MOBILE = "/authentication/mobile";

/**
* 默认登录页面
*/
public static final String DEFAULT_LOGIN_PAGE_URL = "/login.html";

/**
* 发送短信验证码 或 验证短信验证码时,传递手机号的参数的名称
*/
public static final String DEFAULT_PARAMETER_NAME_MOBILE = "phoneNo";
}

然后把我们之前直接写的都替换掉就好了,之后有什么常量也都写在这个里面

验证逻辑重构

之前一直没有写验证逻辑,图形验证码和短信验证码的验证逻辑其实是一样的,所以这个验证也是可以封装起来的

ValidateCodeType修改

首先是 ValidateCodeType ,加一个方法,就是通过枚举类型,获取参数名称,比如是图片类型,那枚举就是IMAGE,通过方法拿到参数名,就是我们上面常量中配置的 image, 修改后的代码如下:

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
public enum ValidateCodeType implements Serializable {

/**
* 短信验证码
*/
SMS{
@Override
public String getParamNameOnValidate() {
return SecurityConstants.DEFAULT_PARAMETER_NAME_CODE_SMS;
}
},

/**
* 图片验证码
*/
IMAGE{
@Override
public String getParamNameOnValidate() {
return SecurityConstants.DEFAULT_PARAMETER_NAME_CODE_IMAGE;
}
},
;

/**
* 校验时从请求中获取的参数的名字
* @return
*/
public abstract String getParamNameOnValidate();

}

ValidateCodeProcessorHolder

然后新建一个类 ValidateCodeProcessorHolder ,这个类用来通过验证码类型获取验证码发送器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class ValidateCodeProcessorHolder {

/**
* 收集系统中的所有{@link ValidateCodeProcessor} 的实现
*/
@Autowired
private Map<String, ValidateCodeProcessor> validateCodeProcessors;

public ValidateCodeProcessor findValidateCodeProcessor (ValidateCodeType type) {
return findValidateCodeProcessor(type.name().toLowerCase());
}

public ValidateCodeProcessor findValidateCodeProcessor(String type) {
String name = type.toLowerCase() + ValidateCodeProcessor.class.getSimpleName();

ValidateCodeProcessor processor = validateCodeProcessors.get(name);
if (processor == null) {
throw new ValidateCodeException("验证码处理器" + name + "不存在");
}
return processor;
}
}

这里主要就是收集所有的 ValidateCodeProcessorHolder 然后通过类型获取具体的处理器

这里,我们去 map 中取处理器的key是 type + ValidateCodeProcessor.class.getSimpleName() 所以说.我们的图片验证码处理器 应该改为 imageValidateCodeProcessor,然后短信验证码应该改为 smsValidateCodeProcessor

ValidateCodeProcessor

然后是 ValidateCodeProcessor, 这个类中之前只有一个发送验证码的方法,然后现在再加一个校验的方法

1
2
3
4
5
6
/**
* 校验验证码
* @param request
* @throws ServletRequestBindingException
*/
void validate(HttpServletRequest request) throws ServletRequestBindingException;

AbstractValidateCodeProcessor

AbstractValidateCodeProcessor 需要去实现我们上面的验证的具体的逻辑

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
/**
* 根据请求的url获取校验码的类型
* @param request
* @return ValidateCodeType
*/
private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
String type = StringUtils.substringBefore(getClass().getSimpleName(), "CodeProcessor");
return ValidateCodeType.valueOf(type.toUpperCase());
}

@Override
public void validate(HttpServletRequest request) throws ServletRequestBindingException {
// 获取验证的类型
ValidateCodeType processorType = getValidateCodeType(request);
// 获取redis中存的key的值
String redisKey = getRedisKey(request);

// 从缓存中拿出来
C codeInCache = (C) redisTemplate.opsForValue().get(redisKey);

// 然后是请求中的验证码
String codeInRequest;
try {
codeInRequest = ServletRequestUtils.getStringParameter(request, processorType.getParamNameOnValidate());
} catch (ServletRequestBindingException e) {
throw new ValidateCodeException("获取验证码的值失败");
}

if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码的值不能为空");
}

if (codeInCache == null) {
throw new ValidateCodeException("验证码不存在");
}

if (codeInCache.expired()) {
redisTemplate.delete(redisKey);
throw new ValidateCodeException("验证码已过期");
}

if (!StringUtils.equalsIgnoreCase(codeInCache.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码错误");
}

// 最后通过的话也从缓存清除掉
redisTemplate.delete(redisKey);

}

首先通过是哪个发送器,来确定类型. 然后还是之前的验证步骤

ValidateCodeFilter

最后是 filter 的修改,修改后的代码如下:

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
@Data
@Slf4j
@Component("validateCodeFilter")
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {

@Autowired
private ValidateCodeProcessorHolder validateCodeProcessorHolder;

@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;

@Autowired
private RedisTemplate redisTemplate;

@Autowired
private SecurityProperties securityProperties;

/**
* 存放所有需要校验验证码的url
*/
private Map<String, ValidateCodeType> urlMap = new HashMap<>();

/**
* 用来匹配url
*/
private AntPathMatcher pathMatcher = new AntPathMatcher();

@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();

// 首先,登录请求必须验证.直接放进去
urlMap.put(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM, ValidateCodeType.IMAGE);
urlMap.put(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, ValidateCodeType.SMS);
// 不同的表单做不同的处理
addUrlToMap(securityProperties.getCode().getImageCode().getUrls(), ValidateCodeType.IMAGE);
addUrlToMap(securityProperties.getCode().getSmsCode().getUrls(), ValidateCodeType.SMS);
}

/**
* 从配置中拆出路径来放到url里面
* @param urlString
* @param type
*/
protected void addUrlToMap(String urlString, ValidateCodeType type) {
if (StringUtils.isBlank(urlString)) {
return;
}

String[] urls = StringUtils.splitByWholeSeparatorPreserveAllTokens(urlString, ",");
for (String url : urls) {
urlMap.put(url, type);
}
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("进入验证码校验的filter.....");
try {
// 这里做验证
validate(request);
} catch (ValidateCodeException exception) {
// 验证失败调用验证失败的处理
authenticationFailureHandler.onAuthenticationFailure(request, response, exception);
// 直接返回 不走下一个过滤器了
return;
}

doFilter(request, response, filterChain);
}

private void validate(HttpServletRequest request) throws ServletRequestBindingException {

ValidateCodeType type = getValidateCodeType(request);
if (type == null){
return;
}
log.info("校验请求[{}]中的验证码,验证码类型是[{}]", request.getRequestURI(), type);

validateCodeProcessorHolder.findValidateCodeProcessor(type).validate(request);

}

/**
* 获取校验码的类型,如果当前请求不需要校验,则返回null
* @param request
* @return
*/
private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
ValidateCodeType result = null;
if (StringUtils.equalsIgnoreCase(request.getMethod(), "get")) {
return null;
}

Set<String> urls = urlMap.keySet();
for (String url : urls) {
if (pathMatcher.match(url, request.getRequestURI())) {
result = urlMap.get(url);
}
}

return result;
}

}

这里的 urlMap 对应的是他们的 要拦截的url和对应的验证码类型,总体的逻辑还是没有变的,只是处理两个类型的验证码

发送验证码的接口的修改

ValidateCodeController 做了一点改动,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@Slf4j
public class ValidateCodeController {

@Autowired
private ValidateCodeProcessorHolder validateCodeProcessorHolder;

@GetMapping(SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/{type}")
public void getCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type) throws IOException, ServletRequestBindingException {
ValidateCodeProcessor processor = validateCodeProcessorHolder.findValidateCodeProcessor(type);
processor.createCode(request, response);
}

}

登录页面修改

常量配置中,默认接收的图片验证码参数名是 image 然后短信的是 sms ,页面对应的input框里面需要把参数的名字对应上

到这儿两种类型登录就都可以使用了,可以启动项目看下效果

首先,路径更新成了常量,然后这里获取验证码处理器就用了我们上面的 ValidateCodeProcessorHolder

配置重构

先来看一下我们现在的配置类 BrowserSecurityConfig
image

这里可以看到,我们的配置都是左右都是可以分成几类的,然后这里就可以都拆分出来,就跟最后的 短信登录的配置一样,单独抽出来,然后这里通过 apply() 方法去引用

我们现在这个类是浏览器的配置类,然后之后后APP的配置的话,有通用的地方也是直接 apply() 连过去就好了

AbstractChannelSecurityConfig

首先新建一个配置类,里面放网页登录的配置,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AbstractChannelSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
protected AuthenticationSuccessHandler myAuthenticationSuccessHandler;

@Autowired
protected AuthenticationFailureHandler myAuthenticationFailureHandler;

protected void applyPasswordAuthenticationConfig(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)
.loginProcessingUrl(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM)
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler);
}

}

ValidateCodeSecurityConfig

再新建一个配置类放验证码的配置,如下:

1
2
3
4
5
6
7
8
9
10
11
@Component("validateCodeSecurityConfig")
public class ValidateCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

@Autowired
private Filter validateCodeFilter;

@Override
public void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
}
}

总的配置修改

最后 BrowserSecurityConfig 里面把这两个引进来, 首先 BrowserSecurityConfig 需要继承 AbstractChannelSecurityConfig 这个里面放着用户名密码的配置, 是必须要有的 ,修改后的代码如下:

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
@Configuration
public class BrowserSecurityConfig extends AbstractChannelSecurityConfig {


@Autowired
private SecurityProperties securityProperties;

@Autowired
private UserDetailsService userDetailsService;

@Autowired
private DataSource dataSource;

@Autowired
private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;

@Autowired
private ValidateCodeSecurityConfig validateCodeSecurityConfig;

@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}

@Override
protected void configure(HttpSecurity http) throws Exception {

// web网页登录的配置
applyPasswordAuthenticationConfig(http);

http
.apply(validateCodeSecurityConfig)
.and()
.apply(smsAuthenticationSecurityConfig)
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
.userDetailsService(userDetailsService)
.and()
.authorizeRequests()
// 匹配的是登录页的话放行
.antMatchers(
SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*",
securityProperties.getBrowser().getLoginPage())
.permitAll()
// 授权请求. anyRequest 就表示所有的请求都需要权限认证
.anyRequest().authenticated()
.and()
.csrf().disable()
;

}
}

浏览器的配置就直接写在了这里,其他可以通用的配置就抽出去

总结

首先是一个验证码的重构, 通过验证码的类型去判断是那种处理,这样只需要一个filter就可以做这两种判断,之后如果再有别的类型也很好扩展

配置的重构主要就是把一些公用的配置全部抽出来,通过 apply() 的方式去引入

本文代码传送门