阅读 1714

盘点认证框架 : SpringSecurity OAuth 篇

总文档 :文章目录
Github : github.com/black-ant

一 . 前言

这一篇我们继续深入 SpringSecurity , 看看其 OAuth2.0 的流程逻辑.

二 . 简易使用

2.1 Maven 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
        

<!-- OAuth 包 -->
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.5.RELEASE</version>
</dependency>
        
复制代码

2.2 配置项

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private List<AuthorizationServerConfigurer> configurers = Collections.emptyList();

    @Autowired
    private AuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler myAuthenctiationFailureHandler;

    @Autowired
    private AuthorizationServerEndpointsConfiguration endpoints;

    @Autowired
    private UserService userService;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Bean
    public AuthenticationManager authenticationManagerBean(DataSource dataSource) throws Exception {
        OAuth2AuthenticationManager authenticationManager = new OAuth2AuthenticationManager();
        authenticationManager.setTokenServices(new DefaultTokenServices());
        authenticationManager.setClientDetailsService(new JdbcClientDetailsService(dataSource));
        return authenticationManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //该方法用于用户认证,此处添加内存用户,并且指定了权限
        auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Autowired
    public void configure(ClientDetailsServiceConfigurer clientDetails) throws Exception {
        for (AuthorizationServerConfigurer configurer : configurers) {
            configurer.configure(clientDetails);
        }
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
        FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
        http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
        configure(configurer);
        http.apply(configurer);
        String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
        String authorizeEndpointPath = handlerMapping.getServletPath("/oauth/authorize");
        String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
        String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
        if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
            UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
            endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
        }

        // PS : 注意 , OAuth 本身有一个 WebSecurityConfigurerAdapter ,我这里选择覆盖自定义
        http.authorizeRequests()
                .antMatchers("/test/**").permitAll()
                .antMatchers("/before/**").permitAll()
                .antMatchers("/index").permitAll()
                .antMatchers(authorizeEndpointPath).authenticated()
                .antMatchers(tokenEndpointPath).fullyAuthenticated()
                .antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
                .antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
                .anyRequest().authenticated()                      //其它请求都需要校验才能访问
                .and()
                .requestMatchers()
//                .antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
                .and()
                .formLogin()
                .loginPage("/login")                             //定义登录的页面"/login",允许访问
                .defaultSuccessUrl("/home")  //登录成功后默认跳转到"list"
                .successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenctiationFailureHandler).permitAll().and()
                .logout()                                           //默认的"/logout", 允许访问
                .logoutSuccessUrl("/index")
                .permitAll();
        http.addFilterBefore(new BeforeFilter(), UsernamePasswordAuthenticationFilter.class);
        http.setSharedObject(ClientDetailsService.class, clientDetailsService);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //解决静态资源被拦截的问题
        web.ignoring().antMatchers("/**/*.js", "/lang/*.json", "/**/*.css", "/**/*.js", "/**/*.map", "/**/*.html", "/**/*.png");
    }

    protected void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        for (AuthorizationServerConfigurer configurer : configurers) {
            configurer.configure(oauthServer);
        }
    }
}

复制代码

Resource 资源配置

@Configuration
@EnableResourceServer
public class ResServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private TokenStore tokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .tokenStore(tokenStore).resourceId("resourceId");
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .requestMatchers()
                .antMatchers("/user", "/res/**")
                .and()
                .authorizeRequests()
                .antMatchers("/user", "/res/**")
                .authenticated();

    }
}


复制代码

OAuthConfig 专属属性

@Configuration
@EnableAuthorizationServer
@Order(2)
public class OAuthConfig extends AuthorizationServerConfigurerAdapter {


    @Autowired
    private DataSource dataSource;

    @Autowired
    @Lazy
    private AuthenticationManager authenticationManager;

    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }

    @Bean
    public JdbcClientDetailsService jdbcClientDetailsService() {
        return new JdbcClientDetailsService(dataSource);
    }

    @Bean
    public ApprovalStore approvalStore() {
        return new JdbcApprovalStore(dataSource);
    }

    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new JdbcAuthorizationCodeServices(dataSource);
    }


    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(jdbcClientDetailsService());
    }

    //检查token的策略
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {

        security.allowFormAuthenticationForClients();
        security.tokenKeyAccess("isAuthenticated()");
        security.checkTokenAccess("permitAll()");
    }

    //OAuth2的主配置信息
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
//				.approvalStore(approvalStore())
                .authenticationManager(authenticationManager)
                .authorizationCodeServices(authorizationCodeServices())
                .tokenStore(tokenStore());
    }

}

复制代码

2.3 数据库

详见项目

2.4 使用方式

请求方式

http://localhost:8080/security/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=app

https://www.baidu.com/?code=jYgDO3

复制代码

AccessToken

var settings = {
  "url": "http://localhost:8080/security/oauth/token",
  "method": "POST",
  "timeout": 0,
  "headers": {
    "Content-Type": "application/x-www-form-urlencoded"
  },
  "data": {
    "grant_type": "authorization_code",
    "client_id": "client",
    "client_secret": "secret",
    "code": "CFUFok",
    "redirect_uri": "http://www.baidu.com"
  }
};

$.ajax(settings).done(function (response) {
  console.log(response);
});

// 失败
{
    "error": "invalid_grant",
    "error_description": "Invalid authorization code: CFUFok"
}

// 成功
{
    "access_token": "c0955d7f-23fb-4ca3-8a52-c715867cbef2",
    "token_type": "bearer",
    "refresh_token": "55f53af0-1133-46dc-a32d-fbb9968e5938",
    "expires_in": 7199,
    "scope": "app"
}

复制代码

check Token

var settings = {
  "url": "http://localhost:8080/security/oauth/check_token?token=c0955d7f-23fb-4ca3-8a52-c715867cbef2",
  "method": "GET",
  "timeout": 0,
};

$.ajax(settings).done(function (response) {
  console.log(response);
});

// 返回
{
    "aud": [
        "resourceId"
    ],
    "exp": 1618241690,
    "user_name": "gang",
    "client_id": "client",
    "scope": [
        "app"
    ]
}

复制代码

三 . 源码解析

3.1 基础类

TokenStore

TokenStore 是一个接口 , 既然是一个接口 , 就意味着使用中是可以完全定制的

public interface TokenStore {

	// 通过 OAuth2AccessToken 对象获取一个 OAuth2Authentication
	OAuth2Authentication readAuthentication(OAuth2AccessToken  token);
	OAuth2Authentication readAuthentication(String token);

	// 持久化关联 OAuth2AccessToken 和 OAuth2Authentication
	void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);

	// OAuth2AccessToken 的获取和移除
	OAuth2AccessToken readAccessToken(String tokenValue);
	void removeAccessToken(OAuth2AccessToken token);
	OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

	// OAuth2RefreshToken 的直接操作
	void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication);
	OAuth2RefreshToken readRefreshToken(String tokenValue);
	OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token);
	void removeRefreshToken(OAuth2RefreshToken token);

	// 使用刷新令牌删除访问令牌 , 该方法会被用于控制令牌数量
	void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken);

        // Client ID 查询令牌
	Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName);
	Collection<OAuth2AccessToken> findTokensByClientId(String clientId);
}

复制代码

我们再来看看 TokenStore 主要的实现类 , 默认提供了 以下几种实现 :

OAuth001.jpg

InMemoryTokenStore

  • 从名字就能看到 , 该类是将 Token 放在内存中 ,
  • 点进去就能看到 , 类中准备了大量的 ConcurrentHashMap , 保证多线程访问的安全性
// 整体其实没什么看的  , 唯一有点特殊的就是 , 里面不是一个集合 , 而是每个业务一个集合 , 这其实相当于分库分表的处理思路
C- InMemoryTokenStore
    - private final ConcurrentHashMap<String, OAuth2AccessToken> accessTokenStore
    - private final ConcurrentHashMap<String, OAuth2AccessToken> authenticationToAccessTokenStore 
    - private final ConcurrentHashMap<String, Collection<OAuth2AccessToken>> userNameToAccessTokenStore 
    
// 内部类 TokenExpiry
PSC- TokenExpiry implements Delayed
    ?- Delayed 是延迟处理的接口 , 用于判断 Token 是否过期
    - private final long expiry;
    - private final String value;

复制代码

JdbcTokenStore

JdbcTokenStore 是可行的处理方式 , 但是并不是最优解 , 数据库处理对高并发 , 高性能会带来不小的挑战

// 关键点一 : SQL 写死了 , 点开就能看到 , sql 是定死的 , 但是提供了 Set 方法 , 即可定制
private static final String DEFAULT_ACCESS_TOKEN_INSERT_STATEMENT =  "insert into oauth_access_token (toke....."
private String insertAccessTokenSql = DEFAULT_ACCESS_TOKEN_INSERT_STATEMENT;

// 关键点二 : 使用 JDBCTemplate , 意味着常规Spring 配置即可
private final JdbcTemplate jdbcTemplate;  

复制代码

RedisTokenStore

Redis 存储 Token , 比较常见的存储方式 , 一般是首选方案 , 环境影响不能使用才会次选 JDBC

  • 冒号区分文件夹
  • RedisConnectionFactory 需要 redis 包
  • JdkSerializationStrategy 序列化策略
    • 序列化这一块反而是最应该关注的 , 部分监控框架可能会和序列化方式存在冲突

后面说一下它的另外2个特别的实现类 , 他们不是一种持久化的方式

JwkTokenStore

  • 提供了对使用JSON Web密钥(JWK)验证JSON Web令牌(JWT)的JSON Web签名(JWS)的支持
  • 令牌库实现专门用于资源服务器 , 唯一责任是解码JWT并使用相应的JWK验证其签名(JWS)
  • 从这个介绍大概就知道了 , 他是用于资源服务器的 , 他的主要目的是转换 , 所以点开后不难发现 , 里面有一个对象用于底层调用
private final TokenStore delegate : 通过该对象再去处理底层的方式

// 常见的构造器
public JwkTokenStore(String jwkSetUrl) 
public JwkTokenStore(List<String> jwkSetUrls)
public JwkTokenStore(String jwkSetUrl, AccessTokenConverter accessTokenConverter)
public JwkTokenStore(String jwkSetUrl, JwtClaimsSetVerifier jwtClaimsSetVerifier)
public JwkTokenStore(String jwkSetUrl, AccessTokenConverter accessTokenConverter,JwtClaimsSetVerifier jwtClaimsSetVerifier)
public JwkTokenStore(List<String> jwkSetUrls, AccessTokenConverter accessTokenConverter,JwtClaimsSetVerifier jwtClaimsSetVerifier)


复制代码

扩展资料 :


JwtTokenStore

这个对象其实是一个全新的体系 , 是 Token 的 JWT 实现 , 而不仅仅只是一种存储方式

- private JwtAccessTokenConverter jwtTokenEnhancer;
- private ApprovalStore approvalStore;
    - JdbcApprovalStore
    - TokenApprovalStore
    - InMemoryApprovalStore  
复制代码
  • 可以看到提供了一个 转换的成员变量和一个 存储的 store 对象
  • 这里要注意的是 , 他只从令牌本身读取数据。不是真正的存储,它从不持久化任何东西.
    • 因为 , JWT 本身就存储了数据

3.2 事件类型和处理

事件用于推送 , 主要使用的有 DefaultAuthenticationEventPublisher ,我们来看看他

OAuth005.jpg

DefaultAuthenticationEventPublisher

从构造器里面可以看到大概的事件类型

public DefaultAuthenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {

    this.applicationEventPublisher = applicationEventPublisher;
    addMapping(BadCredentialsException.class.getName(),AuthenticationFailureBadCredentialsEvent.class);
    addMapping(UsernameNotFoundException.class.getName(),AuthenticationFailureBadCredentialsEvent.class);
    addMapping(AccountExpiredException.class.getName(),AuthenticationFailureExpiredEvent.class);
    addMapping(ProviderNotFoundException.class.getName(),AuthenticationFailureProviderNotFoundEvent.class);
    addMapping(DisabledException.class.getName(),AuthenticationFailureDisabledEvent.class);
    addMapping(LockedException.class.getName(),AuthenticationFailureLockedEvent.class);
    addMapping(AuthenticationServiceException.class.getName(),AuthenticationFailureServiceExceptionEvent.class);
    addMapping(CredentialsExpiredException.class.getName(),AuthenticationFailureCredentialsExpiredEvent.class);
    addMapping(	"org.springframework.security.authentication.cas.ProxyUntrustedException",
        AuthenticationFailureProxyUntrustedEvent.class);
}


M- publishAuthenticationSuccess
    ?- 发布认证成功事件

M- publishAuthenticationFailure
    - AbstractAuthenticationEvent event = constructor.newInstance(authentication, exception);
        ?- 构建一个 AbstractAuthenticationEvent
    - applicationEventPublisher.publishEvent(event)
        ?- 发布事件
                
                
M- setAdditionalExceptionMappings
    ?- 将额外的异常设置为事件映射。它们会自动与ProviderManager定义的事件映射的默认异常合并

复制代码

3.3 Service 处理类

ResourceServerTokenServices 接口

public interface ResourceServerTokenServices {
	OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;
	OAuth2AccessToken readAccessToken(String accessToken);
}
复制代码

DefaultTokenServices

默认的 Token 处理类 , 没看到特别的东西, 主要是对 TokenStore 的调用

RemoteTokenServices

  • 查询/check_token端点以获取访问令牌的内容。如果端点返回400响应,这表明令牌无效。
C- RemoteTokenServices
    F- private RestOperations restTemplate;
        - 简单点说就是通过这个对象调用 check_token 接口查询 token 信息
        - 注意 , 区别于本地类 , 这种方式目的应该是当前 OAuth 服务作为一个 SP 的情况
复制代码

3.4 Token 管理体系

TokenGranter 是一个接口 , 他有很多实现类,

其中最常见的应该是 AuthorizationCodeTokenGranterImplicitTokenGranter , RefreshTokenGranter

C- AuthorizationCodeTokenGranter
    M- getOAuth2Authentication
        - Paramters 中获取 Code , 并且判空 -> InvalidRequestException
        - authorizationCodeServices.consumeAuthorizationCode(authorizationCode) : 通过 Code 获取 OAuth2Authentication
        - 判断 redirectUri 和 clientId 是否存在 -> RedirectMismatchException/InvalidClientException

复制代码

OAuth002.jpg

3.5 Token Conversion 体系

C- DefaultAccessTokenConverter
    ?- 默认 Token 处理体系 ,我们来看一下主要做了什么
    M- convertAccessToken
    ?- 可以看到 , 整个转换逻辑中会通过不同的开关 , 决定显示哪些

复制代码

OAuth003.jpg

3.6 配置类详情

OAuth 中除了原本的 User 概念 ,同时还有个 Client 概念 ,每个 Client 都可以看成一类待认证的对象 , **Spring OAuth 中提供了 OAuth 协议的自动配置 **, 主要包含2个类 :

  • 实现 User 的认证
    • AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter
  • 实现 Resource 的认证
    • ResourceServerConfiguration extends WebSecurityConfigurerAdapter implements Ordered


// ResourceServerConfiguration
    F- private TokenStore tokenStore; // token 管理实现
    F- private AuthenticationEventPublisher eventPublisher; // 事件发布
    F- private Map<String, ResourceServerTokenServices> tokenServices; // Token Service 集合
    F- private ApplicationContext context; 
    F- private List<ResourceServerConfigurer> configurers = Collections.emptyList();	
        ?- 这里的集合可以用于自己定制 ResourceServerConfigurer 类
    F- private AuthorizationServerEndpointsConfiguration endpoints;
        ?- 对 EndPoint 接口做一个初始化操作
    PSC- NotOAuthRequestMatcher
    M- configure(HttpSecurity http) 
        ?- 核心配置方法 , 主要生成了一个 ResourceServerSecurityConfigurer 放在 HttpSecurity 中
        ?- 这里实际上是克隆了一个当前对象给 HttpSecurity ,而不是一个引用
        - 前面几步分别是 : 配置 tokenServices + tokenStore + eventPublisher
        - 然后发现一个有意思的地方 : 从结构上讲 , 这应该算是装饰器的应用
		for (ResourceServerConfigurer configurer : configurers) {
			configurer.configure(resources);
		}
        - 后面几步开始对 HttpSecurity 本身做配置 , 分别是
            - authenticationProvider : AnonymousAuthenticationProvider
            - exceptionHandling
            - accessDeniedHandler 
            - sessionManagement  : session 管理
            - sessionCreationPolicy 
            - 跨域处理 csrf
            - 添加 requestMatcher   
        - 然后又发现了一个有趣的地方 , 双方互相持有对象
		for (ResourceServerConfigurer configurer : configurers) {
			configurer.configure(http);
		}

// AuthorizationServerSecurityConfiguration 
protected void configure(HttpSecurity http) throws Exception {
    
    // 看样子和上面一样 , 构建一个新得 AuthorizationServerSecurityConfigurer 放入 HttpSecurity 中
    AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
    
    // 
    FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
    http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
    
    // 装饰器持有对象 , 获得更多的扩展功能
    configure(configurer);
    http.apply(configurer);
    
    // 此处就是获取 OAuth 的相关接口 , 并且在下面为其配置对应的权限要求
    String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
    String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
    String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
    if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
        UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
        endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
    }
	
    // 略.... 没什么关键的 , 都是通用的东西
    http
        .authorizeRequests()
        .antMatchers(tokenEndpointPath).fullyAuthenticated()
        .antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
        .antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
        .and()
        .requestMatchers()
        .antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
        .and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
		http.setSharedObject(ClientDetailsService.class, clientDetailsService);
}
复制代码

四 . 运行逻辑

看完了配置逻辑 , 我们来看一下主要的运行逻辑 :

4.1 请求入口

我们从请求的 入口 authorize 来开始 , 看看请求经过了哪些途径 , 来到了这里

流程开始

跳过一系列 Invoke 和 MVC 的流程 , 找到了 FilterChainProxy 类 , 大概就能知道 , OAuth 协议是在Filter 整体流程里面对请求进行的过滤

下面来找一下是哪个过滤类讲请求拦截到登录页的 :

Step 1 : SecurityContextPersistenceFilter

这里主要是执行 Filter 链后在 finally 中处理

Step 2 : BasicAuthenticationFilter

可以看到 , 代码中做了这些事情

C01- BasicAuthenticationFilter
    String header = request.getHeader("Authorization");
        ?- Basic Z2FuZzoxMjM0NTY=
    // 如果 header 为空 , 则继续执行 Filter 
    if (header == null || !header.toLowerCase().startsWith("basic ")) {
        chain.doFilter(request, response);
        return;
    }
    
    // 如果认证信息存在
    - String[] tokens = extractAndDecodeHeader(header, request);
    - String username = tokens[0];
    - new UsernamePasswordAuthenticationToken(username, tokens[1]);
        ?- 构建一个 UsernamePasswordAuthenticationToken
    - .... (PS : 这里的逻辑 Filter 详细说过了 , 就不反复说了)
    - SecurityContextHolder.getContext().setAuthentication(authResult);


复制代码

Filter 的逻辑其实之前就已经讲了 , 这里也就不太深入了

其他扩展 Filter

TokenEndpointAuthenticationFilter

TokenEndpoint的可选身份验证过滤器。它位于客户端的另一个过滤器(通常是BasicAuthenticationFilter)的下游,如果请求也包含用户凭证,它就会为Spring SecurityContext创建一个OAuth2Authentication .

如果使用这个过滤器,Spring安全上下文将包含一个OAuth2Authentication封装(作为授权请求)、进入过滤器的表单参数和来自已经经过身份验证的客户端身份验证的客户端id,以及从请求中提取并使用身份验证管理器验证的已验证用户令牌。

OAuth2AuthenticationProcessingFilter

针对OAuth2受保护资源的认证前过滤器。

从传入请求提取一个OAuth2令牌,并使用它用OAuth2Authentication(如果与OAuth2AuthenticationManager一起使用)填充Spring安全上下文。

4.2 接口详情

注意 , 到这个接口时候 ,认证其实已经完成了 , 拦截的过程详见上文 Filter , 这一部分只分析内部的流程

接口一 : authorize

http://localhost:8080/security/oauth/authorize
C06- AuthorizationEndpoint
    M601- authorize
        P- Map<String, Object> model
        P- Map<String, String> parameters : 传入的参数
        P- SessionStatus sessionStatus
        P- Principal principal : 因为实际上已经认证完了 , 所以能拿到 Principal
        - getOAuth2RequestFactory().createAuthorizationRequest(parameters);
            ?- 通过 parameters 生成了一个 AuthorizationRequest , 该对象为认证过程中的流转对象
        - authorizationRequest.getResponseTypes() : 获取 tResponseTypes 的Set<String> 
            ?- 如果集合类型正确 -> UnsupportedResponseTypeException
            ?- TODO : 为什么是集合 ?
        - authorizationRequest.getClientId() : 校验 ClientId 是否存在 -> InvalidClientException
        - principal.isAuthenticated() : 校验是否认证 -> InsufficientAuthenticationException
        - authorizationRequest.setRedirectUri(resolvedRedirect) : 生成并且设置重定向地址
            ?- 注意 , 这个地址此时还不带 Code
        - oauth2RequestValidator.validateScope(authorizationRequest, client)
            ?- 校验当前 client 的作用域是否包含当前请求 
        - userApprovalHandler.checkForPreApproval(authorizationRequest,(Authentication) principal)
            ?- TODO
        - userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal)
            ?- 请求是否已被最终用户(或其他流程)批准
        - getAuthorizationCodeResponse(authorizationRequest,(Authentication) principal)
            ?- ResponseType = code 时的最终处理逻辑 :M602 
            ?- ResponseType = token 时的最终处理逻辑 :M605 
	M602- getAuthorizationCodeResponse
        - getSuccessfulRedirect(authorizationRequest,generateCode(authorizationRequest, authUser)):M603 
	M603- getSuccessfulRedirect
        - Map<String, String> query = new LinkedHashMap<String, String>();
        - query.put("code", authorizationCode);
			?- 插入 Code
        - String state = authorizationRequest.getState();
        - if (state != null) query.put("state", state);
			?- 插入 State
	M604- generateCode
        - OAuth2Request storedOAuth2Request = getOAuth2RequestFactory().createOAuth2Request(authorizationRequest);
        - OAuth2Authentication combinedAuth = new OAuth2Authentication(storedOAuth2Request, authentication);
        - String code = authorizationCodeServices.createAuthorizationCode(combinedAuth);
			?- 核心方法 , 注意 这里的 AuthorizationCodeServices 是一个接口 , 意味着该对象是可以自定义实现的
			?- 这里的生成类是 RandomValueStringGenerator
    
// 补充 Token 模式
当使用 Implicit 模式进行认证的时候 , 这里是怎么处理的呢 ?
	M605- getImplicitGrantResponse(authorizationRequest)   
        - TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(authorizationRequest, "implicit");
        - OAuth2Request storedOAuth2Request = getOAuth2RequestFactory().createOAuth2Request(authorizationRequest);
        - OAuth2AccessToken accessToken = getAccessTokenForImplicitGrant(tokenRequest, storedOAuth2Request);
			?- 此方法中生成最后的 Token , 如果为空会抛出异常
			- getTokenGranter().grant("implicit",new ImplicitTokenRequest(tokenRequest, storedOAuth2Request));

复制代码

接口二 : AccessToken 接口 /oauth/token

C07- TokenEndpoint
    ?- Token 的处理主要集中在该类中 , 该类中提供了 POST 和 GET 两种请求能力 , 这2种无明显区别
    M701-  postAccessToken(Principal principal,Map<String, String> parameters)
    	- 判断是否已经认证
    	- getClientDetailsService().loadClientByClientId(clientId)
    		?- 先获取 clientId , 再获取一个 ClientDetails
    	- getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient)
    		?- 构建出一个 TokenRequest
    	- 校验 Client ID , 再校验 ClientDetails 的 Scope 域
    	- 校验 GrantType 是否合理 , 不能为 空 , 不能为 implicit
    	- tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
    		?-对 RefreshToken 类型 进行处理
		- getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest)
             ?- 核心 , 生成 AccessToken , 详见上文 CodeToken 生成逻辑
		- getResponse(token) : 生成一个 Response 对象

复制代码

接口三 : CheckTokenEndpoint - /oauth/check_token

C08- CheckTokenEndpoint
	M801- checkToken
    - OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
    	?- 通过 Token 获取 OAuth2AccessToken 对象
    - OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
    	?- 如果 OAuth2AccessToken 存在且没过期 , 获取 OAuth2Authentication
    - accessTokenConverter.convertAccessToken(token, authentication)
    	?- 返回用户信息

复制代码

Client 核心处理

Client 也是 OAuth 中一个非常核心的概念 , 毫无意外 , Client 的校验仍然是通过 Filter 处理的

C- ClientCredentialsTokenEndpointFilter
    M- attemptAuthentication
        - String clientId = request.getParameter("client_id");
        - String clientSecret = request.getParameter("client_secret");
        - Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            ?- 如果认证过了 , 则直接返回 (PS : 这里是 Client 认证)
        - UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,clientSecret);
            ?- 构建一个 UsernamePasswordAuthenticationToken , 用于认证 Client
        - his.getAuthenticationManager().authenticate(authRequest)

// 后面会调用 ProviderManager 用于认证处理
C- ProviderManager
    M- authenticate
        FOR - getProviders() 
            - result = provider.authenticate(authentication) : 完成认证
            
// DaoAuthenticationProvider
复制代码

可以看到 , 这里完全是将 Client 当成一个用户在认证 , 整个体系都得到了通用

业务处理之 : OAuth2ErrorHandler 异常处理

TODO

AuthenticationProvider 体系结构

AuthenticationProvider 的整个体系结构异常庞大 , 他是认证的主体

TODO

4.3 其他重要工具类

DefaultStateKeyGenerator

默认state 构建工具

private RandomValueStringGenerator generator = new RandomValueStringGenerator();
    ?- 使用的是随机数
复制代码

4.4 异常类

  • OAuth2AccessDeniedException
    • 当访问被拒绝时,我们通常想要一个403,但是我们想要与所有其他OAuth2Exception类型一样的处理
  • UserApprovalRequiredException
    • 许可异常
  • UserRedirectRequiredException
    • 抛出该异常 , 许可令牌重定向
  • AccessTokenRequiredException
  • JwkException

4.5 补充类

OAuth2RestTemplate

OAuth2 的定制 RestTemplate 使用所提供资源的凭据发出oauth2认证的Rest请求

// 其中包含了一些和 OAuth 相关的定制
- appendQueryParameter : 构建token 请求的 parameter
- acquireAccessToken : 构建一个 OAuth2AccessToken ??
- getAccessToken : 必要情况下获取或更新当前上下文的访问令牌
- createRequest : 创建一个请求 , 会调用 DefaultOAuth2RequestAuthenticator 生成一个 Token

C- DefaultOAuth2RequestAuthenticator
    ?- 通过 AccessToken 生成一个 OAuth2Request
    - Authorization Bearer ....

复制代码

ProviderDiscoveryClient

看这代码 , OAuth2 应该还支持 OIDC 呢 , 该类用于发现 OIDC 规范配置的提供者的客户端

public ProviderConfiguration discover() {

    // 发起请求
    Map responseAttributes = this.restTemplate.getForObject(this.providerLocation, Map.class);
    ProviderConfiguration.Builder builder = new ProviderConfiguration.Builder();
    
    // 获取 OIDC 信息
    builder.issuer((String)responseAttributes.get(ISSUER_ATTR_NAME));
    builder.authorizationEndpoint((String)responseAttributes.get(AUTHORIZATION_ENDPOINT_ATTR_NAME));    
    if (responseAttributes.containsKey(TOKEN_ENDPOINT_ATTR_NAME)) {
        builder.tokenEndpoint((String)responseAttributes.get(TOKEN_ENDPOINT_ATTR_NAME));
    }
    if (responseAttributes.containsKey(USERINFO_ENDPOINT_ATTR_NAME)) {
        builder.userInfoEndpoint((String)responseAttributes.get(USERINFO_ENDPOINT_ATTR_NAME));
    }
    if (responseAttributes.containsKey(JWK_SET_URI_ATTR_NAME)) {
        builder.jwkSetUri((String)responseAttributes.get(JWK_SET_URI_ATTR_NAME));
    }

    return builder.build();
}

复制代码

// 基本上能看到这些 OIDC 的属性
private static final String PROVIDER_END_PATH = "/.well-known/openid-configuration";
private static final String ISSUER_ATTR_NAME = "issuer";
private static final String AUTHORIZATION_ENDPOINT_ATTR_NAME = "authorization_endpoint";
private static final String TOKEN_ENDPOINT_ATTR_NAME = "token_endpoint";
private static final String USERINFO_ENDPOINT_ATTR_NAME = "userinfo_endpoint";
private static final String JWK_SET_URI_ATTR_NAME = "jwks_uri";

复制代码

4.6 业务的扩展定制

我们最终的目的是为了知道哪些节点可以扩展 :

Security 可以扩展的地方主要有这几类 :

  • 使用接口的地方
    • TokenStore
    • ResourceServerTokenServices
    • ClientDetailsService
    • AuthorizationServerConfigurer
  • 使用开关的地方
    • 扩展 Filter , 监听 AccessToken
    • OAuth2AuthenticationFailureEvent
    • OAuth2ClientAuthenticationProcessingFilter
  • 提供Set 方法的地方

TODO : 这个光说没用 , 后续会尝试做个 Demo 出来

总结

整体来说走了个大概 , 其中还有些零零散散的小点就不想走了 , 精力有限 , 后面如果涉及到 ,再完善到其中去

参考和感谢

blog.csdn.net/weixin_4184…

文章分类
后端
文章标签