Spring Security | Note-7

Spring Security Note-7


实现短信验证码登录

开发短信验证码接口
生成验证码接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class ValidateCodeController {
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private ValidateCodeGenerator smsCodeGenerator;
@Autowired
private SmsCodeSender smsCodeSender;

@GetMapping("/code/sms")
public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletRequestBindingException {
ValidateCode smsCode = smsCodeGenerator.generate(new ServletWebRequest(request));
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, smsCode);
String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
smsCodeSender.send(mobile, smsCode.getCode());
}
}
图片验证码 & 短信验证码实体改造
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ValidateCode {
private String code;
private LocalDateTime expireTime;

public ValidateCode(String code, int expireIn) {
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}

public ValidateCode(String code, LocalDateTime expireTime) {
this.code = code;
this.expireTime = expireTime;
}
public boolean isExpired(){
return LocalDateTime.now().isAfter(expireTime);
}
}
继承关系ImageCode
1
2
3
4
5
6
7
8
9
10
11
public class ImageCode extends ValidateCode {
private BufferedImage image;
public ImageCode(BufferedImage image, String code, int expireIn) {
super(code, expireIn);
this.image = image;
}
public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
super(code, expireTime);
this.image = image;
}
}
验证码生成接口改造
1
2
3
public interface ValidateCodeGenerator {
ValidateCode generate(ServletWebRequest request);
}
Bean配置类装配
1
2
3
4
5
6
7
8
@Configuration
public class ValidateCodeBeanConfig {
@Bean
@ConditionalOnMissingBean(SmsCodeSender.class) // 条件
public SmsCodeSender smsCodeSender() {
return new DefaultSmsCodeSender();
}
}
短信验证码模拟实现(默认)
1
2
3
4
5
6
7
8
9
public class DefaultSmsCodeSender implements SmsCodeSender {
/**
* 模拟SMS发生手机验证码
*/
@Override
public void send(String mobile, String code) {
System.out.println("向手机:" + mobile + ". 发送短信验证码" + code);
}
}
短信验证码生成器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component("smsCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator {
// 短信验证码生成器
@Autowired
private SecurityProperties securityProperties;

@Override
public ValidateCode generate(ServletWebRequest request) {
String code = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLength());
return new ValidateCode(code, securityProperties.getCode().getSms().getExpireIn());
}

public SecurityProperties getSecurityProperties() {
return securityProperties;
}

public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
参数配置实体改造
1
2
3
4
5
6
7
8
public class SmsCodeProperties {
// 验证码位数
private int length = 6;
// 超时时间
private int expireIn = 60;
// 用逗号分隔的拦截接口
private String url;
}
配置添加
1
2
3
4
public class ValidateCodeProperties {
private ImageCodeProperties image = new ImageCodeProperties();
private SmsCodeProperties sms = new SmsCodeProperties();
}
改造重构

1
2
3
4
5
6
7
8
9
10
11
12
public interface ValidateCodeProcessor {
/**
* 验证码放入session时的前缀
*/
String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_";

/**
* 创建校验码
* ServletWebRequest封装请求和相应
*/
void create(ServletWebRequest request) throws Exception;
}
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
public abstract class AbstractValidateCodeProcessor<C extends ValidateCode> implements ValidateCodeProcessor {

/**
* 操作session的工具类
*/
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

/**
* 收集系统中所有的 {@link ValidateCodeGenerator} 接口的实现。
*/
@Autowired
private Map<String, ValidateCodeGenerator> validateCodeGenerators;

@Override
public void create(ServletWebRequest request) throws Exception {
C validateCode = generate(request);
save(request, validateCode);
send(request, validateCode);
}

/**
* 生成校验码
*/
@SuppressWarnings("unchecked")
private C generate(ServletWebRequest request) {
String type = getProcessorType(request);
ValidateCodeGenerator validateCodeGenerator = validateCodeGenerators.get(type + "CodeGenerator");
return (C) validateCodeGenerator.generate(request);
}

/**
* 保存校验码
*/
private void save(ServletWebRequest request, C validateCode) {
sessionStrategy.setAttribute(request, SESSION_KEY_PREFIX + getProcessorType(request).toUpperCase(), validateCode);
}

/**
* 发送校验码,由子类实现
*/
protected abstract void send(ServletWebRequest request, C validateCode) throws Exception;

/**
* 根据请求的url获取校验码的类型
*/
private String getProcessorType(ServletWebRequest request) {
return StringUtils.substringAfter(request.getRequest().getRequestURI(), "/code/");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class ValidateCodeController {
@Autowired
private Map<String, ValidateCodeProcessor> validateCodeProcessors;

/**
* 创建验证码,根据验证码类型不同,调用不同的 {@link ValidateCodeProcessor}接口实现
*/
@GetMapping("/code/{type}")
public void createCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type) throws Exception {
validateCodeProcessors.get(type + "CodeProcessor").create(new ServletWebRequest(request, response));
}

}

校验短信验证码并登录

SmsAuthenticationFilter接受请求生成SmsAuthenticationToken

然后交给系统的AuthenticationManager进行管理,然后找到SmsAuthenticationProvider

然后再调用UserDetailsService进行短信验证;

验证码的验证是在请求之前进行验证码过滤验证的;

SmsCodeAuthenticationToken 模仿 UsernamePasswordAuthenticationToken
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
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// 存放认证信息
private final Object principal;
// ~ Constructors
/**
* 认证之前存放手机号
*/
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
/**
* 认证成功存放用户
*/
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
// ~ Methods
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}

super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
@Override
public Object getCredentials() {
// TODO Auto-generated method stub
return null;
}
}
SmsCodeAuthenticationFilter 模仿 UsernamePasswordAuthenticationFilter
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
/**
* 短信登录的过滤器
*/
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// 请求中携带的参数名
public static final String IMOOC_FORM_MOBILE_KEY = "mobile";
// 请求中携带参数的名字是什么
private String mobileParameter = IMOOC_FORM_MOBILE_KEY;
// 是否仅处理post请求
private boolean postOnly = true;
// ~ Constructors
// 处理的短信登录的请求是什么
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}
// ~ Methods
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
// 将请求的信息设置在Token中
setDetails(request, authRequest);
// 拿着Token调用AuthenticationManager
return this.getAuthenticationManager().authenticate(authRequest);
}

/**
* 获取请求参数中的手机号
*/
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}

/**
* 将请求的信息设置在Token中
*/
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}

public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}

public final String getMobileParameter() {
return mobileParameter;
}
}
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
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;

/**
* 手机验证码身份验证逻辑
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
if (user == null) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user,user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}

/**
* AuthenticationManager带着Token调用Provider
* 判断传进来的Token最终调用的是哪个Provider
*/
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
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
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
private Set<String> urls = new HashSet<>();
private SecurityProperties securityProperties;
private AntPathMatcher pathMatcher = new AntPathMatcher();

@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getSms().getUrl(), ",");
for (String configUrl : configUrls) {
urls.add(configUrl);
}
urls.add("/authentication/mobile");
}

@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(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
filterChain.doFilter(request, response);
}

private void validate(ServletWebRequest request) throws ServletRequestBindingException {
ValidateCode codeInSession = (ValidateCode) sessionStrategy.getAttribute(request, ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码不能为空");
}
if (codeInSession == null) {
throw new ValidateCodeException("验证码不存在");
}
if (codeInSession.isExpired()) {
sessionStrategy.removeAttribute(request, ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(request, ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
}

public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}

public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
is.securityProperties = securityProperties;
}
}
配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain,HttpSecurity>{
@Autowired
private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
@Autowired
private UserDetailsService userDetailsService;

@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
// Manager
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// Handler
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);

SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
安全配置类
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
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

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

// 配置TokenRepository
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
// SMS Validate
SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
smsCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
smsCodeFilter.setSecurityProperties(securityProperties);
smsCodeFilter.afterPropertiesSet();
// 表单登录
http.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
...
.apply(smsCodeAuthenticationSecurityConfig);
}
}

重构

重构代码过多,不予展示,详见GITHUB;