前言
在当今数字化时代,随着企业业务规模和复杂性的不断增加,传统的单体应用架构已经难以满足日益增长的需求。微服务架构的兴起,以其高度模块化、可扩展性和可维护性的优势,逐渐成为企业架构升级的首选方案。然而,随着微服务的普及,如何保障服务的安全性、实现用户身份的统一管理和认证,成为了摆在企业和开发者面前的重要课题。
OAuth 2.1作为一种开放标准,为微服务架构下的用户身份认证和授权提供了强有力的支持。它允许第三方应用获取用户在特定服务上的有限访问权限,而无需获取用户的用户名和密码。通过OAuth 2.1,企业可以构建安全、灵活的身份认证体系,实现用户身份的统一管理,确保服务间的数据安全和隐私保护。
本文档旨在探讨微服务集成OAuth 2.1的实践与应用。我们将从OAuth 2.1的基本原理入手,介绍其核心概念和工作流程。接着,我们将详细阐述如何在微服务架构中集成OAuth 2.1,包括认证服务器的搭建、客户端的配置、以及授权流程的实现。此外,我们还将分享一些在实际应用中可能遇到的问题和解决方案,帮助读者更好地理解和应用OAuth 2.1。
通过本文档的学习,读者将能够掌握微服务集成OAuth 2.1的关键技术和方法,为企业构建安全、高效的微服务架构提供有力支持。我们相信,随着OAuth 2.1在微服务领域的广泛应用,它将为企业带来更多的商业价值和发展机遇。
步骤
初始化数据库
/*
Navicat Premium Data Transfer
Source Server : [email protected]
Source Server Type : MySQL
Source Server Version : 80200
Source Host : 154.8.144.189:13306
Source Schema : unified_certification
Target Server Type : MySQL
Target Server Version : 80200
File Encoding : 65001
Date: 25/03/2024 13:24:15
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for authorities
-- ----------------------------
DROP TABLE IF EXISTS `authorities`;
CREATE TABLE `authorities` (
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`authority` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
UNIQUE INDEX `ix_auth_username`(`username`, `authority`) USING BTREE,
CONSTRAINT `fk_authorities_users` FOREIGN KEY (`username`) REFERENCES `users` (`username`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for oauth2_authorization
-- ----------------------------
DROP TABLE IF EXISTS `oauth2_authorization`;
CREATE TABLE `oauth2_authorization` (
`id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`authorization_grant_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`authorized_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`attributes` blob NULL,
`state` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`authorization_code_value` blob NULL,
`authorization_code_issued_at` timestamp NULL DEFAULT NULL,
`authorization_code_expires_at` timestamp NULL DEFAULT NULL,
`authorization_code_metadata` blob NULL,
`access_token_value` blob NULL,
`access_token_issued_at` timestamp NULL DEFAULT NULL,
`access_token_expires_at` timestamp NULL DEFAULT NULL,
`access_token_metadata` blob NULL,
`access_token_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`access_token_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`oidc_id_token_value` blob NULL,
`oidc_id_token_issued_at` timestamp NULL DEFAULT NULL,
`oidc_id_token_expires_at` timestamp NULL DEFAULT NULL,
`oidc_id_token_metadata` blob NULL,
`refresh_token_value` blob NULL,
`refresh_token_issued_at` timestamp NULL DEFAULT NULL,
`refresh_token_expires_at` timestamp NULL DEFAULT NULL,
`refresh_token_metadata` blob NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for oauth2_authorization_consent
-- ----------------------------
DROP TABLE IF EXISTS `oauth2_authorization_consent`;
CREATE TABLE `oauth2_authorization_consent` (
`registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`authorities` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
PRIMARY KEY (`registered_client_id`, `principal_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for oauth2_registered_client
-- ----------------------------
DROP TABLE IF EXISTS `oauth2_registered_client`;
CREATE TABLE `oauth2_registered_client` (
`id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`client_id_issued_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`client_secret` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`client_secret_expires_at` timestamp NULL DEFAULT NULL,
`client_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`client_authentication_methods` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`authorization_grant_types` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`redirect_uris` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`client_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`token_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`PASSWORD` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`enabled` tinyint(1) NOT NULL,
PRIMARY KEY (`username`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
引入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.26</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
添加相关配置
server:
port: 9000
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/unified_certification?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
hikari:
pool-name: HikariCP
minimum-idle: 5
maximum-pool-size: 15
auto-commit: true
idle-timeout: 30000
connection-timeout: 30000
connection-test-query: SELECT 1
max-lifetime: 25200000
授权服务配置
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 定义授权服务配置器
OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer();
configurer.tokenEndpoint(tokenEndpoint -> {
tokenEndpoint.accessTokenRequestConverter(new OAuth2PasswordAuthenticationConverter());
})
// 自定义授权页面
// .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
// Enable OpenID Connect 1.0, 启用 OIDC 1.0
.oidc(Customizer.withDefaults());
// 获取授权服务器相关的请求端点
RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher();
http
// 拦截对授权服务器相关端点的请求
.requestMatcher(endpointsMatcher)
// 拦载到的请求需要认证
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
// 忽略掉相关端点的 CSRF(跨站请求): 对授权端点的访问可以是跨站的
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.exceptionHandling(exceptions ->
exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
// 应用授权服务器的配置
.apply(configurer)
.and()
// 登出处理
.logout().deleteCookies("JSESSIONID")
.invalidateHttpSession(true); // SSO登出成功处理;
DefaultSecurityFilterChain securityFilterChain = http.build();
// 注入自定义授权模式实现
http.authenticationProvider(
new OAuth2PasswordAuthenticationProvider(
http.getSharedObject(OAuth2AuthorizationService.class),
http.getSharedObject(JwtGenerator.class),
new OAuth2RefreshTokenGenerator(),
http.getSharedObject(AuthenticationManager.class)
));
return securityFilterChain;
}
/**
* 注册客户端应用, 对应 oauth2_registered_client 表
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
// JWT(Json Web Token)的配置项:TTL、是否复用refreshToken等等
TokenSettings tokenSettings = TokenSettings.builder()
// 令牌存活时间:2小时
.accessTokenTimeToLive(Duration.ofHours(2))
// 令牌可以刷新,重新获取
.reuseRefreshTokens(true)
// 刷新时间:30天(30天内当令牌过期时,可以用刷新令牌重新申请新令牌,不需要再认证)
.refreshTokenTimeToLive(Duration.ofDays(30))
.build();
// 客户端相关配置
ClientSettings clientSettings = ClientSettings.builder()
// 是否需要用户授权确认
.requireAuthorizationConsent(true)
.build();
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
// 客户端ID和密码
.clientId("messaging-client")
// .clientSecret("{bcrypt}" + new BCryptPasswordEncoder().encode("secret"))
.clientSecret("{noop}secret")
// 授权方法
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 授权模式(授权码模式)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
// 刷新令牌(授权码模式)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 回调地址:授权服务器向当前客户端响应时调用下面地址, 不在此列的地址将被拒绝, 只能使用IP或域名,不能使用 localhost
.redirectUri("http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc")
// OIDC 支持
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
// 授权范围(当前客户端的授权范围)
.scope("message.read")
.scope("message.write")
// JWT(Json Web Token)配置项
.tokenSettings(tokenSettings)
// 客户端配置项
.clientSettings(clientSettings)
.build();
if (registeredClientRepository.findByClientId("messaging-client") == null) {
registeredClientRepository.save(registeredClient);
}
return registeredClientRepository;
}
/**
* 令牌的发放记录, 对应 oauth2_authorization 表
*/
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
/**
* 把资源拥有者授权确认操作保存到数据库, 对应 oauth2_authorization_consent 表
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
/**
* 加载 JWT 资源, 用于生成令牌
*/
@Bean
public JWKSource<SecurityContext> jwkSource() {
// 生成密钥对
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
// 从密钥对中获取公钥和私钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 创建 RSAKey 对象,设置公钥、私钥和 keyID
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// 创建 JWKSet 对象,并添加 rsaKey
JWKSet jwkSet = new JWKSet(rsaKey);
// 返回 JWKSource 对象,用于选择 JWKSet 中的密钥
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
/**
* JWT 解码
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* AuthorizationServerS 的相关配置
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
WebSecurity相关配置
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class ServerSecurityConfig {
@Resource
private DataSource dataSource;
/**
* Spring Security 的过滤器链,用于 Spring Security 的身份认证
*/
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
// 配置放行的请求
.antMatchers("/api/**", "/login").permitAll()
// 其他任何请求都需要认证
.anyRequest().authenticated()
)
// 设置登录表单页面
.formLogin(Customizer.withDefaults());
// .formLogin(formLoginConfigurer -> formLoginConfigurer.loginPage("/login"));
return http.build();
}
@Bean
// 创建一个 UserDetailsManager 对象
UserDetailsManager userDetailsManager() {
// 使用 JdbcUserDetailsManager 来实现 UserDetailsManager 接口
JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager(dataSource);
// 创建一个 UserDetails 对象
UserDetails userDetails = User.builder()
// 设置密码编码器为 BCryptPasswordEncoder
.passwordEncoder(s -> "{bcrypt}" + new BCryptPasswordEncoder().encode(s))
// 设置用户名
.username("user")
// 设置密码
.password("password")
// 设置角色为 ADMIN
.roles("ADMIN")
// 构建 UserDetails 对象
.build();
// 判断用户是否存在
if (!userDetailsManager.userExists(userDetails.getUsername())) {
// 如果用户不存在,则创建用户
userDetailsManager.createUser(userDetails);
}
// 返回 UserDetailsManager 对象
return userDetailsManager;
}
}
自定义密码授权转换器
public class OAuth2PasswordAuthenticationConverter implements AuthenticationConverter {
public final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
@Override
public Authentication convert(HttpServletRequest request) {
//验证是否是密码模式
String grantType = request.getParameter("grant_type");
if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) {
return null;
} else {
// 获取客户端认证信息
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
//获取请求中所有参数
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
//获取用户名信息
String username = parameters.getFirst("username");
//验证用户名是否为空,且只有一个
if (!StringUtils.hasText(username) || parameters.get("password").size() != 1) {
OAuth2EndpointUtils.throwError("invalid_request", "username", null);
}
//获取密码信息
String password = parameters.getFirst("password");
//验证密码是否为空,且只有一个
if (!StringUtils.hasText(password) || parameters.get("password").size() != 1) {
OAuth2EndpointUtils.throwError("invalid_request", "password", null);
}
//获取其他参数
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals("grant_type") && !key.equals("client_id") && !key.equals("username") && !key.equals("password")) {
additionalParameters.put(key, value.get(0));
}
});
// 获取 scope 参数
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE, ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// 解析 scope 参数值
Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
// 返回 OAuth2PasswordAuthenticationToken 对象
return new OAuth2PasswordAuthenticationToken(username, clientPrincipal, password, additionalParameters, requestedScopes);
}
}
}
自定义密码授权实体
public class OAuth2PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
@Getter
private final String username;
@Getter
private final String password;
@Getter
private final Set<String> scopes;
public OAuth2PasswordAuthenticationToken(
// 用户名
String username,
// 客户端主体
Authentication clientPrincipal,
// 密码
String password,
// 附加参数
Map<String, Object> additionalParameters,
// 授权范围
Set<String> scopes) {
// 调用父类构造函数,设置授权类型为密码模式
super(AuthorizationGrantType.PASSWORD, clientPrincipal, additionalParameters);
// 断言用户名不为空
Assert.hasText(username, "code cannot be empty");
// 设置用户名
this.username = username;
// 设置密码
this.password = password;
// 设置授权范围,并确保不可修改
this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
}
}
自定义密码授权处理器
public class OAuth2PasswordAuthenticationProvider implements AuthenticationProvider {
private static final Logger LOGGER = LogManager.getLogger(OAuth2PasswordAuthenticationProvider.class);
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
private final AuthenticationManager authenticationManager;
private final OAuth2TokenGenerator<? extends OAuth2Token> refreshTokenGenerator;
public OAuth2PasswordAuthenticationProvider(OAuth2AuthorizationService authorizationService,
JwtGenerator tokenGenerator,
OAuth2RefreshTokenGenerator refreshTokenGenerator,
AuthenticationManager authenticationManager) {
// 断言authorizationService不为空
Assert.notNull(authorizationService, "authorizationService cannot be null");
// 断言tokenGenerator不为空
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
// 将传入的authorizationService赋值给当前对象的authorizationService成员变量
this.authorizationService = authorizationService;
// 将传入的tokenGenerator赋值给当前对象的tokenGenerator成员变量
this.tokenGenerator = tokenGenerator;
// 将传入的refreshTokenGenerator赋值给当前对象的refreshTokenGenerator成员变量
this.refreshTokenGenerator = refreshTokenGenerator;
// 将传入的authenticationManager赋值给当前对象的authenticationManager成员变量
this.authenticationManager = authenticationManager;
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 类型转换
OAuth2PasswordAuthenticationToken authenticationToken = (OAuth2PasswordAuthenticationToken) authentication;
// 获取客户端认证信息
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(
authenticationToken);
// 获取客户端信息
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// 断言客户端信息不为空
assert registeredClient != null;
//验证是否支持密码模式
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
// 获取客户端认证方法
Set<String> authorizedScopes = new LinkedHashSet<>();
if (!CollectionUtils.isEmpty(authenticationToken.getScopes())) {
for (String requestedScope : authenticationToken.getScopes()) {
if (!registeredClient.getScopes().contains(requestedScope)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
}
}
authorizedScopes = new LinkedHashSet<>(authenticationToken.getScopes());
}
// 获取用户名
String username = authenticationToken.getUsername();
// 获取密码
String password = authenticationToken.getPassword();
// 创建用户名密码认证实体
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
// 认证
Authentication usernamePasswordAuthentication = authenticationManager
.authenticate(usernamePasswordAuthenticationToken);
// 创建token上下文
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthentication)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(authorizedScopes)
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.authorizationGrant(authenticationToken);
// 创建授权
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization
.withRegisteredClient(registeredClient)
.principalName(usernamePasswordAuthentication.getName())
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.authorizedScopes(authorizedScopes);
// 创建token上下文
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
// 生成token
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
// 判断生成的token是否为空
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);
}
// 创建accessToken
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
// 判断生成的token是否为ClaimAccessor类型
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) -> {
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
})
.attribute(Principal.class.getName(), usernamePasswordAuthentication);
} else {
authorizationBuilder.accessToken(accessToken);
}
// 创建refreshToken
OAuth2RefreshToken refreshToken = null;
// 判断是否支持刷新令牌
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) && !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.refreshTokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
OAuth2Error error = new OAuth2Error("server_error", "The token generator failed to generate the refresh token.", "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2");
throw new OAuth2AuthenticationException(error);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
authorizationBuilder.refreshToken(refreshToken);
}
// 创建授权
OAuth2Authorization authorization = authorizationBuilder.build();
// 保存授权
this.authorizationService.save(authorization);
LOGGER.debug("returning OAuth2AccessTokenAuthenticationToken");
// 创建认证
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, Collections.emptyMap());
}
@Override
public boolean supports(Class<?> authentication) {
// 判断传入的authentication对象是否是指定类型或其子类的实例
return OAuth2PasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
用到的工具类
public class OAuth2AuthenticationProviderUtils {
private OAuth2AuthenticationProviderUtils() {
}
public static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
// 定义一个OAuth2ClientAuthenticationToken类型的变量clientPrincipal,用于存储客户端主体
OAuth2ClientAuthenticationToken clientPrincipal = null;
// 判断authentication中的主体是否为OAuth2ClientAuthenticationToken类型
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
// 如果是,则将其转换为OAuth2ClientAuthenticationToken类型,并赋值给clientPrincipal
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
}
// 判断clientPrincipal是否不为null且已经认证通过
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
// 如果是,则返回clientPrincipal
return clientPrincipal;
} else {
// 否则,抛出OAuth2AuthenticationException异常,表示客户端无效
throw new OAuth2AuthenticationException("invalid_client");
}
}
}
public class OAuth2EndpointUtils {
public static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
// 获取请求参数映射表
Map<String, String[]> parameterMap = request.getParameterMap();
// 创建一个可修改的键值对集合
MultiValueMap<String, String> parameters = new LinkedMultiValueMap(parameterMap.size());
// 遍历参数映射表
parameterMap.forEach((key, values) -> {
// 如果参数值数组长度大于0
if (values.length > 0) {
// 定义一个变量来引用参数值数组
String[] var3 = values;
// 获取参数值数组的长度
int var4 = values.length;
// 遍历参数值数组
for (int var5 = 0; var5 < var4; ++var5) {
// 获取当前参数值
String value = var3[var5];
// 将参数名和参数值添加到集合中
parameters.add(key, value);
}
}
});
// 返回参数集合
return parameters;
}
public static void throwError(String errorCode, String parameterName, String errorUri) {
// 创建一个OAuth2Error对象,包含错误码、错误信息和错误URI
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
// 抛出一个OAuth2AuthenticationException异常,包含上述创建的OAuth2Error对象
throw new OAuth2AuthenticationException(error);
}
}
测试
授权码模式
- 请求授权地址
OIDC模式需要将scope值设置为openid
- 重定向到登录,输入用户名密码登录
- 跳转到授权页面,勾选授权信息
- 重定向到回调地址并携带code
- 获取token
- OIDC获取token
刷新token
- 刷新token
OIDC刷新token
客户端模式
密码模式
获取用户信息
下面展示的是OIDC授权默认的获取用户信息接口,如果是其他模式授权需要自定义获取用户信息接口
总结
该集成方案是依据官方提供的例子完成的,额外支持了密码模式。
标签:ci,Oauth2,utf8mb4,Spring,token,SET,new,Security,NULL From: https://blog.csdn.net/qq_38036909/article/details/137018125