Spring Security 源码解读:OAuth2 Authorization Server

news2024/10/1 3:34:15

样例代码请参考:spring-security-oauth2.0-server-sample

Spring Authorization Server刚发展不久,还没有springboot版本,而Resource Server有,但是两个底层很多不兼容,会重复引入不同版本的jar包。

另外,该spring-security-oauth2-authorization-server依赖支持OAuth2.1草案规范。

  • 关于OAuth2.1草案介绍请参考 OAuth 2.1

Server中的SecurityFilterChain

SecurityFilterChain中有以下filter:
在这里插入图片描述
其中AuthenticationManager接口 默认实现类 ProviderManager中有以下 AuthenticationProvider供认证:
在这里插入图片描述

除了以上的一个SecurityFilterChain之外,server本身作为一个web服务器需要另外一个SecurityFilterChain(一个用于授权别人,一个用于认证用户)。

所以存在两个 SecurityFilterChain, 而Spring Security会执行匹配到的第一个SecurityFilterChain。在配置文件中要通过@Order给授权filterchain更高级别:

class SecurityConfig{
	
	@Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        OAuth2AuthorizationServerConfigurer conf = new OAuth2AuthorizationServerConfigurer();
        http.apply(conf);
		...
        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
            throws Exception {
        http.authorizeHttpRequests((authorize) -> authorize
                        .anyRequest().authenticated()
                )
                .formLogin(Customizer.withDefaults());
        return http.build();
    }
}

匹配SecurityFilterChain逻辑在 FilterChainProxy#getFilters :

	private List<Filter> getFilters(HttpServletRequest request) {
		int count = 0;
		for (SecurityFilterChain chain : this.filterChains) {
			if (chain.matches(request)) {
				return chain.getFilters();
			}
		}
		return null;
	}

DefaultSecurityFilterChain implements SecurityFilterChain中有RequestMatcher接口用于filterchain匹配,所有在OAuth2 Authorization Server中配置过的endpoint如果匹配,则进入授权的filterchain:
在这里插入图片描述

OAuth2AuthorizationEndpointFilter

  1. 开始授权
  2. 未登录本应用,重定向到登录页面,用户先登录
  3. 登陆后重定向到本应用授权页面,授权其他应用
  4. 授权成功后重定向code到redirect_uri

其中,2和3步由OAuth2AuthorizationCodeRequestAuthenticationProvider配合完成,4步由OAuth2AuthorizationConsentAuthenticationProvider配合完成。

该类首先进行拦截,进来先将Request转化为Authentication,然后用
ProviderManager implements AuthenticaitonManager中的AuthenticationProvider,具体是OAuth2AuthorizationCodeRequestAuthenticationProvider,在该provider中,先用RegisteredClientRepository查询clientid是否存在。然后 进行PKCE机制校验,成功后判断当前用户是否登录授权,没有登录就由该链中的ExceptionTranslationFilter中调用LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint 重定向到 登录页面进行登录(这个重定向之前会把request的信息包含client_id,redirect_uri,state等参数保存到RequestCache(默认通过HttpSession实现)中,后面登陆成功又有一个重定向到授权页面),重定向后由另一条SecurityFilterChain处理,另一条filterchain中的UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter登录校验成功后其父类AbstractAuthenticationProcessingFilter中的字段SavedRequestAwareAuthenticationSuccessHandlerRequestCache中取出session(会对之前的缓存的request包装现在的request,包含client_id,redirect_uri,state等参数),会进行重定向,重定向到授权页面,询问用户是否授权。后续详见下面的OAuth2AuthorizationEndpointFilter

// 该类负责授权过程中,code换取accessToken之前所有流程,有很多重定向以及利用session保存registeredclient提交的信息以及usernamepassword登陆成功的authentication.
public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
	// 注意 成功handler是一个匿名类,方法为本类中sendAuthorizationResponse
	private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendAuthorizationResponse;
	...
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		// 首先根据路径匹配是否要开始授权,如 默认 /oauth2/authorize
		if (!this.authorizationEndpointMatcher.matches(request)) {
			filterChain.doFilter(request, response);
			return;
		}
		// 开始授权
		try {
			...
			// 具体调用 OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider
			// 其中会判断是否登录和授权,分别返回不同类型的Authentication实现类,后续会根据类型进行判断是否收授权
			Authentication authenticationResult = this.authenticationManager.authenticate(authentication);
			// 授权服务器还未登录,直接让后面的处理,会抛出异常由ExceptionTranslationFilter处理,最后由LoginUrlAuthenticationEntryPoint重定向至登录页面如/login(注意,之前传过来的信息如client_id,redirect_uri都放在session中,并返回sessionid了),
			// 但是登录不会走授权的filterChain,而是web security那一条中的UsernamePasswordAuthenticationFilter。
			// 登陆成功后 由UsernamePasswordAuthenticationFilter的AuthenticationSuccessHandler处理,
			// 其实现类SavedRequestAwareAuthenticationSuccessHandler(在usernamefilter父类中)处理,该handler在session中拿取之前的client_id,redirect_uri等信息,
			// 再次重定向到 默认的开始授权路径/oauth2/authorize(注意,不是redirect_uri)
			if (!authenticationResult.isAuthenticated()) {
				// If the Principal (Resource Owner) is not authenticated then
				// pass through the chain with the expectation that the authentication process
				// will commence via AuthenticationEntryPoint
				filterChain.doFilter(request, response);
				return;
			}
			// 由上面的重定向,用户已经登录(登录信息也是通过session传递的)
			// 如果还没授权,该类型就为OAuth2AuthorizationConsentAuthenticationToken,如果授权过了,这里就不是了
			// 具体流程在OAuth2AuthorizationCodeRequestAuthenticationProvider中
			if (authenticationResult instanceof OAuth2AuthorizationConsentAuthenticationToken) {
				// 发送重定向到授权页面
				sendAuthorizationConsent(...);
				return;
			}
		
		// 如果都授权,开始执行code返回流程,该handler是匿名类,执行本类#sendAuthorizationResponse
		// 用户同意授权后由OAuth2AuthorizationConsentAuthenticationProvider处理
		// 在该Provider中会生成code

		this.authenticationSuccessHandler.onAuthenticationSuccess(
					request, response, authenticationResult);

		} catch (OAuth2AuthenticationException ex) {
				...
			this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
		}
	}
	
	private void sendAuthorizationResponse(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException {

		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
				(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
		// 生成code
		UriComponentsBuilder uriBuilder = UriComponentsBuilder
				.fromUriString(authorizationCodeRequestAuthentication.getRedirectUri())
				.queryParam(OAuth2ParameterNames.CODE, authorizationCodeRequestAuthentication.getAuthorizationCode().getTokenValue());
		String redirectUri;
		// 返回接受的state,防止CSRF
		if (StringUtils.hasText(authorizationCodeRequestAuthentication.getState())) {
			uriBuilder.queryParam(OAuth2ParameterNames.STATE, "{state}");
			Map<String, String> queryParams = new HashMap<>();
			queryParams.put(OAuth2ParameterNames.STATE, authorizationCodeRequestAuthentication.getState());
			redirectUri = uriBuilder.build(queryParams).toString();
		} else {
			redirectUri = uriBuilder.toUriString();
		}
		// 带着code和state参数重定向到用户给的redirect_uri
		this.redirectStrategy.sendRedirect(request, response, redirectUri);
	}

}

OAuth2AuthorizationCodeRequestAuthenticationProvider

public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
	// 注册的client的repo
	private final RegisteredClientRepository registeredClientRepository;
	// OAuth2的Authentication的信息,默认InMemory实现
	// 包含两个部分,一个已经完成,一个流程中
	//	Map<String, OAuth2Authorization> initializedAuthorizations = ...
	//	Map<String, OAuth2Authorization> authorizations = ...
	private final OAuth2AuthorizationService authorizationService;
	// OAuth2已经授权过的信息
	private final OAuth2AuthorizationConsentService authorizationConsentService;
	// code生成器
	private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
	
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
				(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
		// 找client
		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
				authorizationCodeRequestAuthentication.getClientId());
		// 没有就抛异常
		if (registeredClient == null) {
			throwError(...);
		}
		// 有client进一步校验参数
		OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext = ...
		this.authenticationValidator.accept(authenticationContext);
		
		// 注册的用户没有code grant的类型,抛异常
		if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
			throwError(}
		}

		// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
		String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE);
		// 有 code_challenge 和 code_challenge_method参数,进一步校验
		if (StringUtils.hasText(codeChallenge)) {
			...
		} 
		// 没有看server端是否必需PKCE,如果必需抛异常
		else if (registeredClient.getClientSettings().isRequireProofKey()) {
			throwError();
		}
		// 获得当前的 Authentication,此时类型是OAuth2AuthorizationCodeRequestAuthenticationToken 
		Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
		// 没有认证过,直接返回,第一次请求开始就在这里返回
		if (!isPrincipalAuthenticated(principal)) {
			// Return the authorization request as-is where isAuthenticated() is false
			return authorizationCodeRequestAuthentication;
		}	
		// 进行到这里,表示用户已经通过另一条SecurityFilterChain登录过,并返回了登录信息
		// 登录后下面即将开始授权流程
		
		// 生成一个Request包含前面所有信息
		OAuth2AuthorizationRequest authorizationRequest = ...
		// 根据client_id和name查询是否已经授权过,没有授权这里为null	
		OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
				registeredClient.getId(), principal.getName());
		// requireAuthorizationConsent 方法
		// 如果consent为null,获取配置不需要consent就跳过
		if (requireAuthorizationConsent(registeredClient, authorizationRequest, currentAuthorizationConsent)) {
			// 需要授权
			// 生成state
			String state = DEFAULT_STATE_GENERATOR.generateKey();
			OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
					.attribute(OAuth2ParameterNames.STATE, state)
					.build();
			// 保存流程中的Authorization
			this.authorizationService.save(authorization);
			...
			// 返回OAuth2AuthorizationConsentAuthenticationToken类型
			// 在OAuth2AuthenticationEndpointFilter中会判断返回结果Authentication是否是下面的类型
			// 如果是表示需要进行授权操作,授权重定向流程在OAuth2AuthenticationEndpointFilter#sendAuthorizationConsent中
			return new OAuth2AuthorizationConsentAuthenticationToken(...);
		}

		// 此时已经授权难过了

		// 保存已完成的Authentication
		this.authorizationService.save(updatedAuthorization);
		// 返回该类型的Authentication
		return new OAuth2AuthorizationCodeRequestAuthenticationToken()

OAuth2AuthorizationConsentAuthenticationProvider

用户登录后点击授权,返回后由该AuthenticationProvider实现类处理。

public final class OAuth2AuthorizationConsentAuthenticationProvider implements AuthenticationProvider {
	// state参数表示 正在进行的 OAuth2 授权
	private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
	private final RegisteredClientRepository registeredClientRepository;
	private final OAuth2AuthorizationService authorizationService;
	private final OAuth2AuthorizationConsentService authorizationConsentService;
	private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
	private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;


	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication =
				(OAuth2AuthorizationConsentAuthenticationToken) authentication;
		// 找到正在进行的 Authorization,在上面的OAuth2AuthorizationCodeRequestAuthenticationProvider requireAuthorizationConsent的if代码块中保存了,注意不是最后的哪里保存的
		// 根据state参数寻找,防止CSRF
		OAuth2Authorization authorization = this.authorizationService.findByToken(
				authorizationConsentAuthentication.getState(), STATE_TOKEN_TYPE);
		if (authorization == null) {
			throwError(...);
		}
		
		// The 'in-flight' authorization must be associated to the current principal
		// 关联 authroization和principal
		Authentication principal = (Authentication) authorizationConsentAuthentication.getPrincipal();
		if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
			throwError(...);
		}

		// 验证RegisteredClient
		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
				authorizationConsentAuthentication.getClientId());
		if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
			throwError(...);
		}

		// 判断需要的授权和用户给的授权是否覆盖,不足则抛异常
		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
		Set<String> requestedScopes = authorizationRequest.getScopes();
		Set<String> authorizedScopes = new HashSet<>(authorizationConsentAuthentication.getScopes());
		if (!requestedScopes.containsAll(authorizedScopes)) {
			throwError(...);
		}
		
		// 判断是否有授权
		OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(...)// 接下来根据client_id 和 principal_name把用户给的授权添加到已授权目录中
		....
		if (!authorizationConsent.equals(currentAuthorizationConsent)) {
			this.authorizationConsentService.save(authorizationConsent);
		}
		
		// 生成code
		OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
				authorizationConsentAuthentication, registeredClient, authorization, authorizedScopes);
		OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);



		// 保存Authorization进度
		OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
				.authorizedScopes(authorizedScopes)
				.token(authorizationCode)
				.attributes(attrs -> {
					attrs.remove(OAuth2ParameterNames.STATE);
				})
				.build();
		this.authorizationService.save(updatedAuthorization);
		
		// 返回该类型到由OAuth2AuthorizationEndpointFilter的successHandler处理
		return new OAuth2AuthorizationCodeRequestAuthenticationToken(...);

用户自此拿到code以后,开始拿着code换取token,默认访问 /oauth2/token 路径,此时OAuth2AuthorizationEndpointFilter会跳过。

然后分为两步

  • OAuth2ClientAuthenticationFilter拦截,通过ClientSecretAuthenticationProvider验证secret,默认通过Basic方式传递secret。
  • secret验证通过,OAuth2TokenEndpointFilter拦截,由OAuth2AuthorizationCodeAuthenticationProvider生成accessToken和refreshToken返回 (注意名字,不是上面的CodeRequestAuthenticationProvider),如果由openid的SCOPE,则还会加上idToken。

至此,授权服务器内容完毕,剩下的利用accessToken去resource server换取受限资源即可。

OAuth2ClientAuthenticationFilter

  • 校验client_secret

secret的提交方式分为三种:

  • url编码
  • header
  • body

其中url编码在OAuth2.1中被禁用。而在Spring Security中提供多种方式:
参见 ClientAuthenticationMethod代码:

public final class ClientAuthenticationMethod implements Serializable {

	public static final ClientAuthenticationMethod CLIENT_SECRET_BASIC = ...
	public static final ClientAuthenticationMethod CLIENT_SECRET_POST = ...
	public static final ClientAuthenticationMethod CLIENT_SECRET_JWT = ...
	public static final ClientAuthenticationMethod PRIVATE_KEY_JWT = ...
	public static final ClientAuthenticationMethod NONE = ...

}

在配置RegisteredClient时指定示例配置如下:

@Bean
    public RegisteredClientRepository registeredClientRepository() {
		...
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("test")
                .clientSecret(encodeSecret)
                // *************
                // secret校验方式
                // *************
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        		...
        		.build();

        return new CustomRegisteredClientRepository(registeredClient);
    }

具体看下面的ClientSecretAuthenticationProvider

ClientSecretAuthenticationProvider

public final class ClientSecretAuthenticationProvider implements AuthenticationProvider {
	private final RegisteredClientRepository registeredClientRepository;
	private final CodeVerifierAuthenticator codeVerifierAuthenticator;
	private PasswordEncoder passwordEncoder;

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		// 该token包含所有信息,包含从Header中提取的basic client_secret,是base64解码后的原始secret
		OAuth2ClientAuthenticationToken clientAuthentication =
				(OAuth2ClientAuthenticationToken) authentication;
		// 校验secret的方式,默认Basic
		if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientAuthentication.getClientAuthenticationMethod()) &&
				!ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthentication.getClientAuthenticationMethod())) {
			return null;
		}
		// 查找clientId,并做校验
		String clientId = clientAuthentication.getPrincipal().toString();
		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
		...

		// 获取secret
		String clientSecret = clientAuthentication.getCredentials().toString();
		// 用passwordEncoder做校验,校验client_id和secret
		if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {
			throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);
		}

		if (registeredClient.getClientSecretExpiresAt() != null &&
				Instant.now().isAfter(registeredClient.getClientSecretExpiresAt())) {
			throwInvalidClient("client_secret_expires_at");
		}

		// PKCE机制验证,防止CSRF和code fixation attack
		// Validate the "code_verifier" parameter for the confidential client, if available
		this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
		// 返回
		return new OAuth2ClientAuthenticationToken(registeredClient,
				clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
	}
}

OAuth2TokenEndpointFilter

  • 执行code换取token

具体来看 OAuth2AuthorizationCodeAuthenticationProvider

public final class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
	
	private final OAuth2AuthorizationService authorizationService;
	private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;

	...
	
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =
				(OAuth2AuthorizationCodeAuthenticationToken) authentication;
		// 1. 当前认证的client信息
		OAuth2ClientAuthenticationToken clientPrincipal =
				getAuthenticatedClientElseThrowInvalidClient(authorizationCodeAuthentication);
		RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();

		// 2. 从OAuth2AuthorizationService中根据code获取的client信息
		OAuth2Authorization authorization = this.authorizationService.findByToken(
				authorizationCodeAuthentication.getCode(), AUTHORIZATION_CODE_TOKEN_TYPE);
		...
		//将1.2.步中获取的两个信息进行比对
		// client_id 比对
		// redirect_uri 比对
		// code是否有效
		...
			
		// 前面的信息汇总
		DefaultOAuth2TokenContext.Builder tokenContextBuilder = ...
		// 该builder用于存放所有信息,包含后面生成的accessToken和refreshToken和idToken
		OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
		
		// ----- Access token -----
		OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
		OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
		// Bearer, 只有OAuth2AccessToken这里有tokenType ,可设置为Bearer
		// 下面的OAuth2RefreshToken和OidcIdToken没有该选项
		// 一般accessToken通过header中Authorization字段携带
		// 而refreshToken作为cookie或local storage保存在浏览器中
		// idToken一般供后端使用,不像前端展示
		OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
				generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
				generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
		if (generatedAccessToken instanceof ClaimAccessor) {
			authorizationBuilder.token(accessToken, (metadata) ->
					metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
		} else {
			// 加入authorizationBuilder
			authorizationBuilder.accessToken(accessToken);
		}
		
		// ----- Refresh token -----
		...

		// ----- ID token -----
		...

		
		// build所有信息
		authorization = authorizationBuilder.build();

		// Invalidate the authorization code as it can only be used once
		authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, authorizationCode.getToken());
		// 保存已完成的Authorization
		this.authorizationService.save(authorization);

		return new OAuth2AccessTokenAuthenticationToken(
				registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
	}

}

OAuth2TokenGenerator

OAuth2AuthorizationCodeAuthenticationProvider中通过:

private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;

生成accessToken和refreshToken和idToken。
OAuth2TokenGenerator有四个实现类:

  • DelegatingOAuth2TokenGenerator
  • JwtGenerator
  • OAuth2AccessTokenGenerator
  • OAuth2RefreshTokenGenerator

其中 DelegatingOAuth2TokenGenerator做代理,将其他三个generator组合在该代理generator中。

public final class DelegatingOAuth2TokenGenerator implements OAuth2TokenGenerator<OAuth2Token> {
	// 3个,JwtGenerator OAuth2AccessTokenGenerator OAuth2RefreshTokenGenerator`
	private final List<OAuth2TokenGenerator<OAuth2Token>> tokenGenerators;

	@Nullable
	@Override
	public OAuth2Token generate(OAuth2TokenContext context) {
		for (OAuth2TokenGenerator<OAuth2Token> tokenGenerator : this.tokenGenerators) {
			// accessToken使用JwtGenerator
			// refreshToken使用OAuth2RefreshTokenGenerator
			// idToken使用JwtGenerator
			OAuth2Token token = tokenGenerator.generate(context);
			if (token != null) {
				return token;
			}
		}
		return null;
	}
	...
}

后记

剩下的还有两个filter,分别是

  • OAuth2TokenIntrospectionEndpointFilter : 参考 RFC 7662: Token Introspection
  • OAuth2TokenRevocationEndpointFilter : 参考 RFC 7009: Token Revocation

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/348476.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis之哨兵模式

什么是哨兵模式&#xff1f; Sentinel(哨兵)是用于监控Redis集群中Master状态的工具&#xff0c;是Redis高可用解决方案&#xff0c;哨兵可以监视一个或者多个redis master服务&#xff0c;以及这些master服务的所有从服务。 某个master服务宕机后&#xff0c;会把这个master下…

安卓小游戏:俄罗斯方块

安卓小游戏&#xff1a;俄罗斯方块 前言 最近用安卓自定义view写了下飞机大战、贪吃蛇、小板弹球三个游戏&#xff0c;还是比较简单的&#xff0c;这几天又把俄罗斯方块还原了一下&#xff0c;写了一天&#xff0c;又摸鱼调试了两天&#xff0c;逻辑不是很难&#xff0c;但是…

机械革命z2黑苹果改造计划第三番-macOS键盘快捷键Win键盘适配

macOS键盘快捷键&Win键盘适配 键盘区别 首先下图是苹果妙控键盘无指纹版&#xff0c;官网售价699&#xff0c;穷学生的我是真的买不起 然后下图是我正在使用的机械键盘ikbc w200 87键版本 可以看出两者在键位排列上的区别主要在于 win/command 键&#xff0c;在macOS中大…

12 个华丽的 UI 组件,为您提供设计灵感✨

现代 Web 开发已转向基于组件的架构&#xff0c;从而实现更快的开发、更多的控制和更低的维护成本。在本文中&#xff0c;我精心挑选了一些我最喜欢的 UI 组件作为您的设计灵感。我尝试在我们的开发工作流程中包含不同类型的一些最常用的组件&#xff0c;包括卡片、文本、按钮、…

本人使用的idea插件

文章目录&#x1f68f; 本人使用的idea插件&#x1f6ac; pojo to Json&#x1f6ac; GsonFormatPlus&#x1f6ac; EasyYapi&#x1f6ac; Chinese (Simplified) Language Pack / 中文语言包&#x1f6ac; MyBatis Log Free&#x1f6ac; MyBatisPlusX&#x1f6ac; Statistic…

软件测试如何获得高薪?

软件测试如何获得高薪&#xff1f; 目录&#xff1a;导读 测试基础理论/测试设计能力 业务知识 行业技术知识 数据库 掌握编程语言 搞定自动化测试 质量流程管理 下面谈谈不同level的测试工程师应具备的基本能力 第一个&#xff1a;我们称之为测试员/测试工程师 第二…

Linux C/C++ 调试的那些“歪门邪道”

无数次被问道&#xff1a;你在终端下怎么调试更高效&#xff1f;或者怎么在 Vim 里调试&#xff1f;好吧&#xff0c;今天统一回答下&#xff0c;我从来不在 vim 里调试&#xff0c;因为它还不成熟。那除了命令行 GDB 裸奔以外&#xff0c;终端下还有没有更高效的方法&#xff…

【正点原子FPGA连载】第十二章PS端RTC中断实验 摘自【正点原子】DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南

1&#xff09;实验平台&#xff1a;正点原子MPSoC开发板 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id692450874670 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html 第十二章PS端RTC…

免费链接投票作品投票通道线上投票活动制作网络投票制作

“文明健康、绿色环保”网络评选投票_免费链接投票_作品投票通道_扫码投票怎样进行现在来说&#xff0c;公司、企业、学校更多的想借助短视频推广自己。通过微信投票小程序&#xff0c;网友们就可以通过手机拍视频上传视频参加活动&#xff0c;而短视频微信投票评选活动既可以给…

node.js+vue大学生校园论坛系统vscode mysql

技术难点 &#xff08;1&#xff09;没有待测试程序文本、控制流程图及有关要求、规范等文件。 &#xff08;2&#xff09;测试用例及测试例程的分析、理解和设计。 &#xff08;3&#xff09;没有开发组织的配合&#xff0c;没有软件测试团队之间的讨论。 &#xff08;4&#…

九龙证券|巴菲特最新操作曝光!刚建仓就大幅减持,台积电盘后暴跌5%

当地时间2月14日&#xff0c;美股三大指数收盘涨跌纷歧。道指跌0.46%&#xff0c;标普500指数跌0.03%&#xff0c;纳指涨0.57%。 大型科技股多数上涨&#xff0c;特斯拉涨7.51%&#xff0c;领涨标普500指数成份股。热门中概股走弱&#xff0c;纳斯达克中国金龙指数跌0.55%&…

【零基础入门前端系列】—CSS介绍(九)

【零基础入门前端系列】—CSS介绍&#xff08;九&#xff09; 一、为什么需要CSS&#xff1f; 使用Css的目的就是让网页具有美观一致的页面, 另外一个最重要的原因是内容与格式 分离,在没有CSS之前&#xff0c;我们想要修改HTML元素的样式需要为每个HTML元素单独定义 样式属性…

用户认证-cookie和session

无状态&短链接 短链接的概念是指&#xff1a;将原本冗长的URL做一次“包装”&#xff0c;变成一个简洁可读的URL。 什么是短链接-> https://www.cnblogs.com/54chensongxia/p/11673522.html HTTP是一种无状态的协议 短链接&#xff1a;一次请求和一次响应之后&#…

女生可以参加IT培训吗?

2023年了&#xff0c;就不要把性别当作选择专业的前提条件了。虽然这句话说过很多次了&#xff0c;作为IT行业来说&#xff0c;是非常欢迎女生的加入&#xff1b;尤其是整天都是面对一大堆男攻城狮&#xff0c;工作氛围一点都不活跃&#xff0c;反而显得压抑和杂乱&#xff0c;…

在Windows上安装Scala

文章目录Windows上安装Scala&#xff08;一&#xff09;到Scala官网下载Scala&#xff08;二&#xff09;安装Scala安装向导&#xff08;三&#xff09;配置Scala环境变量&#xff08;四&#xff09;测试Scala是否安装成功1、查看Scala版本2、启动Scala&#xff0c;执行语句Win…

什么是装运单IFTMIN?

符合EDIFACT国际报文标准的IFTMIN主要用于传输电子运输订单&#xff0c;这些装运单作为EDI数据交换的一部分&#xff0c;由客户或托运人发送给物流服务提供商。通过EDI传输的运输信息可以被用来计划当前所需的运输能力&#xff0c;并且物流服务提供商也可以据此提前将包装材料准…

【正点原子FPGA连载】第十一章PL SYSMON测量输入模拟电压 摘自【正点原子】DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南

1&#xff09;实验平台&#xff1a;正点原子MPSoC开发板 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id692450874670 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html 第十一章PL SYSM…

使用注意力机制的seq2seq

注意力机制在NLP中的应用&#xff0c;是早期工作之一 1.为什么使用注意力机制 ①在机器翻译的时候&#xff0c;每个生成的词可能相关于源句子不同的词 ②语言翻译的时候&#xff0c;中英文存在倒装句&#xff0c;几个相同意思的句子中的词的位置可能近似的对应。翻译句子某部分…

Lp正则化

一、L1 和 L2范数&#xff08;norm&#xff09;A norm is a mathematical thing that is applied to a vector. The norm of a vector maps vector values to values in [0,∞). In machine learning, norms are useful because they are used to express distances: this vect…

DataWhale-统计学习方法打卡Task01

学习教材《统计学习方法&#xff08;第二版&#xff09;》李航 统计学习方法&#xff08;第2版&#xff09; by...李航 (z-lib.org).pdf https://www.aliyundrive.com/s/maJZ6M9hrTe 点击链接保存&#xff0c;或者复制本段内容&#xff0c;打开「阿里云盘」APP &#xff0c;无…