SpringSecurity 原理初探(三)

1、HttpSecurity 常用配置

  1. **authorizeRequests()**:这是配置请求授权规则的入口点。通常,你会以 authorizeRequests() 方法开始配置,然后定义哪些请求需要进行身份验证和授权。
  2. **antMatchers()**:使用 antMatchers() 方法定义需要授权的 URL 模式。例如,.antMatchers("/admin/**").hasRole("ADMIN") 表示任何以 “/admin/“ 开头的 URL 需要用户具有 “ADMIN” 角色才能访问。
  3. **permitAll()**:permitAll() 方法用于配置允许所有用户访问的请求,即不需要身份验证。
  4. **authenticated()**:authenticated() 方法指示所有已经通过身份验证的用户都可以访问,无需特定的角色或权限。
  5. hasRole() 和 **hasAuthority()**:这些方法用于配置需要具有特定角色或权限的用户才能访问的请求。例如,.hasRole("USER") 表示需要用户具有 “USER” 角色。
  6. **formLogin()**:formLogin() 方法启用基于表单的身份验证。它会自动生成登录页面和处理登录请求。
  7. **loginPage()**:loginPage() 方法允许你指定自定义的登录页面的 URL。
  8. **loginProcessingUrl()**:loginProcessingUrl() 方法指定处理登录请求的 URL。
  9. **failureUrl()**:failureUrl() 方法定义登录失败后重定向到的 URL。
  10. **logout()**:logout() 方法配置登出功能,可以定义登出 URL 和登出成功后的重定向 URL。
  11. **rememberMe()**:rememberMe() 方法配置 “记住我” 功能,允许用户在下一次访问时保持登录状态。
  12. **csrf()**:csrf() 方法配置跨站请求伪造(CSRF)保护,可以启用或禁用 CSRF 保护。
  13. **sessionManagement()**:sessionManagement() 方法用于配置会话管理策略,例如最大会话数和会话过期策略。
  14. **exceptionHandling()**:exceptionHandling() 方法允许配置身份验证和授权失败时的处理方式,例如重定向到自定义错误页面或返回特定错误响应。

2、登录认证流程

image-20231030024545191

通过自定义SpringSecurity,在内存中存放一个用户信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//访问请求的授权规则配置
.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
//用户信息
.inMemoryAuthentication()
.withUser("admin")
.password("{noop}123456")
.authorities(new ArrayList<>());
}

大致流程概述:在登录认证时,请求会先进入过滤器,再使用Provider认证处理,最后进行成功/失败处理器处理

2.1、AbstractAuthenticationProcessingFilter

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
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {

....


private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//判断是否需要身份认证,判断是post请求还是get
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
try {
//调用子类方法
Authentication authenticationResult = this.attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}

//session身份认证此略
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//成功处理器,在这个处理器中会调用SecurityContextHolder.setContext(),将用户信息存入其中
this.successfulAuthentication(request, response, chain, authenticationResult);
} catch (InternalAuthenticationServiceException var5) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var5); //失败处理器
this.unsuccessfulAuthentication(request, response, var5);
} catch (AuthenticationException var6) {
//失败处理器
this.unsuccessfulAuthentication(request, response, var6);
}

}
}

....


}

==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
//AbstractAuthenticationProcessingFilter类的子类
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//判断是否是post请求
if (this.postOnly && !request.getMethod().equals("POST")) {
//如果不是则抛出异常
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
//得到前端穿过来的用户名和密码
String username = this.obtainUsername(request);
username = username != null ? username : "";
username = username.trim();
String password = this.obtainPassword(request);
password = password != null ? password : "";
//将用户名和密码封装成一个UsernamePasswordAuthenticationToken对象 Authentication接口的实现类
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
//调用ProviderManager中的authenticate进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}


}


//接受传进来的用户名和密码
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
//未认证
setAuthenticated(false);
}

image-20231030160817064

2.2、ProviderManager

AuthenticationManager 身份验证管理器 接口的实现类,

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
public class ProviderManager implements AuthenticationManager{

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
//循环所有的 `provider`
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
//provider,DaoAuthenticationProvider
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
//身份认证通过后擦除凭据
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}

return result;
}

// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}



}

image-20231030162009298

  • 这里循环所有的 provider ,每个provider都调用自己的 supports() 方法判断是否支持认证,能支持认证的话就执行自己的 authenticate() 方法进行认证
  • 左边的 ProviderManager 就是这些策略的委托类,所有的provider都会被收集到该类的 providers 属性中,然后认证的时候由委托类循环所有策略,支持认证的话再调用对应的策略去认证。

image-20231030162418482

image-20231030163121146

最后会调用DaoAuthenticationProvider中的authenticate认证方法

2.3、AbstractUserDetailsAuthenticationProvider

DaoAuthenticationProvider的父类

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
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
//获取用户名
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
//从缓存中获取
//缓存是通过存储放置在UserCache中的UserDetails对象来处理的。这确保了可以验证具有相同用户名的后续请求,而无需查询UserDetailsService。需要注意的是,如果用户似乎提供了不正确的密码,则会查询UserDetailsService以确认使用了最新的密码进行比较。只有无状态应用程序才可能需要缓存。例如,在普通的web应用程序中,SecurityContext存储在用户的会话中,并且不会对每个请求重新验证用户。因此,默认的缓存实现是NullUserCache。
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//如果缓存中没有则调用此方法,调用子类的retrieveUser方法,返回内存中的用户信息
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
//检查返回的UserDetails实例的状态,是否过期,是否被禁用。。,如果被禁用或者过期则抛出异常
this.preAuthenticationChecks.check(user);
//校验密码,将authentication中的密码和内存中的密码进行比对,分析见下
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
//判断用户凭据是否过期
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
//将用户信息放到缓存中
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//分析见下
return createSuccessAuthentication(principalToReturn, authentication, user);
}

}

校验密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//其他身份检查
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//判断前端传过来的密码是否为空
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
//得到前端传过来的密码
String presentedPassword = authentication.getCredentials().toString();

//检验密码,默认的密码编码器为 ,DelegatingPasswordEncoder 委派密码编码器,这个编码器会根据内存中密码的前缀类型委托给
//相应类型的编码器处理
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}

最终会调用DelegatingPasswordEncoder中的matches方法校验

image-20231030171701245

封装返回值,并更新内存中的密码,在第二次请求中内存中的密码返回的是加密后的密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
//判断是否需要升级编码
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
//将新密码更新到内存中并返回
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}

image-20231030174825747

==DaoAuthenticationProvider==

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 class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {


@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
//判断密码格式
prepareTimingAttackProtection(); //$2a$10$KtHCJh.LuZzH5Mphxc7Dl.axwqGBzzyRJOtsYXiniKT/bO39GGK7O
try {
//调用 InMemoryUserDetailsManager类中的loadUserByUsername方法查询内存中的用户信息
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}

}

2.4、InMemoryUserDetailsManager

1
2
3
4
5
6
7
8
9
10
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名从内存中查询用户信息
UserDetails user = this.users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}对象返回
//将用户信息封装成一个UserDetails
return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}

内存中的用户信息

image-20231030165713295

2.5、总结

通过fromLogin这种方式登录时会在UsernamePasswordAuthenticationFilter过滤器中进行身份认证

  1. 在处理登录认证时会将前端的用户信息封装成一个UsernamePasswordAuthenticationToken对象传给ProviderManager中的authenticate()方法

    1. 在这个方法中会循环每provider,每个provider都调用自己的 supports() 方法判断是否支持认证,能支持认证的话就执行自己的 authenticate() 方法进行认证
    2. 通过循环后找到DaoAuthenticationProvider策略类
  2. 然后会调用AbstractUserDetailsAuthenticationProvider(DaoAuthenticationProvider的父类)的authenticate方法在这个方法中会将封装的用户信息传递给子类的retrieveUser方法,调用子类的方法进行处理

  3. 调用DaoAuthenticationProvider类的retrieveUser方法,然后调用InMemoryUserDetailsManager类中loadUserByUsername方法,通过username从内存中查询用户信息,

  4. 从内存中获取的用户信息会封装成一个UserDetails对象最后会返回给AbstractUserDetailsAuthenticationProvider

  5. 在AbstractUserDetailsAuthenticationProvider的authenticatn()方法中,会对UserDetails进行检查,判断用户是否被锁定,是否被禁用,调用additionalAuthenticationChecks方法对密码进行校验,最后会把返回得UserDetails放入缓存中,最后调用createSuccessAuthentication方法,如果内存中得密码是明文的话,会将密码进行加密处理修改到内存中,最后封装成一个UsernamePasswordAuthenticationToken对象返回

  6. DelegatingPasswordEncoder默认的密码编码器,委托密码编码器,在matches方法中会匹配内存中密码带有{ 前缀 },根据前缀获得对应的编码器进行处理,比如在内存中的密码是以{noop}的前缀,则会从map集合中根据noop 这个关键字得到NoOpPasswordEncoder编码器,这个编码器不会对密码进行处理

  7. 最后如果认证成功就会调用成功处理器将内存中的用户信息放到SecurityContextHolder中,并进行页面重定向