SpringBoot3集成Oauth2.1——6数据库存储客户端信息
1获取SQL语句
获取SQL文件
oauth2-authorization-schema.sql
oauth2-authorization-consent-schema.sql
oauth2-registered-client-schema.sql
-- 存储用户对客户端的授权决策,避免重复授权
CREATE TABLE oauth2_authorization_consent (registered_client_id VARCHAR(100) NOT NULL COMMENT '注册客户端ID',principal_name VARCHAR(200) NOT NULL COMMENT '用户身份标识',authorities VARCHAR(1000) NOT NULL COMMENT '用户授权的权限范围',PRIMARY KEY (registered_client_id, principal_name)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT='OAuth 2.1 授权同意记录表';
-- OAuth 2.1 客户端注册表:存储第三方应用的注册信息和安全配置
CREATE TABLE oauth2_registered_client (id VARCHAR(100) NOT NULL COMMENT '客户端唯一标识(内部使用)',client_id VARCHAR(100) NOT NULL COMMENT '客户端公开标识符(如应用ID)',client_id_issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '客户端ID颁发时间',client_secret VARCHAR(200) DEFAULT NULL COMMENT '客户端密钥(需加密存储)',client_secret_expires_at TIMESTAMP DEFAULT NULL COMMENT '客户端密钥过期时间',client_name VARCHAR(200) NOT NULL COMMENT '客户端显示名称',client_authentication_methods VARCHAR(1000) NOT NULL COMMENT '客户端认证方式(逗号分隔,如client_secret_basic,client_secret_post)',authorization_grant_types VARCHAR(1000) NOT NULL COMMENT '支持的授权类型(逗号分隔,如authorization_code,refresh_token)',redirect_uris VARCHAR(1000) DEFAULT NULL COMMENT '授权回调URI(多个URI使用逗号分隔)',post_logout_redirect_uris VARCHAR(1000) DEFAULT NULL COMMENT '注销后重定向URI',scopes VARCHAR(1000) NOT NULL COMMENT '客户端默认申请的权限范围',client_settings VARCHAR(2000) NOT NULL COMMENT '客户端配置(JSON格式,如require-proof-key=true)',token_settings VARCHAR(2000) NOT NULL COMMENT '令牌配置(JSON格式,如access-token-time-to-live=PT1H)',PRIMARY KEY (id)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT='OAuth 2.1 客户端注册信息表';
-- OAuth 2.1 授权记录表:存储授权码、访问令牌、刷新令牌等各类令牌的状态和元数据
CREATE TABLE oauth2_authorization (id VARCHAR(100) NOT NULL COMMENT '授权记录唯一标识',registered_client_id VARCHAR(100) NOT NULL COMMENT '关联的客户端ID',principal_name VARCHAR(200) NOT NULL COMMENT '资源所有者(用户)标识',authorization_grant_type VARCHAR(100) NOT NULL COMMENT '授权类型(authorization_code, client_credentials, refresh_token等)',authorized_scopes VARCHAR(1000) DEFAULT NULL COMMENT '授权的权限范围(逗号分隔)',attributes TEXT DEFAULT NULL COMMENT '额外属性(JSON格式)',state VARCHAR(500) DEFAULT NULL COMMENT 'OAuth2状态参数(用于防止CSRF)',-- 授权码相关字段authorization_code_value TEXT DEFAULT NULL COMMENT '授权码值(Base64编码)',authorization_code_issued_at TIMESTAMP DEFAULT NULL COMMENT '授权码颁发时间',authorization_code_expires_at TIMESTAMP DEFAULT NULL COMMENT '授权码过期时间',authorization_code_metadata TEXT DEFAULT NULL COMMENT '授权码元数据(JSON格式)',-- 访问令牌相关字段access_token_value TEXT DEFAULT NULL COMMENT '访问令牌值(Base64编码)',access_token_issued_at TIMESTAMP DEFAULT NULL COMMENT '访问令牌颁发时间',access_token_expires_at TIMESTAMP DEFAULT NULL COMMENT '访问令牌过期时间',access_token_metadata TEXT DEFAULT NULL COMMENT '访问令牌元数据(JSON格式)',access_token_type VARCHAR(100) DEFAULT NULL COMMENT '令牌类型(Bearer, MAC等)',access_token_scopes VARCHAR(1000) DEFAULT NULL COMMENT '访问令牌权限范围',-- ID令牌(OIDC)相关字段oidc_id_token_value TEXT DEFAULT NULL COMMENT 'ID令牌值(JWT格式)',oidc_id_token_issued_at TIMESTAMP DEFAULT NULL COMMENT 'ID令牌颁发时间',oidc_id_token_expires_at TIMESTAMP DEFAULT NULL COMMENT 'ID令牌过期时间',oidc_id_token_metadata TEXT DEFAULT NULL COMMENT 'ID令牌元数据(JSON格式)',-- 刷新令牌相关字段refresh_token_value TEXT DEFAULT NULL COMMENT '刷新令牌值(Base64编码)',refresh_token_issued_at TIMESTAMP DEFAULT NULL COMMENT '刷新令牌颁发时间',refresh_token_expires_at TIMESTAMP DEFAULT NULL COMMENT '刷新令牌过期时间',refresh_token_metadata TEXT DEFAULT NULL COMMENT '刷新令牌元数据(JSON格式)',-- 设备码(Device Code Flow)相关字段user_code_value TEXT DEFAULT NULL COMMENT '用户码值(用户输入的短码)',user_code_issued_at TIMESTAMP DEFAULT NULL COMMENT '用户码颁发时间',user_code_expires_at TIMESTAMP DEFAULT NULL COMMENT '用户码过期时间',user_code_metadata TEXT DEFAULT NULL COMMENT '用户码元数据(JSON格式)',device_code_value TEXT DEFAULT NULL COMMENT '设备码值(设备生成的长码)',device_code_issued_at TIMESTAMP DEFAULT NULL COMMENT '设备码颁发时间',device_code_expires_at TIMESTAMP DEFAULT NULL COMMENT '设备码过期时间',device_code_metadata TEXT DEFAULT NULL COMMENT '设备码元数据(JSON格式)',PRIMARY KEY (id)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT='OAuth 2.1 授权与令牌记录表';
2添加客户端信息
2.1SQL语句添加(不推荐)
如下所示,可以提看到,其他的一些字段,基本都是我们知道的字符串,但是其中client_settings,则很难知道填写什么。
下面是一个示例:token_settings的示例,这种配置方式是挺奇葩的,不知道那天官方开始变聪明,然后重构这的代码
{"@class": "java.util.Collections$UnmodifiableMap", "settings.token.reuse-refresh-tokens": true, "settings.token.id-token-signature-algorithm": ["org.springframework.security.oauth2.jose.jws.SignatureAlgorithm", "RS256"], "settings.token.access-token-time-to-live": ["java.time.Duration", 300], "settings.token.access-token-format": {"@class": "org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat", "value": "self-contained"}, "settings.token.refresh-token-time-to-live": ["java.time.Duration", 3600], "settings.token.authorization-code-time-to-live": ["java.time.Duration", 300], "settings.token.device-code-time-to-live": ["java.time.Duration", 300]
}
然后我们正常的Json是下面这样的。
{"settings": {"settings.token.reuse-refresh-tokens": true, "settings.token.x509-certificate-bound-access-tokens": false, "settings.token.id-token-signature-algorithm": "RS256", "settings.token.access-token-time-to-live": 1800, "settings.token.access-token-format": {"value": "self-contained"}, "settings.token.refresh-token-time-to-live": 604800, "settings.token.authorization-code-time-to-live": 300, "settings.token.device-code-time-to-live": 300}, "deviceCodeTimeToLive": 300, "reuseRefreshTokens": true, "x509CertificateBoundAccessTokens": false, "refreshTokenTimeToLive": 604800, "authorizationCodeTimeToLive": 300, "accessTokenFormat": {"value": "self-contained"}, "accessTokenTimeToLive": 1800, "idTokenSignatureAlgorithm": "RS256"
}
我尝试之下行的SQL,这个sql是不包含上述的token_settings的,最后报错:Field ‘client_settings’ doesn’t have a default value,从我们代码配置来看:这里不是简单的字符串,同理,token_settings,也是需要我们对这个client_settings、token_settings比较熟了,才可能知道,具体填写什么。
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
insert into `pcgy_gis`.`oauth2_registered_client` (`id`, `client_id`, `client_id_issued_at`, `client_secret`, `client_name`, `client_authentication_methods`, `authorization_grant_types`, `redirect_uris`, `post_logout_redirect_uris`, `scopes`)
values
('1', 'oidc-client', '2025-05-26 01:32:28', '$2a$10$.J0Rfg7y2Mu8AN8Dk2vL.eBFa9NGbOYCPOAFEw.QhgGLVXjO7eFDC', '测试客户端', 'client_secret_basic', 'authorization_code,refresh_token,client_credentials', 'http://www.baidu.com', 'http://127.0.0.1:8080/', 'openid,profile');
2.2代码添加(推荐)
比如这里,我先生成相关的代码
然后将新接口设置为不拦截:
2.2.1Json依赖
至于为什么还要单独引入jackson-datatype-jsr310,我个人觉得,在未来的某一天,说不定Spring 官方这里会被重构,毕竟,这里的代码写的确实烂(准确说Spring Oauth2的代码就写的很烂,烂到官方一直在重构代码)
<!-- fastjson --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.12</version></dependency><!-- fastjson对java8日期时间进行支持 --><dependency><groupId>com.fasterxml.jackson.datatype</groupId><artifactId>jackson-datatype-jsr310</artifactId></dependency>
2.2.2保存客户端信息
注意:clientSettings.getSettings()、tokenSettings.getSettings()、@class
如果这三个参数的Json序列化不对,保存到数据库以后,进行登录认证,会报错:
java.lang.IllegalArgumentException: Could not resolve subtype of [map type; class java.util.Map, [simple type, class java.lang.String] -> [simple type, class java.lang.Object]]: missing type id property '@class'at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 268]
/*** @description:新增OAuth 2.1 客户端注册信息表* @author hutao* @throws JsonProcessingException * @date 2025-05-26*/@Operation(summary = "1新增OAuth 2.1 客户端注册信息表")@PostMapping("/info/save")@ApiOperationSupport(order = 1)public R<String> saveOauth2RegisteredClientInfo(@RequestBody Oauth2RegisteredClient entity) throws JsonProcessingException {ObjectMapper objectMapper = new ObjectMapper();//支持 Java8日期模块 ObjectMapper 并注册 JSR-310 模块objectMapper.registerModule(new JavaTimeModule());//添加@class支持objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(),ObjectMapper.DefaultTyping.NON_FINAL,JsonTypeInfo.As.PROPERTY);//构建 clientSettingsClientSettings clientSettings = ClientSettings.builder().requireAuthorizationConsent(true).build();//构建 TokenSettingsTokenSettings tokenSettings = TokenSettings.builder().accessTokenTimeToLive(Duration.ofMinutes(30)) // 访问令牌有效期.refreshTokenTimeToLive(Duration.ofDays(7)) // 刷新令牌有效期.reuseRefreshTokens(true) // 允许复用刷新令牌.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)// ID令牌签名算法.build();entity.setClientSettings(objectMapper.writeValueAsString(clientSettings.getSettings()));entity.setTokenSettings(objectMapper.writeValueAsString(tokenSettings.getSettings()));oauth2RegisteredClientService.save(entity);return R.ok();}
2.2.3接口请求参数
{"id": "1","clientId": "oidc-client","clientSecret": "{noop}secret","clientName": "客户端测试","clientAuthenticationMethods": "client_secret_basic","authorizationGrantTypes": "authorization_code,refresh_token,client_credentials","redirectUris": "http://www.baidu.com","postLogoutRedirectUris": "http://127.0.0.1:8080/","scopes": "openid,profile","clientSettings": "","tokenSettings": ""
}
如果你的Json报错:是因为 没有对Jackson 对 Java 8 日期时间的支持
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.Duration` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"
2.2.4测试验证
通过调用接口,clientSettings、和tokenSettings写入数据库
3修改默认配置
3.1删除内存存储信息代码
3.2注入 RegisteredClientRepository
/** @Description: 客户端信息:对应表:oauth2_registered_client* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月26日 上午10:50:52*/@Beanpublic RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {return new JdbcRegisteredClientRepository(jdbcTemplate);}
3.3注入OAuth2AuthorizationService
/** @Description: 授权信息:对应表:oauth2_authorization* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月26日 上午10:51:10*/@Beanpublic OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);}
3.4注入OAuth2AuthorizationConsentService
/** @Description: 授权确认:对应表:oauth2_authorization_consent* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月26日 上午10:51:29*/@Beanpublic OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);}
4验证认证
4.1客户端认证验证
4.2授权码认证验证
http://localhost:18080/oauth2/authorize?response_type=code&client_id=oidc-client&scope=profile
4.3数据库操作验证
查看oauth2-authorization、oauth2-authorization-consen表
标题secret的默认加密
我们新增的客户端信息如下:
“clientSecret”: “{noop}secret”
仅新增时,数据库是这样的。
但是当我们进行任意的登录授权以后,此时在查看数据库中存储的信息,已经变成加密了
5完整代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.jdbc.core.JdbcTemplate;
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.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.web.context.WebApplicationContext;import com.example.demo.UrlsUtils;
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 lombok.extern.log4j.Log4j2;@Configuration
@EnableWebSecurity
@Log4j2
public class SecurityConfig {@Autowiredprivate WebApplicationContext applicationContext;@Value(value = "${ignore-url}")private String ignoreUrl;@Value(value = "${package.path}")private String packagePath;/** @Description: 配置授权服务器(用于登录操作)* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月23日 下午3:15:35*/@Bean@Order(1)public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =OAuth2AuthorizationServerConfigurer.authorizationServer();http.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()).with(authorizationServerConfigurer, (authorizationServer) ->authorizationServer.oidc(Customizer.withDefaults())).authorizeHttpRequests((authorize) ->authorize.anyRequest().authenticated()).exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login"),new MediaTypeRequestMatcher(MediaType.TEXT_HTML)));return http.build();}/** @Description: 配置需要保护的资源* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月23日 下午2:28:20*/@Bean@Order(2)public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception {//获取所有的接口地址Map<String, Object> filteredControllers = UrlsUtils.getControllerClass(packagePath, applicationContext);// 使用过滤后的控制器获取 URLList<String> allUrl = UrlsUtils.getControllerUrls(filteredControllers);String[] split = ignoreUrl.split(";");for (String url : split) {url = url.replaceAll(" ", "");UrlsUtils.removeUrl(allUrl, url);UrlsUtils.removeUrl(allUrl, url);}log.info("受保护的API地址:{}",allUrl);http.securityMatcher(allUrl.toArray(new String[0])).csrf(csrf -> csrf.disable()).authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()).oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())).formLogin(form -> form.disable());return http.build();}/** @Description: 配置不需要保护的资源* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月23日 下午2:28:20*/@Bean@Order(3)public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {String[] split = ignoreUrl.split(";");for (int i = 0; i < split.length; i++) {split[i] = split[i].replaceAll(" ", "");}log.info("公共开放的API地址:{}",Arrays.asList(split));http.csrf(csrf -> csrf.ignoringRequestMatchers(split)).authorizeHttpRequests(authorize ->authorize.requestMatchers(split).permitAll().anyRequest().authenticated()).formLogin(Customizer.withDefaults()).logout(Customizer.withDefaults());return http.build();}@Beanpublic UserDetailsService userDetailsService() {UserDetails userDetails = User.withDefaultPasswordEncoder().username("hutao").password("2025.com").roles("USER").build();return new InMemoryUserDetailsManager(userDetails);}/* @Beanpublic RegisteredClientRepository registeredClientRepository() {RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString()).clientId("oidc-client").clientSecret("{noop}secret").clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).redirectUri("http://www.baidu.com").postLogoutRedirectUri("http://127.0.0.1:8080/").scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE).clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).build();return new InMemoryRegisteredClientRepository(oidcClient);}*/@Beanpublic JWKSource<SecurityContext> jwkSource() {KeyPair keyPair = generateRsaKey();RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();RSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();JWKSet jwkSet = new JWKSet(rsaKey);return new ImmutableJWKSet<>(jwkSet);}private static KeyPair generateRsaKey() {KeyPair keyPair;try {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");keyPairGenerator.initialize(2048);keyPair = keyPairGenerator.generateKeyPair();}catch (Exception ex) {throw new IllegalStateException(ex);}return keyPair;}@Beanpublic JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);}@Beanpublic AuthorizationServerSettings authorizationServerSettings() {return AuthorizationServerSettings.builder().build();}/** @Description: 客户端信息:对应表:oauth2_registered_client* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月26日 上午10:50:52*/@Beanpublic RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {return new JdbcRegisteredClientRepository(jdbcTemplate);}/** @Description: 授权信息:对应表:oauth2_authorization* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月26日 上午10:51:10*/@Beanpublic OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);}/** @Description: 授权确认:对应表:oauth2_authorization_consent* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月26日 上午10:51:29*/@Beanpublic OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);}}