Инфраструктура аутентификации инвентаря: SpringSecurity OAuth

Java
Инфраструктура аутентификации инвентаря: SpringSecurity OAuth

Общая документация:Каталог статей
Github : github.com/black-ant

Введение

В этой статье мы продолжим углубляться в Spring Security и рассмотрим логику его процесса OAuth2.0.

2. Простота использования

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);
        }
    }
}

Конфигурация ресурсов ресурсов

@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. Анализ исходного кода

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
    • Сериализация — это самое важное, на что следует обратить внимание.Некоторые фреймворки мониторинга могут конфликтовать с методами сериализации.

Давайте поговорим о двух других его специальных классах реализации позже, они не являются постоянным способом.

JwkTokenStore

  • Обеспечивает поддержку веб-подписи JSON (JWS) с использованием веб-ключа JSON (JWK) для проверки веб-токена JSON (JWT).
  • Реализация хранилища токенов выделена серверу ресурсов, единственная ответственность заключается в декодировании JWT и проверке его подписи (JWS) с помощью соответствующего JWK.
  • Из этого введения вы, вероятно, знаете, что он используется для серверов ресурсов, и его основная цель — преобразование, поэтому нетрудно обнаружить, что в нем есть объект для базового вызова.
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

Этот объект на самом деле является совершенно новой системой, реализацией токена JWT, а не просто методом хранения.

- private JwtAccessTokenConverter jwtTokenEnhancer;
- private ApprovalStore approvalStore;
    - JdbcApprovalStore
    - TokenApprovalStore
    - InMemoryApprovalStore  
  • Вы можете видеть, что предоставлены преобразованная переменная-член и сохраненный объект хранилища.
  • Здесь следует отметить, что он считывает данные только с самого токена. Не настоящий магазин, он никогда ничего не сохраняет.
    • Потому что JWT сам хранит данные

3.2 Типы событий и обработка

События используются для push, в основном с помощью 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 класс обработки услуг

Интерфейс 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 Система управления токенами

TokenGranter — это интерфейс, у него много классов реализации,

Наиболее распространенными из них должны бытьAuthorizationCodeTokenGranterа такжеImplicitTokenGranter , RefreshTokenGranter

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

OAuth002.jpg

3.5 Система конвертации токенов

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

OAuth003.jpg

3.6 Детали класса конфигурации

В дополнение к исходной концепции пользователя в OAuth существует также концепция клиента.Каждый клиент можно рассматривать как класс объектов, подлежащих аутентификации.**Spring OAuth обеспечивает автоматическую настройку протокола OAuth**, который в основном включает 2 класса. :

  • Реализовать аутентификацию пользователей
    • AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter
  • Реализовать аутентификацию ресурсов
    • 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. Запустите логику

После прочтения логики конфигурации давайте взглянем на основную логику работы:

4.1 Запросить запись

Начнем с авторизации записи запроса, и посмотрим, какие пути прошел запрос и пришел сюда

процесс начинается

Пропустить ряд процессов Invoke и MVC и найтиFilterChainProxyclass, вы, вероятно, знаете, что протокол OAuth фильтрует запросы в общем процессе Filter

Выясним, какой класс фильтра перехватывает запрос на страницу входа:

Step 1 : SecurityContextPersistenceFilter

Тут главное обработать его окончательно после выполнения цепочки Filter

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);


Логика фильтра уже обсуждалась ранее, так что здесь она не слишком глубока.

Другие расширения Фильтр

TokenEndpointAuthenticationFilter

Дополнительный фильтр проверки подлинности для TokenEndpoint. Это в другом фильтре на стороне клиента (Обычно BasicAuthenticationFilter), если запрос также содержит учетные данные пользователя, он будетСоздайте OAuth2Authentication для Spring SecurityContext.

Если этот фильтр используется, контекст безопасности Spring будет содержать оболочку OAuth2Authentication (как запрос авторизации), параметры формы, которые входят в фильтр, и идентификатор клиента из аутентифицированной аутентификации клиента, а также идентификатор, извлеченный из запроса и используемый Authenticated токен пользователя, проверенный менеджером аутентификации.

OAuth2AuthenticationProcessingFilter

Фильтр предварительной аутентификации для защищенных ресурсов OAuth2.

Извлеките токен OAuth2 из входящего запроса и используйте его с OAuth2Authentication (если сOAuth2AuthenticationManagerвместе) для заполнения контекста безопасности Spring.

4.2 Детали интерфейса

Обратите внимание, что ко времени этого интерфейса аутентификация фактически завершена,Процесс перехвата подробно описан выше Фильтр, эта часть анализирует только внутренний процесс

Интерфейс 1: авторизация

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));

Интерфейс 2: интерфейс 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 对象

Интерфейс 3: 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)
    	?- 返回用户信息

Обработка ядра клиента

Клиент также является очень важной концепцией в OAuth. Неудивительно, что проверка клиента по-прежнему обрабатывается фильтром.

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

Видно, что здесь Клиент полностью аутентифицирован как пользователь, и вся система универсальна.

Бизнес-обработка: обработка исключений OAuth2ErrorHandler

TODO

Архитектура AuthenticationProvider

Вся архитектура AuthenticationProvider чрезвычайно велика, и он является субъектом аутентификации.

TODO

4.3 Другие важные инструменты

DefaultStateKeyGenerator

Построитель состояния по умолчанию

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

4.4 ненормальный класс

  • OAuth2AccessDeniedException
    • Обычно нам нужна ошибка 403 при отказе в доступе, но нам нужна такая же обработка, как и для всех других типов исключений OAuth2Exception.
  • UserApprovalRequiredException
    • Исключение лицензии
  • UserRedirectRequiredException
    • Выбрасывая это исключение, токен лицензии перенаправляет
  • AccessTokenRequiredException
  • JwkException

4.5 Дополнительные классы

OAuth2RestTemplate

Пользовательский RestTemplate для OAuth2 отправляет запрос Rest с проверкой подлинности oauth2, используя учетные данные предоставленного ресурса.

// 其中包含了一些和 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 Расширенная настройка бизнеса

Наша конечная цель — узнать, какие узлы могут масштабироваться:

Существуют в основном следующие категории, где можно расширить Безопасность:

  • Где использовать интерфейс
    • TokenStore
    • ResourceServerTokenServices
    • ClientDetailsService
    • AuthorizationServerConfigurer
  • где используется переключатель
    • Расширить фильтр для мониторинга AccessToken
    • OAuth2AuthenticationFailureEvent
    • OAuth2ClientAuthenticationProcessingFilter
  • Где предоставляется метод Set

TODO : Говорить об этом бесполезно, позже попробую сделать демку

Суммировать

В целом я прошел грубо, и есть какие-то разбросанные мелкие точки, которые я не хочу оставлять.У меня ограниченная энергия.Если она будет задействована позже, я ее улучшу.

Ссылка и спасибо

blog.CSDN.net/WeChat_4184…