前言

  • spring boot 版本: 2.7.3
  • 所有中文注释都为作者所加,源代码中并没有
  • 本文引用spring security官网的一些图片和介绍

spring security 基本原理

spring security 基于Servlet,通过Filter组成的FilterChain来过滤请求。
强烈建议先看spring security官网的介绍,这对你理解spring security 有很大的帮助。
这里我先介绍一些关键类或接口,强烈建议先看spring security官网的介绍。

  • SecurityContextHolder
    • Spring Security 身份验证模型的核心
    • 它包含SecurityContext securitycontextholder
  • SecurityContext
    • 从SecurityContextHolder中获取,SecurityContext中包含Authentication
  • Authentication
    • 用户未验证时,用于提供身份验证凭据(一般是username和password)
    • 认证成功时,提供当前经过身份验证的用户
    • 包含的属性
      • principal 未验证时是通常是username,验证后通常是UserDetails
      • credentials 通常是password。在许多情况下,这将在用户通过身份验证后被清除,以确保它不被泄露。
      • authorities GrantedAuthority的集合,一般是角色
  • GrantedAuthority
    • GrantedAuthority是授予用户的高级权限,一般是角色,如”ROLE_USER”、”ROLE_ADMIN”。
    • GrantedAuthority 可以从Authentication.getAuthorities()方法中得到。这个方法提供了一个GrantedAuthority的Collection对象。
  • AuthenticationManager
    • AuthenticationManager是定义 Spring Security 的过滤器如何执行身份验证的 API 。
    • AuthenticationManager接口默认的实现是ProviderManager
  • ProviderManager
    • ProviderManager是最常用的AuthenticationManager的实现类。ProviderManager管理着一列AuthenticationProvider。providermanage
    • 每个AuthenticationProvider都有机会表明身份验证应该成功、失败,或者表明它不能做出决定并允许下游AuthenticationProvider做出决定。如果配置的AuthenticationProvider都不能进行身份验证,则身份验证将失败,并抛出带有ProviderNotFoundException一个特殊AuthenticationException值,表明ProviderManager未配置为支持Authentication传递给它的类型。
  • AuthenticationProvider
    • 可以将多个AuthenticationProvider注入ProviderManager。每个AuthenticationProvider都可以执行特定类型的身份验证。例如,DaoAuthenticationProvider支持基于用户名/密码的身份验证,同时JwtAuthenticationProvider支持对 JWT 令牌进行身份验证。
  • AbstractAuthenticationProcessingFilter
    • AbstractAuthenticationProcessingFilter用作Filter验证用户凭据的基础。在认证凭证之前,Spring Security 通常使用AuthenticationEntryPoint。
    • 接下来,AbstractAuthenticationProcessingFilter可以对提交给它的任何身份验证请求进行身份验证。abstractauthenticationprocessingfilter

spring security 如何验证

基础的用户名+密码验证流程

请看spring security 默认的过滤器 security_filter_chain_list
spring security 默认的登录使用的是用户名+密码,主要的验证流程就是 抽象类AbstractAuthenticationProcessingFilter和实现类UsernamePasswordAuthenticationFilter这个过滤器。

第一步开启过滤链

1.将Servlet转换了HttpServlet
2.判断该路径是否需要验证
3.尝试验证
4.保存session
5.创建SecurityContext
6.如果失败,开启验证失败处理

AbstractAuthenticationProcessingFilter.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
43
44
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 转化为http请求
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 通过请求路径判断请求是否需要验证
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 尝试验证,具体实现在UsernamePasswordAuthenticationFilter类中
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
// 保存session
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 验证成功,保存Authentication,创建SecurityContext,开启验证成功处理
// 具体实现请看源代码
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
// 验证失败,清空securityContext,开启验证失败处理
// 具体实现请看源代码
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
// 验证失败,清空securityContext,开启验证失败处理
// 具体实现请看源代码
unsuccessfulAuthentication(request, response, ex);
}
}

UsernamePasswordAuthenticationFilter具体处理过程

1.从代码中可以看出,默认的登录路径为’/login’,请求方式只能为POST。
2.获取username和password
3.创建未验证的Authentication对象,设置Details
4.进行验证

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
43
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
"POST");

private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

private boolean postOnly = true;

public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}

public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
// 把username和password分别存入principal和credentials,并设置未验证
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
// spring security 默认的类是WebAuthenticationDetails
// 该类保存有两个属性remoteAddress(访问地址)和sessionId
setDetails(request, authRequest);
// 进行验证,成功就返回填充正确属性的Authentication的对线
// 如果验证失败,就进入异常处理阶段
// 具体流程请看下文AuthenticationManager 验证管理流程
return this.getAuthenticationManager().authenticate(authRequest);
}

AuthenticationManager 验证管理流程

AuthenticationManager接口的实现类为ProviderManager。

  1. 遍历AuthenticationProvider,先判断该AuthenticationProvider是否支持提供的Authentication
  2. 如果该AuthenticationProvider不支持提供的Authentication,就继续遍历
  3. 如果支持验证,则开启验证

ProviderManager.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
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
@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();
for (AuthenticationProvider provider : getProviders()) {
// 判断该AuthenticationProvider是否支持验证提供的Authentication
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
// 进行验证
// 请看AbstractUserDetailsAuthenticationProvider类中的authenticate方法(下文中介绍细节)
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;
}

AbstractUserDetailsAuthenticationProvider 验证流程

1.获取用户(先从缓存中获取, 如果缓存中没有就从数据库中拿)
2.如果数据库中没有用户,会抛出UsernameNotFoundException
3.拿到用户之后会检查密码是否正确,账号是否锁住,是否过期等等,如果出现问题会抛出异常
4.上述没有问题后,如果是从数据库拿的用户,则会把用户放入缓存
5.创建成功验证,返回Authentication

AbstractUserDetailsAuthenticationProvider.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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 判断Authentication 是否是UsernamePasswordAuthenticationToken的实例
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
// 从缓存中获取用户
UserDetails user = this.userCache.getUserFromCache(username);
// 缓存中没有用户,需要从数据库中获取用户
if (user == null) {
cacheWasUsed = false;
try {
// 获取用户,具体实现在实现类DaoAuthenticationProvider中
// 我们要实现的UserDetailsService的接口中的loadUserByUsername方法,就与这个方法有关
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 {
// 检查账号是否锁定、是否可用、是否过期
this.preAuthenticationChecks.check(user);
// 检查密码是否正确,错误会抛出BadCredentialsException异常
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);
}
// 检查用户凭证(密码)是否过期,这个主要看你自己在UserDetails怎么设置的
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.一般是通过我们实现UserDetailsService接口中的loadUserByUsername方法来检索用户
2.如果没有找到,则抛出UsernameNotFoundException异常
DaoAuthenticationProvider.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
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 这就是我们实现UserDetailsService接口中的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);
}
}

spring security 如何授权

spring security + jwt