OAuth2服务端
OAuth 2.0 是一种广泛使用的授权框架,用于第三方应用程序安全地访问用户资源,而无需用户提供其用户名和密码。OAuth 2.0 主要用于授权,而不是认证。本文将详细介绍 OAuth 2.0 的内存集成和持久化实现,并解决一些常见的问题。
一.在内存中集成
在内存中的 OAuth 2.0 实现通常用于测试环境,因为它简单且易于实现。内存中的实现通常包含以下数据:
- 访问令牌(Access Token):用于访问受保护资源的令牌。
- 刷新令牌(Refresh Token):用于获取新的访问令牌,以延长访问权限的有效期。
- 客户端的标识(client_id):客户端的唯一标识符。
- 客户端密钥(client_secret):客户端的密钥,用于验证客户端身份。
- 重定向 URI:客户端指定的重定向地址,授权服务器在授权成功后将用户重定向到该地址。
- 授权码(Authorization Code):用于换取访问令牌的临时代码。
1.代码
在 pom.xml 文件中添加以下依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
</parent>
<!--OAuth 2.0 授权服务器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
授权服务配置
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
@Configuration
public class AuthorizationServerConfig {
/**
* Spring Security的过滤器链,用于协议端点的
* @param http
* @return
* @throws Exception
*/
@Bean
// 设置此过滤链的优先级为1,确保在其他过滤链之前执行
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
/**
* OAuth 2.0 授权服务器应用默认的安全配置,如下
* 1.配置授权端点的安全性:
* 授权端点(如 /oauth2/authorize)会被配置为需要身份验证。这意味着只有经过身份验证的用户才能访问这些端点。
* 令牌端点(如 /oauth2/token)会被配置为需要客户端认证。这意味着只有合法的客户端才能请求访问令牌。
* 2.配置错误处理:
* 当请求未通过身份验证或授权时,会自动重定向到登录页面或返回相应的错误响应。
* 3.配置 CORS 支持:
* 默认情况下,会启用跨域资源共享(CORS)支持,以便客户端可以从不同的域请求授权服务器的资源。
* 4.配置 CSRF 保护:
* 默认情况下,会启用跨站请求伪造(CSRF)保护,以防止恶意攻击者利用用户的会话进行未经授权的操作。
* 5.配置 JWT 支持:
* 如果启用了 JWT 支持,会自动配置 JWT 编码和解码的相关设置。
*/
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// 启用 OpenID Connect 1.0 支持
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
// 当未通过身份验证时,从授权端点重定向到登录页面
http.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
// 指定登录页面的URL
new LoginUrlAuthenticationEntryPoint("/login"),
// 匹配HTML请求
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// 接受访问令牌以获取用户信息或客户端注册信息
.oauth2ResourceServer((resourceServer) -> resourceServer
// 使用JWT进行资源服务器保护
.jwt(Customizer.withDefaults()));
// 构建并返回 SecurityFilterChain
return http.build();
}
/**
* 管理客户端
* @return
*/
@Bean
public RegisteredClientRepository registeredClientRepository() {
// 生成唯一的客户端 ID
RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
// 设置客户端 ID
.clientId("oidc-client")
// 设置客户端密钥,使用明文
.clientSecret("{noop}secret")
// 设置客户端认证方法
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 允许授权码授予类型
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
// 允许刷新令牌授予类型
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// 允许客户端凭证授予类型
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 添加重定向 URI
.redirectUri("https://www.baidu.com")
// 添加另一个重定向 URI
.redirectUri("http://localhost:9001/login/oauth2/code/oidc-client")
// 添加第三个重定向 URI
.redirectUri("http://localhost:9001/api/login/welcome")
// 设置注销后的重定向 URI
.postLogoutRedirectUri("http://127.0.0.1:8080/")
/**
* 添加 OpenID 范围
* 用于请求用户的基本身份验证信息,是使用 OpenID Connect 1.0 时的必需范围
*/
.scope(OidcScopes.OPENID)
/**
* 添加用户资料范围
* 用于请求用户的个人资料信息,是可选范围,客户端可以根据需要请求这个范围来获取更多的用户信息。
*/
.scope(OidcScopes.PROFILE)
// 自定义范围:读取消息
.scope("message.read")
// 自定义范围:写入消息
.scope("message.write")
// 自定义范围:所有权限
.scope("all")
// 设置客户端需要页面审核授权
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
// 返回内存中的客户端注册仓库
return new InMemoryRegisteredClientRepository(oidcClient);
}
/**
* 签名访问令牌
* @return
*/
@Bean
public JWKSource<SecurityContext> jwkSource() {
// 生成 RSA 密钥对
KeyPair keyPair = generateRsaKey();
// 获取公钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
// 获取私钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
// 设置私钥
.privateKey(privateKey)
// 设置键 ID
.keyID(UUID.randomUUID().toString())
.build();
// 创建 JWKSet
JWKSet jwkSet = new JWKSet(rsaKey);
// 返回不可变的 JWKSet
return new ImmutableJWKSet<>(jwkSet);
}
/**
* 启动时生成的带有密钥的KeyPair实例,用于创建上面的JWKSource
* @return
*/
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
// 获取 RSA 密钥对生成器
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
// 初始化密钥长度为 2048 位
keyPairGenerator.initialize(2048);
// 生成密钥对
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {
// 如果生成密钥对失败,抛出异常
throw new IllegalStateException(ex);
}
return keyPair;
}
/**
* JwtDecoder的一个实例,用于解码已签名的访问令牌
* @param jwkSource
* @return
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
// 创建 JWT 解码器
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* 配置Spring Authorization Server的AuthorizationServerSettings实例
* @return
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
// 创建授权服务器设置
return AuthorizationServerSettings.builder().build();
}
}
SpringSecurity配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
/**
* Spring Security的过滤器链,用于Spring Security的身份认证
*/
@EnableWebSecurity
/**
* 禁用代理 Bean 方法以提高性能
* 优化配置类的性能,通过减少方法调用检查的开销来提升性能。使用这个注解时,开发人员需要确保 @Bean 方法的行为是幂等的,以避免潜在的问题。
*/
@Configuration(proxyBeanMethods = false)
public class DefaultSecurityConfig {
@Bean
// 设置此过滤链的优先级为 2,确保它在授权服务器过滤链之后执行
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
// 允许访问 /actuator/**
.requestMatchers(new AntPathRequestMatcher("/actuator/**"),
// 允许访问 /oauth2/**
new AntPathRequestMatcher("/oauth2/**"),
// 允许访问所有 .json 文件
new AntPathRequestMatcher("/**/*.json"),
// 允许访问所有 .html 文件
new AntPathRequestMatcher("/**/*.html"))
// 上述路径无需身份验证
.permitAll()
// 所有其他请求都需要身份验证
.anyRequest().authenticated()
)
// 启用 CORS 支持
.cors(Customizer.withDefaults())
// 禁用 CSRF 保护
.csrf((csrf) -> csrf.disable())
// 启用表单登录,并且表单登录处理从授权服务器过滤链重定向到登录页面
.formLogin(Customizer.withDefaults())
;
// 构建并返回 SecurityFilterChain
return http.build();
}
/**
* 用户身份验证
*
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
// 使用默认的密码编码器
UserDetails userDetails = User.withDefaultPasswordEncoder()
// 设置用户名
.username("user")
// 设置密码
.password("password")
// 设置用户角色
.roles("USER")
.build();
// 返回内存中的用户详细信息服务
return new InMemoryUserDetailsManager(userDetails);
}
}
2.接口
1.查看授权服务配置接口
获取授权服务器的配置信息,包含授权端点、令牌端点、用户信息端点、密钥集端点。
地址
GET http://127.0.0.1:8080/.well-known/openid-configurati
响应
{
"issuer": "http://127.0.0.1:8080",
"authorization_endpoint": "http://127.0.0.1:8080/oauth2/authorize",
"device_authorization_endpoint": "http://127.0.0.1:8080/oauth2/device_authorization",
"token_endpoint": "http://127.0.0.1:8080/oauth2/token",
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"jwks_uri": "http://127.0.0.1:8080/oauth2/jwks",
"userinfo_endpoint": "http://127.0.0.1:8080/userinfo",
"end_session_endpoint": "http://127.0.0.1:8080/connect/logout",
"response_types_supported": [
"code"
],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code"
],
"revocation_endpoint": "http://127.0.0.1:8080/oauth2/revoke",
"revocation_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"introspection_endpoint": "http://127.0.0.1:8080/oauth2/introspect",
"introspection_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"scopes_supported": [
"openid"
]
}
2.获取授权码或访问令牌
用户同意授权后获取授权码或访问令牌的步骤如下:
- 用户访问端点:用户访问客户端提供的授权请求 URL,被重定向到授权服务器的登录页面进行身份验证,通常需要输入用户名和密码。
- 身份验证成功:用户在身份验证成功后,会被提示是否同意授权客户端访问特定的资源或范围(scopes)。
- 用户同意授权:如果用户同意授权,授权服务器会将用户重定向回客户端提供的重定向 URI,并附带授权码或访问令牌(取决于使用的授权类型)。
地址
GET http://localhost:8080/oauth2/authorize?response_type=code&client_id=oidc-client&scope=message.read openid&redirect_uri=https://www.baidu.com
响应
使用user/password登录
3.获取访问令牌接口
客户端向授权服务器请求访问令牌(access token)。这个端点的主要作用是验证客户端的身份和授权请求,并返回相应的访问令牌。访问令牌可以用于后续的 API 请求,以证明客户端具有访问特定资源的权限。
Authorization:Basic b2lkYy1jbGllbnQ6c2VjcmV0(将 client_id 和 client_secret 通过 : 拼接起来后进行 Base64 编码后得到的字符串,在该字符串前拼接 Basic )
POST
http://localhost:8080/oauth2/token?grant_type=authorization_code&code=FouwKhWPOTGdMo11c5uKAE3F7ilLNbY7ExLvuS2VBDuIpVwiADqPQeh_iX3W2g8lwm3UVHG23z6tQ7TBsH7FzQ_j9NtTSKSWyoEcAf3ZQmmZKwVQOE3kPV0aFfxCWNox&redirect_uri=https://www.baidu.com
form-data
client_id=oidc-client
client_secret=secret
header
Authorization=Basic b2lkYy1jbGllbnQ6c2VjcmV0
响应
{
"access_token": "eyJraWQiOiI2OWUxZDllZS01YmJiLTRiNTgtOWE0YS03MDgzMjczNWYxOTYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoib2lkYy1jbGllbnQiLCJuYmYiOjE3MzA0NjgyMzUsInNjb3BlIjpbIm9wZW5pZCIsIm1lc3NhZ2UucmVhZCJdLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJleHAiOjE3MzA0Njg1MzUsImlhdCI6MTczMDQ2ODIzNX0.OErfPE7xARYx05vJZXqSc2HuV6CEW6O8C_yeEJo9_Lx3k28KSRBOc0bdTzg_lDhGs08D91fFlFhpzlcv11uJVlWLkuPSxmvvv5OPM6C_i0Z5S8YHDsWRZARvl7CFmSE5Raq3rfD4cxwKh5TjQDuXrcE_aD_nzdsg2SJpKTy_AvoFUSkY-VIl0o1OESkuN-x6JFxMehEYJghRABMXqo1JYs3q9o4jBmzjjA5qNtahbB24cO1o1ilyZOeJGNMwGwJksJoPIbxmKNFS4RKvgP2QI-hFEkOkER3QkBtn-flw_Ylr1SfY-sGrKULtDAbbY5bxb8Z2nvitbKsRmHdRHrCmwg",
"refresh_token": "sHsM7NQhhscD-z3wzd29MG2ALZxt4klkSjIaM6kW-Sf_9S1oaMvtqsBgoLP6RzdGN-M1LotY3ER469hl8k5IQNAMWjU0a0riE7Adw_y5IgGaQG6zG5_2GfL8zIK6vY6_",
"scope": "openid message.read",
"id_token": "eyJraWQiOiI2OWUxZDllZS01YmJiLTRiNTgtOWE0YS03MDgzMjczNWYxOTYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoib2lkYy1jbGllbnQiLCJhenAiOiJvaWRjLWNsaWVudCIsImF1dGhfdGltZSI6MTczMDQ2ODEzMCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiZXhwIjoxNzMwNDcwMDM1LCJpYXQiOjE3MzA0NjgyMzUsInNpZCI6IklZRHhuNDFLb3o0SXhCS2xmT2xUd25qNG8zYzd3TEpJbVA5a05VTWRiWlUifQ.U15LoaAhdYQSpgGHHePkKmI7XwOpIUxKZur1Kay44tf4StiJqEOiOxwBa6fNr6gwiIwAZBWp8iy59hZpTuXXPA83Df0frBK0VoK43pQUHCKK2KrGk-H068_sKUjDbhGvAtkA8fbprgOAJQ-1IeAdHITczgZqqT5vtKRJkePSruPtw0PeGPHiGOzjcV2RPnokJ4Q7p9Ix2kfUdWFhyPGa4_CM5VweHOU2fJWX7J6zmuYY8YrpdvexACMMXIWY5posNC6gHXLZNHzUoh-VepzgDnV2tgG7nl9E1NeRDePeMao7WCkj19nUfhZrBWr6uihaih8z5Gb5gRIfsXT74rU0PA",
"token_type": "Bearer",
"expires_in": 300
}
4.获取用户信息接口
获取用户信息接口允许客户端使用访问令牌(access token)查询用户的详细信息。这些信息通常包括用户的唯一标识符(sub)、姓名、电子邮件等。
地址
POST http://localhost:8080/userinfo
请求头
- Authorization:
Bearer ACCESS_TOKEN
(将ACCESS_TOKEN
替换为实际的访问令牌)
响应
{
"sub": "user",
"name": "John Doe",
"email": "john.doe@example.com",
"picture": "https://example.com/profile.jpg"
}
3.状态码
1.客户端错误
- 400 Bad Request
-
- 意义:请求无效或格式错误。
- 示例:客户端发送的请求缺少必要的参数,或者参数格式不正确。
- 401 Unauthorized
-
- 意义:请求需要用户认证。
- 示例:客户端尝试访问受保护的资源,但未提供有效的访问令牌。
- 403 Forbidden
-
- 意义:请求被拒绝,即使提供了正确的认证信息。
- 示例:客户端拥有有效的访问令牌,但没有足够的权限访问请求的资源。
- 404 Not Found
-
- 意义:请求的资源不存在。
- 示例:客户端请求的端点不存在。
2.服务器错误
- 500 Internal Server Error
-
- 意义:服务器遇到意外情况,无法完成请求。
- 示例:授权服务器内部发生错误,无法处理客户端的请求。
- 501 Not Implemented
-
- 意义:服务器不支持请求的方法。
- 示例:客户端使用了授权服务器不支持的 HTTP 方法。
- 502 Bad Gateway
-
- 意义:服务器作为网关或代理,从上游服务器收到了无效的响应。
- 示例:授权服务器作为代理,从其他服务收到了无效的响应。
- 503 Service Unavailable
-
- 意义:服务器暂时无法处理请求,通常是因为过载或维护。
- 示例:授权服务器正在进行维护,暂时无法处理请求。
3.常见的 OAuth 2.0 错误代码
除了 HTTP 状态码,OAuth 2.0 还定义了一些标准的错误代码,这些错误代码通常在 400 状态码的响应中使用:
- invalid_request
-
- 意义:请求参数无效或缺失。
- 示例:客户端发送的请求缺少
client_id
参数。注意不同参数传递方式传同一个参数也会报错。
- invalid_client
-
- 意义:客户端认证失败。
- 示例:客户端提供的
client_id
或client_secret
无效。
- invalid_grant
-
- 意义:授权码或刷新令牌无效。
- 示例:客户端提供的授权码已过期或已被使用。
- unauthorized_client
-
- 意义:客户端没有权限使用请求的授权类型。
- 示例:客户端尝试使用
authorization_code
授权类型,但未被授权使用该类型。
- unsupported_grant_type
-
- 意义:授权服务器不支持请求的授权类型。
- 示例:客户端请求使用
password
授权类型,但授权服务器不支持该类型。
- invalid_scope
-
- 意义:请求的范围无效。
- 示例:客户端请求的范围
read write
无效或未被授权。
二.持久化实现
在生产环境中,为了确保数据的持久性和可靠性,OAuth2 的实现通常会依赖于持久化存储,通常包含以下数据:
- 访问令牌(Access Token):用于访问受保护资源的令牌。
- 刷新令牌(Refresh Token):用于获取新的访问令牌,以延长访问权限的有效期。
- 客户端的标识(client_id):客户端的唯一标识符。
- 客户端密钥(client_secret):客户端的密钥,用于验证客户端身份。
- 重定向 URI:客户端指定的重定向地址,授权服务器在授权成功后将用户重定向到该地址。
- 授权码(Authorization Code):用于换取访问令牌的临时代码。
- 用户信息:如用户名、用户ID、角色等
1.代码
在 pom.xml 文件中添加以下依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--OAuth 2.0 授权服务器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<!-- MySQL 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- Alibaba Druid 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.23</version>
</dependency>
<!-- MyBatis Plus Spring Boot Starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.9</version>
</dependency>
<!-- Alibaba FastJSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.53</version>
</dependency>
<!-- Spring Boot Starter Logging (默认包含 Logback) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Lombok 用于简化日志记录代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
在 application.yml 文件中添加以下配置
spring:
application:
name: oauth2Demo
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
# allowPublicKeyRetrieval=true允许从服务器检索公钥,解决java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed问题
url: jdbc:mysql://127.0.0.1:3306/oauth2-demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: Video201@
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/*.xml
logging:
file:
# 设置日志文件的存储路径
path: logs
level:
# 设置根日志级别
root: info
# 为特定包设置日志级别
com.example: debug
为了实现 OAuth 2.0 授权服务器并将相关数据存储在数据库中,我们需要创建以下表结构。这些表用于存储授权码、访问令牌、刷新令牌、客户端信息和用户信息。
-- ----------------------------
-- Table structure for oauth2_authorization
-- ----------------------------
DROP TABLE IF EXISTS `oauth2_authorization`;
CREATE TABLE `oauth2_authorization` (
`id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`authorization_grant_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`authorized_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`attributes` blob NULL,
`state` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_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_general_ci NULL DEFAULT NULL,
`access_token_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_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,
`user_code_value` blob NULL,
`user_code_issued_at` timestamp NULL DEFAULT NULL,
`user_code_expires_at` timestamp NULL DEFAULT NULL,
`user_code_metadata` blob NULL,
`device_code_value` blob NULL,
`device_code_issued_at` timestamp NULL DEFAULT NULL,
`device_code_expires_at` timestamp NULL DEFAULT NULL,
`device_code_metadata` blob NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_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_general_ci NOT NULL,
`principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`authorities` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
PRIMARY KEY (`registered_client_id`, `principal_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for oauth2_authorized_client
-- ----------------------------
DROP TABLE IF EXISTS `oauth2_authorized_client`;
CREATE TABLE `oauth2_authorized_client` (
`client_registration_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`access_token_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`access_token_value` blob NOT NULL,
`access_token_issued_at` timestamp NOT NULL,
`access_token_expires_at` timestamp NOT NULL,
`access_token_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`refresh_token_value` blob NULL,
`refresh_token_issued_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`client_registration_id`, `principal_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_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_general_ci NOT NULL,
`client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`client_id_issued_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`client_secret` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`client_secret_expires_at` timestamp NULL DEFAULT NULL,
`client_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`client_authentication_methods` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`authorization_grant_types` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`redirect_uris` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`post_logout_redirect_uris` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`client_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`token_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint NOT NULL COMMENT '主键',
`username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名',
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码',
`status` tinyint NOT NULL COMMENT '状态',
`create_time` datetime NOT NULL COMMENT '创建时间',
`create_user` bigint NOT NULL COMMENT '创建人',
`update_time` datetime NOT NULL COMMENT '修改时间',
`update_user` bigint NOT NULL COMMENT '修改人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
授权服务器配置,包括安全配置、客户端注册、授权服务、JWT 签名和解码等。
/**
* 配置 OAuth 2.0 授权服务器,包括安全配置、客户端注册、授权服务、JWT 签名和解码等
*/
@Configuration
public class AuthorizationServerConfig {
@Autowired
private UserService userService;
/**
* Spring Security的过滤器链,用于协议端点的
*
* @param http
* @return
* @throws Exception
*/
@Bean
// 设置此过滤链的优先级为1,确保在其他过滤链之前执行
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
/**
* OAuth 2.0 授权服务器应用默认的安全配置,如下
* 1.配置授权端点的安全性:
* 授权端点(如 /oauth2/authorize)会被配置为需要身份验证。这意味着只有经过身份验证的用户才能访问这些端点。
* 令牌端点(如 /oauth2/token)会被配置为需要客户端认证。这意味着只有合法的客户端才能请求访问令牌。
* 2.配置错误处理:
* 当请求未通过身份验证或授权时,会自动重定向到登录页面或返回相应的错误响应。
* 3.配置 CORS 支持:
* 默认情况下,会启用跨域资源共享(CORS)支持,以便客户端可以从不同的域请求授权服务器的资源。
* 4.配置 CSRF 保护:
* 默认情况下,会启用跨站请求伪造(CSRF)保护,以防止恶意攻击者利用用户的会话进行未经授权的操作。
* 5.配置 JWT 支持:
* 如果启用了 JWT 支持,会自动配置 JWT 编码和解码的相关设置。
*/
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// 用户信息映射器
Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper = (context) -> {
OidcUserInfoAuthenticationToken authentication = context.getAuthentication();
JwtAuthenticationToken principal = (JwtAuthenticationToken) authentication.getPrincipal();
return new OidcUserInfo(userService.getUserInfoMap(principal.getName()));
};
// 配置OAuth 2.0授权服务器的接口
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
// 设置客户端授权中失败的handler处理
.clientAuthentication((auth) -> auth.errorResponseHandler(new Oauth2FailureHandler()))
// token相关配置,如/oauth2/token接口
.tokenEndpoint((token) -> token.errorResponseHandler(new Oauth2FailureHandler()))
// 启用 OpenID Connect 1.0 支持, 包括用户信息等
.oidc((oidc) -> {
oidc.userInfoEndpoint((userInfo) -> {
// 设置用户信息映射器,用于将认证上下文转换为用户信息
userInfo.userInfoMapper(userInfoMapper);
// 设置用户信息响应处理器,用于处理成功的用户信息请求
userInfo.userInfoResponseHandler(new Oauth2SuccessHandler());
});
});
// 当未通过身份验证时,从授权端点重定向到登录页面
http.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(
// 指定登录页面的URL
new LoginUrlAuthenticationEntryPoint("/login"),
// 匹配HTML请求
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)));
// 接受访问令牌以获取用户信息或客户端注册信息
http.oauth2ResourceServer((resourceServer) -> resourceServer
// 使用JWT进行资源服务器保护
.jwt(Customizer.withDefaults()));
// 构建并返回 SecurityFilterChain
return http.build();
}
/**
* 注册客户端
*
* @param jdbcTemplate
* @return
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
return new JdbcRegisteredClientRepository(jdbcTemplate);
}
/**
* 授权
*
* @param jdbcTemplate
* @param registeredClientRepository
* @return
*/
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
// 创建一个基于 JDBC 的 OAuth2 授权服务实例
JdbcOAuth2AuthorizationService jdbcOAuth2AuthorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
// 创建一个自定义的 OAuth2 授权行映射器
JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper authorizationRowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);
// 设置 LOB 处理器,用于处理大对象(如 BLOB 和 CLOB)
authorizationRowMapper.setLobHandler(new DefaultLobHandler());
// 创建一个 ObjectMapper 实例,用于 JSON 序列化和反序列化
ObjectMapper objectMapper = new ObjectMapper();
// 获取当前类加载器
ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
// 获取 Spring Security 提供的 Jackson 模块,这些模块包含了一些安全相关的序列化和反序列化逻辑
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
// 注册这些安全模块到 ObjectMapper 中
objectMapper.registerModules(securityModules);
// 注册 OAuth2 授权服务器的 Jackson 模块
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
// 注册自定义的 MixIn 类,用于自定义 `CustomUserDetails` 类的序列化和反序列化行为
objectMapper.addMixIn(CustomUserDetails.class, CustomUserMixin.class);
// 将配置好的 ObjectMapper 设置到授权行映射器中
authorizationRowMapper.setObjectMapper(objectMapper);
// 将自定义的授权行映射器设置到 JDBC 授权服务中
jdbcOAuth2AuthorizationService.setAuthorizationRowMapper(authorizationRowMapper);
// 返回配置好的 OAuth2 授权服务实例
return jdbcOAuth2AuthorizationService;
}
/**
* 签名访问令牌
*
* @return
*/
@Bean
public JWKSource<SecurityContext> jwkSource() {
// 生成 RSA 密钥对
KeyPair keyPair = generateRsaKey();
// 获取公钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
// 获取私钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
// 设置私钥
.privateKey(privateKey)
// 设置键 ID
.keyID(UUID.randomUUID().toString()).build();
// 创建 JWKSet
JWKSet jwkSet = new JWKSet(rsaKey);
// 返回不可变的 JWKSet
return new ImmutableJWKSet<>(jwkSet);
}
/**
* 启动时生成的带有密钥的KeyPair实例,用于创建上面的JWKSource
*
* @return
*/
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
// 获取 RSA 密钥对生成器
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
// 初始化密钥长度为 2048 位
keyPairGenerator.initialize(2048);
// 生成密钥对
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
// 如果生成密钥对失败,抛出异常
throw new IllegalStateException(ex);
}
return keyPair;
}
/**
* JwtDecoder的一个实例,用于解码已签名的访问令牌
*
* @param jwkSource
* @return
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
// 创建 JWT 解码器
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* 配置Spring Authorization Server的AuthorizationServerSettings实例
*
* @return
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
// 创建授权服务器设置
return AuthorizationServerSettings.builder().build();
}
}
Spring Security的身份认证配置
/**
* Spring Security的过滤器链,用于Spring Security的身份认证
*/
@EnableWebSecurity
/**
* 禁用代理 Bean 方法以提高性能
* 优化配置类的性能,通过减少方法调用检查的开销来提升性能。使用这个注解时,开发人员需要确保 @Bean 方法的行为是幂等的,以避免潜在的问题。
*/
@Configuration(proxyBeanMethods = false)
public class DefaultSecurityConfig {
@Bean
// 设置此过滤链的优先级为 2,确保它在授权服务器过滤链之后执行
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
// 允许访问 /actuator/**
.requestMatchers(new AntPathRequestMatcher("/actuator/**"),
// 允许访问 /oauth2/**
new AntPathRequestMatcher("/oauth2/**"),
// 允许访问所有 .json 文件
new AntPathRequestMatcher("/**/*.json"),
// 允许访问所有 .html 文件
new AntPathRequestMatcher("/**/*.html"))
// 上述路径无需身份验证
.permitAll()
// 所有其他请求都需要身份验证
.anyRequest().authenticated()
)
// 启用 CORS 支持
.cors(Customizer.withDefaults())
// 禁用 CSRF 保护
.csrf((csrf) -> csrf.disable())
// 启用表单登录,并且表单登录处理从授权服务器过滤链重定向到登录页面
.formLogin(Customizer.withDefaults())
;
// 构建并返回 SecurityFilterChain
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
处理 OAuth 2.0 认证失败情况
/**
* 处理 OAuth 2.0 认证失败情况
*/
@Component
public class Oauth2FailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
String message;
if (exception instanceof OAuth2AuthenticationException auth2AuthenticationException) {
OAuth2Error error = auth2AuthenticationException.getError();
message = "认证信息错误:" + error.getErrorCode() + error.getDescription();
} else {
message = exception.getMessage();
}
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.OK.value());
response.getWriter().write(JSONObject.toJSONString(ApiResponse.error(401, message)));
response.getWriter().flush();
}
}
处理 OAuth 2.0 认证成功的情况
/**
* 处理 OAuth 2.0 认证成功的情况
*/
@Component
public class Oauth2SuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// 将 Authentication 对象转换为 OidcUserInfoAuthenticationToken 对象
OidcUserInfoAuthenticationToken userInfoAuthenticationToken = (OidcUserInfoAuthenticationToken) authentication;
// 设置响应内容类型为 JSON,字符集为 UTF-8
response.setContentType("application/json;charset=UTF-8");
// 设置响应状态码为 200 OK
response.setStatus(HttpStatus.OK.value());
// 将 JSON 字符串写入响应体
response.getWriter().write(JSONObject.toJSONString(ApiResponse.success(userInfoAuthenticationToken.getUserInfo())));
// 刷新响应输出流,确保数据被发送
response.getWriter().flush();
}
}
处理用户身份验证和获取用户信息
/**
* 处理用户身份验证和获取用户信息
*/
@Slf4j
@Service
public class UserService implements UserDetailsService {
@Resource
private UserMapper userMapper;
/**
* 当进行身份验证时,Spring Security会调用此方法以获取用户的详细信息
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
User user = userMapper.selectOne(queryWrapper);
return CustomUserDetails.fromUserEntity(username, user.getPassword());
}
public Map<String, Object> getUserInfoMap(String username) throws UsernameNotFoundException {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
User user = userMapper.selectOne(queryWrapper);
Map<String, Object> result = new HashMap<>();
result.put("id", user.getId());
result.put("username", user.getUsername());
result.put("name", user.getName());
return result;
}
}
2.问题
1.重定向到客户端
报错:org.springframework.security.access.AccessDeniedException: Access Denied
解决方案:客户端允许匿名访问重定向接口
/**
* 启用Spring Security的安全配置
*/
@EnableWebSecurity
// 不要为配置类生成CGLIB代理,从而减少运行时开销。你需要确保在配置类中不会因为多次调用Bean方法而导致重复创建Bean实例。
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize ->
// 配置HTTP请求的授权规则
authorize
// 允许匿名访问/logged-out和/callback路径
.requestMatchers("/logged-out", "/callback").permitAll()
// 所有其他请求都需要经过身份验证
.anyRequest().authenticated()
)
// 配置注销功能,这里没有具体配置,只是启用了默认的注销功能
.logout(logout -> {
})
// 配置OAuth2客户端功能,这里没有具体配置,只是启用了默认的客户端功能
.oauth2Client(client -> {
})
// 配置OAuth2登录功能,这里没有具体配置,只是启用了默认的登录功能
.oauth2Login(login -> {
});
return http.build();
}
}
- 本文标签: Java Spring Boot
- 本文链接: https://lanzi.cyou/article/18
- 版权声明: 本文由咖啡豆原创发布,转载请遵循《署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)》许可协议授权