述
上文中,已经把短信登录流程的代码写好了,但是这些都没有加到 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
41public 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
30public 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 | /** |
首先通过是哪个发送器,来确定类型. 然后还是之前的验证步骤
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
这里可以看到,我们的配置都是左右都是可以分成几类的,然后这里就可以都拆分出来,就跟最后的 短信登录的配置一样,单独抽出来,然后这里通过 apply()
方法去引用
我们现在这个类是浏览器的配置类,然后之后后APP的配置的话,有通用的地方也是直接 apply()
连过去就好了
AbstractChannelSecurityConfig
首先新建一个配置类,里面放网页登录的配置,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public 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()
的方式去引入