开局一张图
项目源码:youlai-mall
通过 Spring Cloud Gateway 访问认证中心进行认证并获取得到访问令牌。
再根据访问令牌 access_token 获取当前登录的用户信息。
前言
Spring Security OAuth2 的最终版本是2.5.2,并于2022年6月5日正式宣布停止维护。Spring 官方为此推出了新的替代产品,即 Spring Authorization Server。然而,出于安全考虑,Spring Authorization Server 不再支持密码模式,因为密码模式要求客户端直接处理用户的密码。但对于受信任的第一方系统(自有APP和管理系统等),许多情况下需要使用密码模式。在这种情况下,需要在 Spring Authorization Server 的基础上扩展密码模式的支持。本文基于开源微服务商城项目 youlai-mall、Spring Boot 3 和 Spring Authorization Server 1.1 版本,演示了如何扩展密码模式,以及如何将其应用于 Spring Cloud 微服务实战。
数据库初始化
Spring Authorization Server 官方提供的授权服务器示例 demo-authorizationserver 初始化数据库所使用的3个SQL脚本路径如下:
根据路径找到3张表的SQL脚本
令牌发放记录表: oauth2-authorization-schema.sql
授权记录表: oauth2-authorization-consent-schema.sql
客户端信息表: oauth2-registered-client-schema.sql
整合后的完整数据库 SQL 脚本如下:
-- ---------------------------- -- 1. 创建数据库 -- ---------------------------- CREATE DATABASE IF NOT EXISTS oauth2_server DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; -- ---------------------------- -- 2. 创建表 -- ---------------------------- use oauth2_server; SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- 2.1 oauth2_authorization 令牌发放记录表 -- ---------------------------- CREATE TABLE oauth2_authorization ( id varchar(100) NOT NULL, registered_client_id varchar(100) NOT NULL, principal_name varchar(200) NOT NULL, authorization_grant_type varchar(100) NOT NULL, authorized_scopes varchar(1000) DEFAULT NULL, attributes blob DEFAULT NULL, state varchar(500) DEFAULT NULL, authorization_code_value blob DEFAULT NULL, authorization_code_issued_at timestamp DEFAULT NULL, authorization_code_expires_at timestamp DEFAULT NULL, authorization_code_metadata blob DEFAULT NULL, access_token_value blob DEFAULT NULL, access_token_issued_at timestamp DEFAULT NULL, access_token_expires_at timestamp DEFAULT NULL, access_token_metadata blob DEFAULT NULL, access_token_type varchar(100) DEFAULT NULL, access_token_scopes varchar(1000) DEFAULT NULL, oidc_id_token_value blob DEFAULT NULL, oidc_id_token_issued_at timestamp DEFAULT NULL, oidc_id_token_expires_at timestamp DEFAULT NULL, oidc_id_token_metadata blob DEFAULT NULL, refresh_token_value blob DEFAULT NULL, refresh_token_issued_at timestamp DEFAULT NULL, refresh_token_expires_at timestamp DEFAULT NULL, refresh_token_metadata blob DEFAULT NULL, user_code_value blob DEFAULT NULL, user_code_issued_at timestamp DEFAULT NULL, user_code_expires_at timestamp DEFAULT NULL, user_code_metadata blob DEFAULT NULL, device_code_value blob DEFAULT NULL, device_code_issued_at timestamp DEFAULT NULL, device_code_expires_at timestamp DEFAULT NULL, device_code_metadata blob DEFAULT NULL, PRIMARY KEY (id) ); -- ---------------------------- -- 2.2 oauth2_authorization_consent 授权记录表 -- ---------------------------- CREATE TABLE oauth2_authorization_consent ( registered_client_id varchar(100) NOT NULL, principal_name varchar(200) NOT NULL, authorities varchar(1000) NOT NULL, PRIMARY KEY (registered_client_id, principal_name) ); -- ---------------------------- -- 2.3 oauth2-registered-client OAuth2 客户端信息表 -- ---------------------------- CREATE TABLE oauth2_registered_client ( id varchar(100) NOT NULL, client_id varchar(100) NOT NULL, client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, client_secret varchar(200) DEFAULT NULL, client_secret_expires_at timestamp DEFAULT NULL, client_name varchar(200) NOT NULL, client_authentication_methods varchar(1000) NOT NULL, authorization_grant_types varchar(1000) NOT NULL, redirect_uris varchar(1000) DEFAULT NULL, post_logout_redirect_uris varchar(1000) DEFAULT NULL, scopes varchar(1000) NOT NULL, client_settings varchar(2000) NOT NULL, token_settings varchar(2000) NOT NULL, PRIMARY KEY (id) );
授权服务器
youlai-auth 模块作为认证授权服务器
maven 依赖
在 youlai-auth 模块的 pom.xml 添加授权服务器依赖
<!-- Spring Authorization Server 授权服务器依赖 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-authorization-server</artifactId> <version>1.1.1</version> </dependency>
application.yml
认证中心配置 oauth2_server 数据库连接信息
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/oauth2_server?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true username: root password: 123456
授权服务器配置
参考 Spring Authorization Server 官方示例 demo-authorizationserver
AuthorizationServierConfig
参考: Spring Authorization Server 官方示例 demo-authorizationserver 下的 AuthorizationServerConfig.java 进行授权服务器配置
package com.youlai.auth.config; /** * 授权服务器配置 * * @author haoxr * @since 3.0.0 */ @Configuration @RequiredArgsConstructor @Slf4j public class AuthorizationServerConfig { private final OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer; /** * 授权服务器端点配置 */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain( HttpSecurity http, AuthenticationManager authenticationManager, OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<?> tokenGenerator ) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); authorizationServerConfigurer .tokenEndpoint(tokenEndpoint -> tokenEndpoint .accessTokenRequestConverters( authenticationConverters ->// <1> authenticationConverters.addAll( // 自定义授权模式转换器(Converter) List.of( new PasswordAuthenticationConverter() ) ) ) .authenticationProviders(authenticationProviders ->// <2> authenticationProviders.addAll( // 自定义授权模式提供者(Provider) List.of( new PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator) ) ) ) .accessTokenResponseHandler(new MyAuthenticationSuccessHandler()) // 自定义成功响应 .errorResponseHandler(new MyAuthenticationFailureHandler()) // 自定义失败响应 ); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); http.securityMatcher(endpointsMatcher) .authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated()) .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) .apply(authorizationServerConfigurer); return http.build(); } @Bean // <5> public JWKSource<SecurityContext> jwkSource() { KeyPair keyPair = generateRsaKey(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); // @formatter:off RSAKey rsaKey = new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); // @formatter:on JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); } private static KeyPair generateRsaKey() { // <6> KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } return keyPair; } @Bean public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } @Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); } @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } @Bean public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate); // 初始化 OAuth2 客户端 initMallAppClient(registeredClientRepository); initMallAdminClient(registeredClientRepository); return registeredClientRepository; } @Bean public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { JdbcOAuth2AuthorizationService service = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository); JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository); rowMapper.setLobHandler(new DefaultLobHandler()); ObjectMapper objectMapper = new ObjectMapper(); ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader(); List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader); objectMapper.registerModules(securityModules); objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); // 使用刷新模式,需要从 oauth2_authorization 表反序列化attributes字段得到用户信息(SysUserDetails) objectMapper.addMixIn(SysUserDetails.class, SysUserMixin.class); objectMapper.addMixIn(Long.class, Object.class); rowMapper.setObjectMapper(objectMapper); service.setAuthorizationRowMapper(rowMapper); return service; } @Bean public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { // Will be used by the ConsentController return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository); } @Bean OAuth2TokenGenerator<?> tokenGenerator(JWKSource<SecurityContext> jwkSource) { JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource)); jwtGenerator.setJwtCustomizer(jwtCustomizer); OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator(); OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); return new DelegatingOAuth2TokenGenerator( jwtGenerator, accessTokenGenerator, refreshTokenGenerator); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } /** * 初始化创建商城管理客户端 * * @param registeredClientRepository */ private void initMallAdminClient(JdbcRegisteredClientRepository registeredClientRepository) { String clientId = "mall-admin"; String clientSecret = "123456"; String clientName = "商城管理客户端"; /* 如果使用明文,客户端认证时会自动升级加密方式,换句话说直接修改客户端密码,所以直接使用 bcrypt 加密避免不必要的麻烦 官方ISSUE: https://github.com/spring-projects/spring-authorization-server/issues/1099 */ String encodeSecret = passwordEncoder().encode(clientSecret); RegisteredClient registeredMallAdminClient = registeredClientRepository.findByClientId(clientId); String id = registeredMallAdminClient != null ? registeredMallAdminClient.getId() : UUID.randomUUID().toString(); RegisteredClient mallAppClient = RegisteredClient.withId(id) .clientId(clientId) .clientSecret(encodeSecret) .clientName(clientName) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .authorizationGrantType(AuthorizationGrantType.PASSWORD) // 密码模式 .authorizationGrantType(CaptchaAuthenticationToken.CAPTCHA) // 验证码模式 .redirectUri("http://127.0.0.1:8080/authorized") .postLogoutRedirectUri("http://127.0.0.1:8080/logged-out") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofDays(1)).build()) .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) .build(); registeredClientRepository.save(mallAppClient); } /** * 初始化创建商城APP客户端 * * @param registeredClientRepository */ private void initMallAppClient(JdbcRegisteredClientRepository registeredClientRepository) { String clientId = "mall-app"; String clientSecret = "123456"; String clientName = "商城APP客户端"; // 如果使用明文,在客户端认证的时候会自动升级加密方式,直接使用 bcrypt 加密避免不必要的麻烦 String encodeSecret = passwordEncoder().encode(clientSecret); RegisteredClient registeredMallAppClient = registeredClientRepository.findByClientId(clientId); String id = registeredMallAppClient != null ? registeredMallAppClient.getId() : UUID.randomUUID().toString(); RegisteredClient mallAppClient = RegisteredClient.withId(id) .clientId(clientId) .clientSecret(encodeSecret) .clientName(clientName) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .authorizationGrantType(WxMiniAppAuthenticationToken.WECHAT_MINI_APP) // 微信小程序模式 .authorizationGrantType(SmsCodeAuthenticationToken.SMS_CODE) // 短信验证码模式 .redirectUri("http://127.0.0.1:8080/authorized") .postLogoutRedirectUri("http://127.0.0.1:8080/logged-out") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofDays(1)).build()) .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) .build(); registeredClientRepository.save(mallAppClient); } }
密码模式扩展
PasswordAuthenticationToken
package com.youlai.auth.authentication.password; /** * 密码授权模式身份验证令牌(包含用户名和密码等) * * @author haoxr * @since 3.0.0 */ public class PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { public static final AuthorizationGrantType PASSWORD = new AuthorizationGrantType("password"); /** * 令牌申请访问范围 */ private final Set<String> scopes; /** * 密码模式身份验证令牌 * * @param clientPrincipal 客户端信息 * @param scopes 令牌申请访问范围 * @param additionalParameters 自定义额外参数(用户名和密码) */ public PasswordAuthenticationToken( Authentication clientPrincipal, Set<String> scopes, @Nullable Map<String, Object> additionalParameters ) { super(PASSWORD, clientPrincipal, additionalParameters); this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet()); } /** * 用户凭证(密码) */ @Override public Object getCredentials() { return this.getAdditionalParameters().get(OAuth2ParameterNames.PASSWORD); } public Set<String> getScopes() { return scopes; } }
PasswordAuthenticationConverter
package com.youlai.auth.authentication.password; /** * 密码模式参数解析器 * <p> * 解析请求参数中的用户名和密码,并构建相应的身份验证(Authentication)对象 * * @author haoxr * @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter * @since 3.0.0 */ public class PasswordAuthenticationConverter implements AuthenticationConverter { @Override public Authentication convert(HttpServletRequest request) { // 授权类型 (必需) String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) { return null; } // 客户端信息 Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); // 参数提取验证 MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request); // 令牌申请访问范围验证 (可选) String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE); if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) { OAuth2EndpointUtils.throwError( OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE, OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); } Set<String> requestedScopes = null; if (StringUtils.hasText(scope)) { requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); } // 用户名验证(必需) String username = parameters.getFirst(OAuth2ParameterNames.USERNAME); if (StrUtil.isBlank(username)) { OAuth2EndpointUtils.throwError( OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.USERNAME, OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI ); } // 密码验证(必需) String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD); if (StrUtil.isBlank(password)) { OAuth2EndpointUtils.throwError( OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.PASSWORD, OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI ); } // 附加参数(保存用户名/密码传递给 PasswordAuthenticationProvider 用于身份认证) Map<String, Object> additionalParameters = parameters .entrySet() .stream() .filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) && !e.getKey().equals(OAuth2ParameterNames.SCOPE) ).collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))); return new PasswordAuthenticationToken( clientPrincipal, requestedScopes, additionalParameters ); } }
PasswordAuthenticationProvider
package com.youlai.auth.authentication.password; /** * 密码模式身份验证提供者 * <p> * 处理基于用户名和密码的身份验证 * * @author haoxr * @since 3.0.0 */ @Slf4j public class PasswordAuthenticationProvider implements AuthenticationProvider { private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; private final AuthenticationManager authenticationManager; private final OAuth2AuthorizationService authorizationService; private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator; /** * Constructs an {@code OAuth2ResourceOwnerPasswordAuthenticationProviderNew} using the provided parameters. * * @param authenticationManager the authentication manager * @param authorizationService the authorization service * @param tokenGenerator the token generator * @since 0.2.3 */ public PasswordAuthenticationProvider(AuthenticationManager authenticationManager, OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator ) { Assert.notNull(authorizationService, "authorizationService cannot be null"); Assert.notNull(tokenGenerator, "tokenGenerator cannot be null"); this.authenticationManager = authenticationManager; this.authorizationService = authorizationService; this.tokenGenerator = tokenGenerator; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { PasswordAuthenticationToken resourceOwnerPasswordAuthentication = (PasswordAuthenticationToken) authentication; OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils .getAuthenticatedClientElseThrowInvalidClient(resourceOwnerPasswordAuthentication); RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); // 验证客户端是否支持授权类型(grant_type=password) if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); } // 生成用户名密码身份验证令牌 Map<String, Object> additionalParameters = resourceOwnerPasswordAuthentication.getAdditionalParameters(); String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME); String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password); // 用户名密码身份验证,成功后返回带有权限的认证信息 Authentication usernamePasswordAuthentication; try { usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken); } catch (Exception e) { // 需要将其他类型的异常转换为 OAuth2AuthenticationException 才能被自定义异常捕获处理,逻辑源码 OAuth2TokenEndpointFilter#doFilterInternal throw new OAuth2AuthenticationException(e.getCause() != null ? e.getCause().getMessage() : e.getMessage()); } // 验证申请访问范围(Scope) Set<String> authorizedScopes = registeredClient.getScopes(); Set<String> requestedScopes = resourceOwnerPasswordAuthentication.getScopes(); if (!CollectionUtils.isEmpty(requestedScopes)) { Set<String> unauthorizedScopes = requestedScopes.stream() .filter(requestedScope -> !registeredClient.getScopes().contains(requestedScope)) .collect(Collectors.toSet()); if (!CollectionUtils.isEmpty(unauthorizedScopes)) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE); } authorizedScopes = new LinkedHashSet<>(requestedScopes); } // 访问令牌(Access Token) 构造器 DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() .registeredClient(registeredClient) .principal(usernamePasswordAuthentication) // 身份验证成功的认证信息(用户名、权限等信息) .authorizationServerContext(AuthorizationServerContextHolder.getContext()) .authorizedScopes(authorizedScopes) .authorizationGrantType(AuthorizationGrantType.PASSWORD) // 授权方式 .authorizationGrant(resourceOwnerPasswordAuthentication) // 授权具体对象 ; // 生成访问令牌(Access Token) OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType((OAuth2TokenType.ACCESS_TOKEN)).build(); OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext); if (generatedAccessToken == null) { OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the access token.", ERROR_URI); throw new OAuth2AuthenticationException(error); } OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); // 权限数据(perms)比较多通过反射移除,不随令牌一起持久化至数据库 ReflectUtil.setFieldValue(usernamePasswordAuthentication.getPrincipal(), "perms", null); OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) .principalName(usernamePasswordAuthentication.getName()) .authorizationGrantType(AuthorizationGrantType.PASSWORD) .authorizedScopes(authorizedScopes) .attribute(Principal.class.getName(), usernamePasswordAuthentication); // attribute 字段 if (generatedAccessToken instanceof ClaimAccessor) { authorizationBuilder.token(accessToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims())); } else { authorizationBuilder.accessToken(accessToken); } // 生成刷新令牌(Refresh Token) OAuth2RefreshToken refreshToken = null; if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) && // Do not issue refresh token to public client !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) { tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build(); OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext); if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) { OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the refresh token.", ERROR_URI); throw new OAuth2AuthenticationException(error); } refreshToken = (OAuth2RefreshToken) generatedRefreshToken; authorizationBuilder.refreshToken(refreshToken); } OAuth2Authorization authorization = authorizationBuilder.build(); // 持久化令牌发放记录到数据库 this.authorizationService.save(authorization); additionalParameters = Collections.emptyMap(); return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters); } /** * 判断传入的 authentication 类型是否与当前认证提供者(AuthenticationProvider)相匹配--模板方法 * <p> * ProviderManager#authenticate 遍历 providers 找到支持对应认证请求的 provider-迭代器模式 * * @param authentication * @return */ @Override public boolean supports(Class<?> authentication) { return PasswordAuthenticationToken.class.isAssignableFrom(authentication); } }
Spring Authorization Server 1.1 扩展实现 OAuth2 密码模式与 Spring Cloud 的整合实战(下):