Spring Security | Note-6

Spring Security Note-6


认证流程源码详解

认证处理流程说明

当一个登录请求进入到过滤器链开始到整个认证完成;

1.发送登录请求,进入到UsernamePasswordAuthenticationFilter的类中,它将用户名和密码封装成UsernamePasswordAuthenticationToken的对象,设置认证状况为false,并且将请求的信息发送到details当中,接下来将进入到AuthenticationManager

2.AuthenticationManager本身不参与认证的作用,它主要作为一个类似工具的作用,主要起到认证逻辑的对象是集合AuthenticationProvider,对于不同的用户方式,它的认证逻辑是不同的,例如用户名+密码,微信;

3.不同的Provider支持的类型是不一样的,通过循环找到支持的类型;

4.通过UserDetailsService的逻辑,获得具体的UserDetails对象并返回;

5.做完预检查之后,还有一个附加检查,通过PasswordEncoder再次密码的校验,完成之后,最后有一个后检查,所有的检查通过之后,认为这个用户认证是成功的,就可以获取用户的信息;

6.将用户信息拼装,作为一个已认证的Authentication返回;

认证结果如何在多个请求之间共享

在完成认证的流程之后;

在调用认证成功处理器之前,有一个SecurityContextHolder.getContext().setAuthentication(authResult),它将完成认证的Authentication放入到SecurityContext当中;

在整个处理过程中,都能通过SecurityContextHolder读取已认证的Authentication;

SecurityContextPersistenceFilter在整个过滤器链请求的最前端,在响应的最后一端;

检查在Session中是否有SecurityContext,如果在Session中有,那么将取出来放入SecurityContextHolder当中,如果没有;

在线程SecurityContextHolder当中如果有SecurityContext,那么取出来放入Session当中;

获取认证用户信息

SecurityContextHolder获取认证用户的信息;

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/me")
public Object getCurrentUser(){
return SecurityContextHolder.getContext().getAuthentication();
}
// 另一种写法
@GetMapping("/me")
public Object getCurrentUser(@AuthenticationPrincipal UserDetails user){
return user;
}
}

实现图形验证码功能

开发生成图形验证码接口

步骤:根据随机数生成图片;将随机数存到Session中;将生成的图片写到接口的响应中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ImageCode {
private BufferedImage image;
private String code;
private LocalDateTime expireTime;
public ImageCode(BufferedImage image, String code, int expireIn) {
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
this.image = image;
this.code = code;
this.expireTime = expireTime;
}
public boolean isExpired(){
return LocalDateTime.now().isAfter(expireTime);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class ValidateCodeController {
private static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = createImageCode(request);
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}

private ImageCode createImageCode(HttpServletRequest request) {
...
}

private Color getRandColor(int fc, int bc) {
...
}

}
在认证流程中加入图形验证码校验

整合Spring Security校验,自定义一个Filter,将该Filter设置在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
public class ValidateCodeFilter extends OncePerRequestFilter {
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (StringUtils.equals("/authentication/form", request.getRequestURI()) && StringUtils.equalsIgnoreCase(request.getMethod(), "post")) {
try {
validate(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(request,response,e);
}
}
filterChain.doFilter(request, response);
}

private void validate(ServletWebRequest request) throws ServletRequestBindingException {
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request,ValidateCodeController.SESSION_KEY);
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(),"imageCode");
if(StringUtils.isBlank(codeInRequest)){
throw new ValidateCodeException("验证码不能为空");
}
if(codeInSession == null){
throw new ValidateCodeException("验证码不存在");
}
if(codeInSession.isExpired()){
sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("验证码已过期");
}
if(!StringUtils.equals(codeInSession.getCode(),codeInRequest)){
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationFailureHandler imoocAuthenticationFailHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailHandler);
// 表单登录
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
...
}
}
重构代码

将图形验证码的基本参数配置,以三级模式的形式,层层覆盖;

上层是请求级配置,配置值在调用接口时传递;

中层是应用级配置,配置值在demo中,可被请求级覆盖;

下层是默认配置,配置值在core中,可被应用级覆盖;

验证码基本参数可配置
1
2
3
4
5
6
7
8
9
10
public class ImageCodeProperties {
// 图片宽度
private int width = 67;
// 图片高度
private int height = 23;
// 验证码位数
private int length = 4;
// 超时时间
private int expireIn = 60;
}
1
2
3
public class ValidateCodeProperties {
private ImageCodeProperties image = new ImageCodeProperties();
}
1
2
3
4
5
@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
private ValidateCodeProperties code = new ValidateCodeProperties();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
public class ValidateCodeController {
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private SecurityProperties securityProperties;


private ImageCode generate(ServletWebRequest request) {
int width = ServletRequestUtils.getIntParameter(request.getRequest(),"width",securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request.getRequest(),"height",securityProperties.getCode().getImage().getHeight());
...
String code = "";
for (int i = 0; i < securityProperties.getCode().getImage().getLength(); i++) {
...
}
g.dispose();
return new ImageCode(image, code, securityProperties.getCode().getImage().getExpireIn());
}
}
验证码拦截接口可配置
1
2
3
4
public class ImageCodeProperties {
// 用逗号分隔的拦截接口
private String url;
}
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
public class ValidateCodeFilter 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().getImage().getUrl(), ",");
for(String configUrl:configUrls){
urls.add(configUrl);
}
urls.add("/authentication/form");
}

@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);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler imoocAuthenticationFailHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailHandler);
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet();
...
}
}
1
2
3
security.code.image.length=6
security.code.image.width=100
security.code.image.url=/user,/user/*
验证码生成逻辑可配置(以增量的方式,实现变化)
1
2
3
public interface ValidateCodeGenerator {
ImageCode generate(ServletWebRequest request);
}
1
2
3
4
5
6
7
8
9
10
11
public class ImageCodeGenerator implements ValidateCodeGenerator {
@Autowired
private SecurityProperties securityProperties;
@Override
public ImageCode generate(ServletWebRequest request) {
...
}
private Color getRandColor(int fc, int bc) {
...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class ValidateCodeBeanConfig {
@Autowired
private SecurityProperties securityProperties;

@Bean
@ConditionalOnMissingBean(name = "imageCodeGenerator") // 条件
public ValidateCodeGenerator imageValidateCodeGenerator() {
ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
}

“记住我”功能

1
2
3
4
public class BrowserProperties {
// 记住我超时时间
private int rememberMeSeconds = 3600;
}
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
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler imoocAuthenticationFailHandler;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;

@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 {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailHandler);
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet();
// 表单登录
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin().loginPage("/authentication/require").loginProcessingUrl("/authentication/form")
// 登录成功与失败的处理
.successHandler(imoocAuthenticationSuccessHandler).failureHandler(imoocAuthenticationFailHandler)
// 记住我的配置
.and().rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()).userDetailsService(userDetailsService)
// 默认
.and()
// 都需要认证
.authorizeRequests()
// 当访问以下URL,不需要身份认证
.antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage(), "/code/image").permitAll()
// 任何请求
.anyRequest()
// 认证后才能访问
.authenticated().and().csrf().disable();
}
}
原理

用户发送认证请求到UsernamePasswordAuthenticationFilter,当Spring Security认证成功之后,调用一个RememberMeService的服务,这个服务里存在一个TokenRepositoryTokenRepository会生成一个Token,并且将这个Token写入浏览器中的Cookie中,同时TokenRepository将生成的Token与当前的用户身份,写入到数据库中;

当再次发送请求时,不需要重新登录,而是直接访问一个受保护的服务,这个请求不再走过滤器链中的UsernamePasswordAuthenticationFilter,而是通过过滤器链中的RemberMeAuenticationFilter去获取用户信息,直接读取Cookie中的Token给到RememberMeServiceTokenRepository将会根据获取到的Token到数据库中查询对应的用户信息和Token是否匹配和存在;

如果有记录,就会把用户名取出来,取出来之后会调用UserDetailsService,获取用户信息,然后把用户信息放入到SecurityContext当中;