# 一、认证会用到的相关请求

# 1.1、获取 access_token 请求(/oauth/token)

  • 请求所需参数:client_id、client_secret、grant_type、username、password
http://localhost/oauth/token?client_id=demoClientId&client_secret=demoClientSecret&grant_type=password&username=demoUser&password=50575tyL86xp29O380t1

# 1.2、检查头肯是否有效请求(/oauth/check_token)

  • 请求所需参数:token
http://localhost/oauth/check_token?token=f57ce129-2d4d-4bd7-1111-f31ccc69d4d1

# 1.3、刷新 token 请求(/oauth/token)

  • 请求所需参数:grant_type、refresh_token、client_id、client_secret 其中 grant_type 为固定值:grant_type=refresh_token
http://localhost/oauth/token?grant_type=refresh_token&refresh_token=fbde81ee-f419-42b1-1234-9191f1f95be9&client_id=demoClientId&client_secret=demoClientSecret

# 二、工作原理

# 2.1、结构总结

Spring Security 所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截, 校验每个请求是否能够访问它所期望的资源。我们可以通过 Filter 或 AOP 等技术来实现,Spring Security 对 Web 资源的保护是靠 Filter 实现的,所以从这个 Filter 来入手,逐步深入 Spring Security 原理。
当初始化 Spring Security 时,会创建一个名为 SpringSecurityFilterChain 的 Servlet 过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了 javax.servlet.Filter,因此外部的请求会经过此 类,下图是 Spring Security 过虑器链结构图:

image.png
image.png
FilterChainProxy 是一个代理,真正起作用的是 FilterChainProxy 中 SecurityFilterChain 所包含的各个 Filter,同时 这些 Filter 作为 Bean 被 Spring 管理,它们是 Spring Security 核心,各有各的职责,但他们并不直接处理用户的认 证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器 (AccessDecisionManager 进行处理,下图是 FilterChainProxy 相关类的 UML 图示。
image.png
spring Security 功能的实现主要是由一系列过滤器链相互配合完成。
image.png
下面介绍过滤器链中主要的几个过滤器及其作用

  • SecurityContextPersistenceFilter 这个 Filter 是整个拦截过程的入口和出口(也就是第一个和最后一个拦截 器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好 的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
  • UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;
  • FilterSecurityInterceptor 是用于保护 web 资源的,使用 AccessDecisionManager 对当前用户进行授权访问,前面已经详细介绍过了;
  • ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。

# 2.2、认证流程

image.png
让我们仔细分析认证过程:

  1. 用户提交用户名、密码被 SecurityFilterChain 中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求 Authentication,通常情况下是 UsernamePasswordAuthenticationToken 这个实现类。
  2. 然后过滤器将 Authentication 提交至认证管理器(AuthenticationManager)进行认证
  3. 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
  4. SecurityContextHolder 安全上下文容器将第 3 步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext ().setAuthentication (…) 方法,设置到其中。 可以看出 AuthenticationManager 接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为 ProviderManager。而 Spring Security 支持多种认证方式,因此 ProviderManager 维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider 完成的。咱们知道 web 表单的对应的 AuthenticationProvider 实现类为 DaoAuthenticationProvider,它的内部又维护着一个 UserDetailsService 负责 UserDetails 的获取。最终 AuthenticationProvider 将 UserDetails 填充至 Authentication。 认证核心组件的大体关系如下:

image.png

# AuthenticationProvider

通过前面的 Spring Security 认证流程我们得知,认证管理器(AuthenticationManager)委托 AuthenticationProvider 完成认证工作。 AuthenticationProvider 是一个接口,定义如下

public interface AuthenticationProvider {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
    boolean supports(Class<?> var1);
}
  • authenticate () 方法定义了认证的实现过程,它的参数是一个 Authentication,里面包含了登录用户所提交的用户名、密码等。而返回值也是一个 Authentication,这个 Authentication 则是在认证成功后,将用户的权限及其他信 息重新组装后生成。
  • Spring Security 中维护着一个 List 列表,存放多种认证方式,不同的认证方式使用不同的 AuthenticationProvider。如使用用户名密码登录时,使用 AuthenticationProvider1,短信登录时使用 AuthenticationProvider2 等等这样的例子很多。
  • 每个 AuthenticationProvider 需要实现 supports()方法来表明自己支持的认证方式,如我们使用表单方式认证, 在提交请求时 Spring Security 会生成 UsernamePasswordAuthenticationToken,它是一个 Authentication,里面 封装着用户提交的用户名、密码信息。而对应的,哪个 AuthenticationProvider 来处理它?

我们在 DaoAuthenticationProvider 的基类 AbstractUserDetailsAuthenticationProvider 发现以下代码:

public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

也就是说当 web 表单提交用户名密码时,Spring Security 由 DaoAuthenticationProvider 处理。
最后,我们来看一下 Authentication (认证信息) 的结构,它是一个接口,我们之前提到的 UsernamePasswordAuthenticationToken 就是它的实现之一:

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    Object getCredentials();
    Object getDetails();
    Object getPrincipal();
    boolean isAuthenticated();
    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

image.png

  1. Authentication 是 spring security 包中的接口,直接继承自 Principal 类,而 Principal 是位于 java.security 包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个 getName () 方法。
  2. getAuthorities (),权限信息列表,默认是 GrantedAuthority 接口的一些实现类,通常是代表权限信息的一系 列字符串。
  3. getCredentials (),凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
  4. getDetails (),细节信息,web 应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的 ip 地 址和 sessionId 的值。
  5. getPrincipal (),身份信息,大部分情况下返回的是 UserDetails 接口的实现类,UserDetails 代表用户的详细 信息,那从 Authentication 中取出来的 UserDetails 就是当前登录用户信息,它也是框架中的常用接口之一。

# UserDetailsService

现在咱们现在知道 DaoAuthenticationProvider 处理了 web 表单的认证逻辑,认证成功后既得到一个 Authentication (UsernamePasswordAuthenticationToken 实现),里面包含了身份信息(Principal)。这个身份 信息就是一个 Object ,大多数情况下它可以被强转为 UserDetails 对象。

DaoAuthenticationProvider 中包含了一个 UserDetailsService 实例,它负责根据用户名提取用户信息 UserDetails (包含密码),而后 DaoAuthenticationProvider 会去对比 UserDetailsService 提取的用户密码与用户提交 的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的 UserDetailsService 公开为 spring bean 来定义自定义身份验证

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

很多人把 DaoAuthenticationProvider 和 UserDetailsService 的职责搞混淆,其实 UserDetailsService 只负责从特定 的地方(通常是数据库)加载用户信息,仅此而已。而 DaoAuthenticationProvider 的职责更大,它完成完整的认证流程,同时会把 UserDetails 填充至 Authentication。

# UserDetails

上面一直提到 UserDetails 是用户信息,咱们看一下它的真面目:

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

它和 Authentication 接口很类似,比如它们都拥有 username,authorities。Authentication 的 getCredentials () 与 UserDetails 中的 getPassword () 需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证 其实就是对这两者的比对。Authentication 中的 getAuthorities () 实际是由 UserDetails 的 getAuthorities () 传递而形成的。还记得 Authentication 接口中的 getDetails () 方法吗?其中的 UserDetails 用户详细信息便是经过了 AuthenticationProvider 认证之后被填充的。
通过实现 UserDetailsService 和 UserDetails,我们可以完成对用户信息获取方式以及用户信息字段的扩展。
Spring Security 提供的 InMemoryUserDetailsManager (内存认证),JdbcUserDetailsManager (jdbc 认证) 就是 UserDetailsService 的实现类,主要区别无非就是从内存还是从数据库加载用户

# 2.3、获取 token 的主要流程

  1. 用户发起获取 token 的请求。
  2. 过滤器会验证 path 是否是认证的请求 /oauth/token,如果为 false,则直接返回没有后续操作。
  3. 过滤器通过 clientId 查询生成一个 Authentication 对象。
  4. 然后会通过 username 和生成的 Authentication 对象生成一个 UserDetails 对象,并检查用户是否存在。
  5. 以上全部通过会进入地址 /oauth/token,即 TokenEndpoint 的 postAccessToken 方法中。
  6. postAccessToken 方法中会验证 Scope,然后验证是否是 refreshToken 请求等。
  7. 之后调用 AbstractTokenGranter 中的 grant 方法。
  8. grant 方法中调用 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法,通过 username 和 Authentication 对象来检索用户是否存在。
  9. 然后通过 DefaultTokenServices 类从 tokenStore 中获取 OAuth2AccessToken 对象。
  10. 然后将 OAuth2AccessToken 对象包装进响应流返回。

# 2.3.1、代码执行流程:

TokenEndpoint.postAccessToken () ---> AbstractTokenGranter.grant () ---> AbstractUserDetailsAuthenticationProvider.authenticate () ---> DefaultTokenServices 类从 tokenStore 中获取 OAuth2AccessToken 对象 ---> OAuth2AccessToken 对象包装进响应流返回

# 2.3.2、代码截图执行流程

# 解析 客户端凭证,值的格式为 Basic 空格 + client_id:client_secret 经过 Base64 加密后的值

image.png
image.png

# ①一个比较重要的过滤器

image.png

# ②此处是①中的 attemptAuthentication 方法

image.png

# ③此处是②中调用的 authenticate 方法

image.png
image.png

# ④ 此处是③中调用的 AbstractUserDetailsAuthenticationProvider 类的 authenticate 方法

image.png

# ⑤ 此处是④中调用的 DaoAuthenticationProvider 类的 retrieveUser 方法

image.png

# ⑥此处为⑤中调用的 ClientDetailsUserDetailsService 类的 loadUserByUsername 方法,执行完后接着返回执行④之后的方法

image.png

# ⑦此处为④中调用的 DaoAuthenticationProvider 类的 additionalAuthenticationChecks 方法,此处执行完则主要过滤器执行完毕,后续会进入 /oauth/token 映射的方法。

image.png

# ⑧此处进入 /oauth/token 映射的 TokenEndpoint 类的 postAccessToken 方法

image.png
postAccessToken 方法
image.png

# ⑨此处为⑧中调用的 AbstractTokenGranter 类的 grant 方法

image.png

# ⑩此处为⑨中调用的 ResourceOwnerPasswordTokenGranter 类中的 getOAuth2Authentication 方法

image.png
image.png
image.png

# ⑩① 此处为⑩中调用的自定义的 CustomUserAuthenticationProvider 类中的 authenticate 方法,此处校验用户密码是否正确,此处执行完则返回⑨执行后续方法。

image.png

# ⑩② 此处为⑨中调用的 DefaultTokenServices 中的 createAccessToken 方法

image.png
image.png

# ⑩③此处为 12 中调用的 RedisTokenStore 中的 getAccessToken 方法等,此处执行完,则一直向上返回到⑧中执行后续方法。

image.png

# ⑩④此处为⑧中获取到 token 后需要包装返回流操作

image.png