基于Spring Security 6的OAuth2 系列之七 - 授权服务器--自定义数据库客户端信息
之所以想写这一系列,是因为之前工作过程中使用Spring Security OAuth2搭建了网关和授权服务器,但当时基于spring-boot 2.3.x,其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0,结果一看Spring Security也升级为6.3.0。无论是Spring Security的风格和以及OAuth2都做了较大改动,里面甚至将授权服务器模块都移除了,导致在配置同样功能时,花费了些时间研究新版本的底层原理,这里将一些学习经验分享给大家。
注意:由于框架不同版本改造会有些使用的不同,因此本次系列中使用基本框架是 spring-boo-3.3.0(默认引入的Spring Security是6.3.0),JDK版本使用的是19,本系列OAuth2的代码采用Spring Security6.3.0框架,所有代码都在oauth2-study项目上:https://github.com/forever1986/oauth2-study.git
目录
- 1 客户端认证原理
- 2 Spring Authrization Server客户端表说明
- 3 基于数据库客户端
- 3.1 自带的Jdbc实现类
- 3.2 使用自带的Jdbc实现
- 4 基于自定义数据库客户端
前面我们自定义了授权页面,但是截止到目前为止,我们的客户端应用注册都是放在yaml文件或者在代码中加入,其实就是基于内存存储中,这样会导致每次增加客户端都要重启。在实际项目中,一般会放在数据库或者Redis缓存中,本章就将实现基于数据库的客户端应用注册。在了解如何自定义基于数据库的客户端之前,我们先来了解一下客户端认证的原理
1 客户端认证原理
我们知道Spring Authrization Server虽然从Spring Security分离出来,但是底层还是基于Spring Security的,如果读过Spring Security 6系列之二的朋友,应该很快就能掌握这一部分,因为实现的方式几乎一样。
1)看源码就是直接看过滤器,我们先看看OAuth2AuthorizationEndpointFilter,其doFilterInternal方法中就做了认证
2)从上图可以知道基于AuthenticationManager,而AuthenticationManager只是一个接口,实际的实现类ProviderManager。但是其实ProviderManager只是一个代理。ProviderManager里面有一个AuthenticationProvider数组,通过这个数据实现不同认证的。这部分都是Spring Security的内容
3)客户端信息是通过OAuth2AuthorizationCodeRequestAuthenticationProvider实现类的
4)到此,我们就知道获取客户端信息就是使用RegisteredClientRepository,我们再看看RegisteredClientRepository,返回的是客户端信息放在一个RegisteredClient类,另外RegisteredClientRepository有两个实现类
- InMemoryRegisteredClientRepository:基于内存,我们在yaml文件中配置都是基于内存,这个系列一中Spring Boot自动化配置可以找到注入原理
- JdbcRegisteredClientRepository:基于数据库,可以看到该类是基于传统的Jdbc方式实现
从原理分析,我们知道要么我们直接使用JdbcRegisteredClientRepository,要么就自定义一个RegisteredClientRepository。下面,我们先了解相关的数据库表。
2 Spring Authrization Server客户端表说明
既然要保存到数据库,那么就需要做数据库表,Spring Authrization Server已经为我们准备好了SQL,但不是一张表,而是3张表。如下图Spring Authrization Server有三张跟OAuth2流程有关的表:分别是oauth2_registered_client(客户端表)、oauth2_authorization(授权表)、oauth2_authorization_consent (授权确认表),下面说一下3张表作用和流程
- 1)首先是你需要将授权服务器中原先在yaml文件中配置的客户端信息存入oauth2_registered_client(客户端表)
- 2)当你进入授权页面时,授权服务器会往oauth2_authorization(授权表)中插入一条授权信息
- 3)当你确认授权之后,授权服务器会更新oauth2_authorization(授权表)的信息,同时往oauth2_authorization_consent (授权确认表)插入一条关联oauth2_registered_client表和oauth2_authorization表的记录,这样下次就不用再次授权
- 客户端信息表:保存客户端信息的,可以参考一下注解大概知道其字段含义
CREATE TABLE oauth2_registered_client (-- 唯一标识idid varchar(100) NOT NULL,-- 注册客户端idclient_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配置token_settings varchar(2000) NOT NULL,PRIMARY KEY (id)
);
- 授权信息表:授权信息,在跳转到授权页面时,会插入一条信息。
/*
IMPORTANT:If using PostgreSQL, update ALL columns defined with 'blob' to 'text',as PostgreSQL does not support the 'blob' data type.
*/
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)
);
- 确认授权表:授权确认后,就会将客户端表的记录与授权信息表的记录关联在一起,并记录授权情况
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)
);
这3个信息都有内存和数据库实现方式,默认都是内存方式。
3 基于数据库客户端
我们先展现以自带实现Jdbc的类的实现方式,后面实现完全自定义的方式。
3.1 自带的Jdbc实现类
从源码中,我们知道其读取的接口分别是RegisteredClientRepository、OAuth2AuthorizationService和OAuth2AuthorizationConsentService。而这几个接口分别都有内存实现和数据库实现的类,如下图以RegisteredClientRepository为例,就可以看到有这2个实现类
3.2 使用自带的Jdbc实现
1)既然Spring Security已经有其实现类,那么我们实现数据库存储只需要将默认内存换成Jdbc方式,只需要在SecurityConfig注入对应的Bean
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate){return new JdbcRegisteredClientRepository(jdbcTemplate);
}@Bean
public OAuth2AuthorizationService oAuth2AuthorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository){return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}@Bean
public OAuth2AuthorizationConsentService oAuth2AuthorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository){return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
2)需要创建对应的表,并在yaml文件中配置数据库连接即可
其默认数据库存储都是基于传统的Jdbc方式进行的,但是很多生产项目其实都会使用Mybatis等框架,下面就基于mybatis-plus重新定义这几个Jdbc。
4 基于自定义数据库客户端
代码参考lesson04子模块,该模块是一个自定义数据库客户端的授权服务器,这一章还会利用lesson02子模块的oauth-client模块作为客户端演示
lesson04 前提条件:本次演示我们先在mysql数据库创建oauth-study库,并创建表oauth2_registered_client、oauth2_authorization和oauth2_authorization_consent三个表
1)在mysql数据库创建oauth-study库,并创建表oauth2_registered_client、oauth2_authorization和oauth2_authorization_consent三个表
2)新建lesson04子模块,其pom引入如下:
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-authorization-server</artifactId></dependency><!-- lombok依赖,用于get/set的简便--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- mysql依赖,用于连接mysql数据库--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!-- mybatis-plus依赖,用于使用mybatis-plus--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId></dependency><!-- pool2和druid依赖,用于mysql连接池--><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId></dependency><!-- 解决java.time.Duration序列化问题--><dependency><groupId>com.fasterxml.jackson.datatype</groupId><artifactId>jackson-datatype-jsr310</artifactId></dependency><!-- 解决jacketjson序列化包 --><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-cas</artifactId></dependency>
</dependencies>
3)在entity包下自定义类SelfRegisteredClient、SelfOAuth2Authorization和SelfOAuth2AuthorizationConsent三个类,分别对应数据库表,之所以无法使用RegisteredClient、OAuth2Authorization和OAuth2AuthorizationConsent,是因为这些类的属性并不与数据库字段一一对应,同时有些字段序列化到数据库需要特殊处理,因此需要自定义。(注意:里面有些字段需要使用特殊TypeHandler处理,在后面会附上这些特殊定义的TypeHandler)
@TableName("oauth2_registered_client")
@Data
public class SelfRegisteredClient implements Serializable {private String id;private String clientId;private Instant clientIdIssuedAt;private String clientSecret;private Instant clientSecretExpiresAt;private String clientName;@TableField(typeHandler = SetStringTypeHandler.class)private Set<String> clientAuthenticationMethods;@TableField(typeHandler = SetStringTypeHandler.class)private Set<String> authorizationGrantTypes;@TableField(typeHandler = SetStringTypeHandler.class)private Set<String> redirectUris;@TableField(typeHandler = SetStringTypeHandler.class)private Set<String> postLogoutRedirectUris;@TableField(typeHandler = SetStringTypeHandler.class)private Set<String> scopes;@TableField(typeHandler = ClientSettingsTypeHandler.class)private ClientSettings clientSettings;@TableField(typeHandler = TokenSettingsTypeHandler.class)private TokenSettings tokenSettings;public static RegisteredClient covertRegisteredClient(SelfRegisteredClient selfClient){if(selfClient!=null){return RegisteredClient.withId(selfClient.getId()).clientId(selfClient.getClientId()).clientSecret(selfClient.getClientSecret()).clientName(selfClient.getClientName()).clientIdIssuedAt(selfClient.getClientIdIssuedAt()).clientSecretExpiresAt(selfClient.getClientSecretExpiresAt()).clientAuthenticationMethods(methods->{methods.addAll(SelfRegisteredClient.getMethodSetFromString(selfClient.getClientAuthenticationMethods()));}).authorizationGrantTypes(types->{types.addAll(SelfRegisteredClient.getSetTypeFromString(selfClient.getAuthorizationGrantTypes()));}).redirectUris(uris->{uris.addAll(selfClient.getRedirectUris());}).postLogoutRedirectUris(uris->{uris.addAll(selfClient.getPostLogoutRedirectUris());}).scopes(scopes1 ->{scopes1.addAll(selfClient.getScopes());}).tokenSettings(selfClient.getTokenSettings()).clientSettings(selfClient.getClientSettings()).build();}return null;}public static SelfRegisteredClient covertSelfRegisteredClient(RegisteredClient client){if(client!=null){SelfRegisteredClient selfRegisteredClient = new SelfRegisteredClient();selfRegisteredClient.setId(client.getId());selfRegisteredClient.setClientId(client.getClientId());selfRegisteredClient.setClientSecret(client.getClientSecret());selfRegisteredClient.setClientName(client.getClientName());selfRegisteredClient.setClientAuthenticationMethods(getSetFromMethod(client.getClientAuthenticationMethods()));selfRegisteredClient.setAuthorizationGrantTypes(getSetFromType(client.getAuthorizationGrantTypes()));selfRegisteredClient.setRedirectUris(client.getRedirectUris());selfRegisteredClient.setPostLogoutRedirectUris(client.getPostLogoutRedirectUris());selfRegisteredClient.setScopes(client.getScopes());selfRegisteredClient.setClientSettings(client.getClientSettings());selfRegisteredClient.setTokenSettings(client.getTokenSettings());selfRegisteredClient.setClientIdIssuedAt(client.getClientIdIssuedAt());selfRegisteredClient.setClientSecretExpiresAt(client.getClientSecretExpiresAt());return selfRegisteredClient;}return null;}public static Set<AuthorizationGrantType> getSetTypeFromString(Set<String> strs){Set<AuthorizationGrantType> set = new HashSet<>();if(strs!=null&& !strs.isEmpty()){// 这里只是用目前OAuth2.1支持的类型,原先的密码就不支持for(String authorizationGrantType : strs){AuthorizationGrantType type;if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) {type = AuthorizationGrantType.AUTHORIZATION_CODE;}else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(authorizationGrantType)) {type = AuthorizationGrantType.CLIENT_CREDENTIALS;}else if (AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(authorizationGrantType)) {type = AuthorizationGrantType.REFRESH_TOKEN;}else{// Custom authorization grant typetype = new AuthorizationGrantType(authorizationGrantType);}set.add(type);}}return set;}public static Set<ClientAuthenticationMethod> getMethodSetFromString(Set<String> strs){Set<ClientAuthenticationMethod> set = new HashSet<>();if(strs!=null&& !strs.isEmpty()){for(String method : strs){ClientAuthenticationMethod clientAuthenticationMethod;if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue().equals(method)) {clientAuthenticationMethod = ClientAuthenticationMethod.CLIENT_SECRET_BASIC;}else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(method)) {clientAuthenticationMethod = ClientAuthenticationMethod.CLIENT_SECRET_POST;}else if (ClientAuthenticationMethod.NONE.getValue().equals(method)) {clientAuthenticationMethod = ClientAuthenticationMethod.NONE;}else {// Custom client authentication methodclientAuthenticationMethod = new ClientAuthenticationMethod(method);}set.add(clientAuthenticationMethod);}}return set;}public static Set<String> getSetFromType(Set<AuthorizationGrantType> parameters){Set<String> set = new HashSet<>();if(parameters!=null){StringBuilder sb = new StringBuilder();for(AuthorizationGrantType parameter : parameters){set.add(parameter.getValue());}}return set;}public static Set<String> getSetFromMethod(Set<ClientAuthenticationMethod> parameters){Set<String> set = new HashSet<>();if(parameters!=null){StringBuilder sb = new StringBuilder();for(ClientAuthenticationMethod parameter : parameters){set.add(parameter.getValue());}}return set;}}
@TableName("oauth2_authorization")
@Data
public class SelfOAuth2Authorization implements Serializable {private String id;private String registeredClientId;private String principalName;private String authorizationGrantType;@TableField(typeHandler = SetStringTypeHandler.class)private Set<String> authorizedScopes;@TableField(typeHandler = TokenMetadataTypeHandler.class)private Map<String, Object> attributes;private String state;private String authorizationCodeValue;private Timestamp authorizationCodeIssuedAt;private Timestamp authorizationCodeExpiresAt;@TableField(typeHandler = TokenMetadataTypeHandler.class)private Map<String, Object> authorizationCodeMetadata;private String accessTokenValue;private Timestamp accessTokenIssuedAt;private Timestamp accessTokenExpiresAt;@TableField(typeHandler = TokenMetadataTypeHandler.class)private Map<String, Object> accessTokenMetadata;private String accessTokenType;@TableField(typeHandler = SetStringTypeHandler.class)private Set<String> accessTokenScopes;private String oidcIdTokenValue;private Timestamp oidcIdTokenIssuedAt;private Timestamp oidcIdTokenExpiresAt;@TableField(typeHandler = TokenMetadataTypeHandler.class)private Map<String, Object> oidcIdTokenMetadata;private String refreshTokenValue;private Timestamp refreshTokenIssuedAt;private Timestamp refreshTokenExpiresAt;@TableField(typeHandler = TokenMetadataTypeHandler.class)private Map<String, Object> refreshTokenMetadata;private String userCodeValue;private Timestamp userCodeIssuedAt;private Timestamp userCodeExpiresAt;@TableField(typeHandler = TokenMetadataTypeHandler.class)private Map<String, Object> userCodeMetadata;private String deviceCodeValue;private Timestamp deviceCodeIssuedAt;private Timestamp deviceCodeExpiresAt;@TableField(typeHandler = TokenMetadataTypeHandler.class)private Map<String, Object> deviceCodeMetadata;public static OAuth2Authorization covertOAuth2Authorization(SelfOAuth2Authorization selfOAuth2Authorization, RegisteredClientRepository registeredClientRepository){if(selfOAuth2Authorization!=null){RegisteredClient registeredClient = registeredClientRepository.findById(selfOAuth2Authorization.getRegisteredClientId());if (registeredClient == null) {throw new DataRetrievalFailureException("The RegisteredClient with id '" + selfOAuth2Authorization.getRegisteredClientId()+ "' was not found in the RegisteredClientRepository.");}OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(registeredClient);builder.id(selfOAuth2Authorization.getId()).principalName(selfOAuth2Authorization.getPrincipalName()).authorizationGrantType(new AuthorizationGrantType(selfOAuth2Authorization.getAuthorizationGrantType())).authorizedScopes(selfOAuth2Authorization.getAuthorizedScopes()).attributes((attrs) -> attrs.putAll(selfOAuth2Authorization.getAttributes()));String state = selfOAuth2Authorization.getState();if (StringUtils.hasText(state)) {builder.attribute(OAuth2ParameterNames.STATE, state);}Instant tokenIssuedAt;Instant tokenExpiresAt;String authorizationCodeValue = selfOAuth2Authorization.getAuthorizationCodeValue();if (StringUtils.hasText(authorizationCodeValue)) {tokenIssuedAt = selfOAuth2Authorization.getAuthorizationCodeIssuedAt().toInstant();tokenExpiresAt = selfOAuth2Authorization.getAuthorizationCodeExpiresAt().toInstant();Map<String, Object> authorizationCodeMetadata = selfOAuth2Authorization.getAuthorizationCodeMetadata();OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(authorizationCodeValue,tokenIssuedAt, tokenExpiresAt);builder.token(authorizationCode, (metadata) -> metadata.putAll(authorizationCodeMetadata));}String accessTokenValue = selfOAuth2Authorization.getAccessTokenValue();if (StringUtils.hasText(accessTokenValue)) {tokenIssuedAt = selfOAuth2Authorization.getAccessTokenIssuedAt().toInstant();tokenExpiresAt = selfOAuth2Authorization.getAccessTokenExpiresAt().toInstant();Map<String, Object> accessTokenMetadata = selfOAuth2Authorization.getAccessTokenMetadata();OAuth2AccessToken.TokenType tokenType = null;if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(selfOAuth2Authorization.getAccessTokenType())) {tokenType = OAuth2AccessToken.TokenType.BEARER;}Set<String> scopes = selfOAuth2Authorization.getAccessTokenScopes();OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenType, accessTokenValue, tokenIssuedAt,tokenExpiresAt, scopes);builder.token(accessToken, (metadata) -> metadata.putAll(accessTokenMetadata));}String oidcIdTokenValue = selfOAuth2Authorization.getOidcIdTokenValue();if (StringUtils.hasText(oidcIdTokenValue)) {tokenIssuedAt = selfOAuth2Authorization.getOidcIdTokenIssuedAt().toInstant();tokenExpiresAt = selfOAuth2Authorization.getOidcIdTokenExpiresAt().toInstant();Map<String, Object> oidcTokenMetadata = selfOAuth2Authorization.getOidcIdTokenMetadata();OidcIdToken oidcToken = new OidcIdToken(oidcIdTokenValue, tokenIssuedAt, tokenExpiresAt,(Map<String, Object>) oidcTokenMetadata.get(OAuth2Authorization.Token.CLAIMS_METADATA_NAME));builder.token(oidcToken, (metadata) -> metadata.putAll(oidcTokenMetadata));}String refreshTokenValue = selfOAuth2Authorization.getRefreshTokenValue();if (StringUtils.hasText(refreshTokenValue)) {tokenIssuedAt = selfOAuth2Authorization.getRefreshTokenIssuedAt().toInstant();tokenExpiresAt = null;Timestamp refreshTokenExpiresAt = selfOAuth2Authorization.getRefreshTokenExpiresAt();if (refreshTokenExpiresAt != null) {tokenExpiresAt = refreshTokenExpiresAt.toInstant();}Map<String, Object> refreshTokenMetadata = selfOAuth2Authorization.getRefreshTokenMetadata();OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(refreshTokenValue, tokenIssuedAt,tokenExpiresAt);builder.token(refreshToken, (metadata) -> metadata.putAll(refreshTokenMetadata));}String userCodeValue = selfOAuth2Authorization.getUserCodeValue();if (StringUtils.hasText(userCodeValue)) {tokenIssuedAt = selfOAuth2Authorization.getUserCodeIssuedAt().toInstant();tokenExpiresAt = selfOAuth2Authorization.getUserCodeExpiresAt().toInstant();Map<String, Object> userCodeMetadata = selfOAuth2Authorization.getUserCodeMetadata();OAuth2UserCode userCode = new OAuth2UserCode(userCodeValue, tokenIssuedAt, tokenExpiresAt);builder.token(userCode, (metadata) -> metadata.putAll(userCodeMetadata));}String deviceCodeValue = selfOAuth2Authorization.getDeviceCodeValue();if (StringUtils.hasText(deviceCodeValue)) {tokenIssuedAt = selfOAuth2Authorization.getDeviceCodeIssuedAt().toInstant();tokenExpiresAt = selfOAuth2Authorization.getDeviceCodeExpiresAt().toInstant();Map<String, Object> deviceCodeMetadata = selfOAuth2Authorization.getDeviceCodeMetadata();OAuth2DeviceCode deviceCode = new OAuth2DeviceCode(deviceCodeValue, tokenIssuedAt, tokenExpiresAt);builder.token(deviceCode, (metadata) -> metadata.putAll(deviceCodeMetadata));}return builder.build();}return null;}public static SelfOAuth2Authorization covertSelfOAuth2Authorization(OAuth2Authorization auth2Authorization){if(auth2Authorization!=null){SelfOAuth2Authorization selfOAuth2Authorization = new SelfOAuth2Authorization();selfOAuth2Authorization.setId(auth2Authorization.getId());selfOAuth2Authorization.setRegisteredClientId(auth2Authorization.getRegisteredClientId());selfOAuth2Authorization.setPrincipalName(auth2Authorization.getPrincipalName());selfOAuth2Authorization.setAuthorizationGrantType(auth2Authorization.getAuthorizationGrantType().getValue());selfOAuth2Authorization.setAuthorizedScopes(auth2Authorization.getAuthorizedScopes());selfOAuth2Authorization.setAttributes(auth2Authorization.getAttributes());String state = null;String authorizationState = auth2Authorization.getAttribute(OAuth2ParameterNames.STATE);if (StringUtils.hasText(authorizationState)) {state = authorizationState;}selfOAuth2Authorization.setState(state==null?"":state);OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = auth2Authorization.getToken(OAuth2AuthorizationCode.class);if(authorizationCode!=null){selfOAuth2Authorization.setAuthorizationCodeValue(authorizationCode.getToken().getTokenValue());selfOAuth2Authorization.setAuthorizationCodeIssuedAt(new Timestamp(authorizationCode.getToken().getIssuedAt().getEpochSecond()*1000));selfOAuth2Authorization.setAuthorizationCodeExpiresAt(new Timestamp(authorizationCode.getToken().getExpiresAt().getEpochSecond()*1000));selfOAuth2Authorization.setAuthorizationCodeMetadata(authorizationCode.getMetadata());}OAuth2Authorization.Token<OAuth2AccessToken> accessToken = auth2Authorization.getToken(OAuth2AccessToken.class);if (accessToken != null) {selfOAuth2Authorization.setAccessTokenValue(accessToken.getToken().getTokenValue());selfOAuth2Authorization.setAccessTokenIssuedAt(new Timestamp(accessToken.getToken().getIssuedAt().getEpochSecond()*1000));selfOAuth2Authorization.setAccessTokenExpiresAt(new Timestamp(accessToken.getToken().getExpiresAt().getEpochSecond()*1000));selfOAuth2Authorization.setAccessTokenMetadata(accessToken.getMetadata());selfOAuth2Authorization.setAccessTokenType(accessToken.getToken().getTokenType().getValue());selfOAuth2Authorization.setAccessTokenScopes(accessToken.getToken().getScopes());}OAuth2Authorization.Token<OidcIdToken> oidcIdToken = auth2Authorization.getToken(OidcIdToken.class);if (oidcIdToken != null) {selfOAuth2Authorization.setOidcIdTokenValue(oidcIdToken.getToken().getTokenValue());selfOAuth2Authorization.setOidcIdTokenIssuedAt(new Timestamp(oidcIdToken.getToken().getIssuedAt().getEpochSecond()*1000));selfOAuth2Authorization.setOidcIdTokenExpiresAt(new Timestamp(oidcIdToken.getToken().getExpiresAt().getEpochSecond()*1000));selfOAuth2Authorization.setOidcIdTokenMetadata(oidcIdToken.getMetadata());}OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken = auth2Authorization.getRefreshToken();if (refreshToken != null) {selfOAuth2Authorization.setRefreshTokenValue(refreshToken.getToken().getTokenValue());selfOAuth2Authorization.setRefreshTokenIssuedAt(new Timestamp(refreshToken.getToken().getIssuedAt().getEpochSecond()*1000));selfOAuth2Authorization.setRefreshTokenExpiresAt(new Timestamp(refreshToken.getToken().getExpiresAt().getEpochSecond()*1000));selfOAuth2Authorization.setRefreshTokenMetadata(refreshToken.getMetadata());}OAuth2Authorization.Token<OAuth2UserCode> userCode = auth2Authorization.getToken(OAuth2UserCode.class);if (userCode != null) {selfOAuth2Authorization.setUserCodeValue(userCode.getToken().getTokenValue());selfOAuth2Authorization.setUserCodeIssuedAt(new Timestamp(userCode.getToken().getIssuedAt().getEpochSecond()*1000));selfOAuth2Authorization.setUserCodeExpiresAt(new Timestamp(userCode.getToken().getExpiresAt().getEpochSecond()*1000));selfOAuth2Authorization.setUserCodeMetadata(userCode.getMetadata());}OAuth2Authorization.Token<OAuth2DeviceCode> deviceCode = auth2Authorization.getToken(OAuth2DeviceCode.class);if (deviceCode != null) {selfOAuth2Authorization.setDeviceCodeValue(deviceCode.getToken().getTokenValue());selfOAuth2Authorization.setDeviceCodeIssuedAt(new Timestamp(deviceCode.getToken().getIssuedAt().getEpochSecond()*1000));selfOAuth2Authorization.setDeviceCodeExpiresAt(new Timestamp(deviceCode.getToken().getExpiresAt().getEpochSecond()*1000));selfOAuth2Authorization.setDeviceCodeMetadata(deviceCode.getMetadata());}return selfOAuth2Authorization;}return null;}
}
@TableName("oauth2_authorization_consent")
@Data
public class SelfOAuth2AuthorizationConsent implements Serializable {private String registeredClientId;private String principalName;@TableField(typeHandler = SetStringTypeHandler.class)private Set<String> authorities;public static SelfOAuth2AuthorizationConsent convertSelfOAuth2AuthorizationConsent(OAuth2AuthorizationConsent auth2AuthorizationConsent){if(auth2AuthorizationConsent!=null){SelfOAuth2AuthorizationConsent selfOAuth2AuthorizationConsent = new SelfOAuth2AuthorizationConsent();selfOAuth2AuthorizationConsent.setRegisteredClientId(auth2AuthorizationConsent.getRegisteredClientId());selfOAuth2AuthorizationConsent.setPrincipalName(auth2AuthorizationConsent.getPrincipalName());if(auth2AuthorizationConsent.getAuthorities()!=null){selfOAuth2AuthorizationConsent.setAuthorities(auth2AuthorizationConsent.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()));}return selfOAuth2AuthorizationConsent;}return null;}public static OAuth2AuthorizationConsent convertOAuth2AuthorizationConsent(SelfOAuth2AuthorizationConsent selfOAuth2AuthorizationConsent, RegisteredClientRepository registeredClientRepository){if(selfOAuth2AuthorizationConsent!=null){RegisteredClient registeredClient = registeredClientRepository.findById(selfOAuth2AuthorizationConsent.getRegisteredClientId());if (registeredClient == null) {throw new DataRetrievalFailureException("The RegisteredClient with id '" + selfOAuth2AuthorizationConsent.getRegisteredClientId()+ "' was not found in the RegisteredClientRepository.");}OAuth2AuthorizationConsent.Builder builder = OAuth2AuthorizationConsent.withId(selfOAuth2AuthorizationConsent.getRegisteredClientId(),selfOAuth2AuthorizationConsent.getPrincipalName());for (String authority : selfOAuth2AuthorizationConsent.getAuthorities()) {builder.authority(new SimpleGrantedAuthority(authority));}return builder.build();}return null;}
}
4)在mapper包下,自定义Mapper读取oauth2_registered_client 表
@Mapper
public interface Oauth2RegisteredClientMapper extends BaseMapper<SelfRegisteredClient> {// 根据client_id,查询客户端信息@Select("select * from oauth2_registered_client where client_id = #{client_id}")SelfRegisteredClient selectByClientId(String client_id);
}
@Mapper
public interface OAuth2AuthorizationMapper extends BaseMapper<SelfOAuth2Authorization> {
}
@Mapper
public interface OAuth2AuthorizationConsentMapper extends BaseMapper<SelfOAuth2AuthorizationConsent> {
}
5)在handler包下,自定义某些字段存储到库的TypeHandler。这是由于三个表中有几个字段需要特殊存储,因此需要自定义TypeHandler
@MappedJdbcTypes({JdbcType.VARCHAR}) //对应数据库类型
@MappedTypes({ClientSettings.class}) //java数据类型
public class ClientSettingsTypeHandler implements TypeHandler<ClientSettings> {private ObjectMapper objectMapper;public ClientSettingsTypeHandler() {objectMapper = new ObjectMapper();/*** 此处注册json存储格式化*/ClassLoader classLoader = ClientSettingsTypeHandler.class.getClassLoader();List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);this.objectMapper.registerModules(securityModules);this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());}@Overridepublic void setParameter(PreparedStatement ps, int i, ClientSettings parameter, JdbcType jdbcType) throws SQLException {if(parameter!=null&¶meter.getSettings()!=null){ps.setString(i ,writeMap(parameter.getSettings()));}else{ps.setString(i, "");}}@Overridepublic ClientSettings getResult(ResultSet rs, String columnName) throws SQLException {String str = rs.getString(columnName);return ClientSettings.withSettings(parseMap(str)).build();}@Overridepublic ClientSettings getResult(ResultSet rs, int columnIndex) throws SQLException {String str = rs.getString(columnIndex);return ClientSettings.withSettings(parseMap(str)).build();}@Overridepublic ClientSettings getResult(CallableStatement cs, int columnIndex) throws SQLException {String str = cs.getString(columnIndex);return ClientSettings.withSettings(parseMap(str)).build();}private String writeMap(Map<String, Object> data) {try {return this.objectMapper.writeValueAsString(data);}catch (Exception ex) {throw new IllegalArgumentException(ex.getMessage(), ex);}}private Map<String, Object> parseMap(String data) {if(data!=null&&!data.isEmpty()){try {return this.objectMapper.readValue(data, new TypeReference<Map<String, Object>>() {});}catch (Exception ex) {throw new IllegalArgumentException(ex.getMessage(), ex);}}else{return new HashMap<>();}}
}
@MappedJdbcTypes({JdbcType.VARCHAR}) //对应数据库类型
@MappedTypes({Set.class}) //java数据类型
public class SetStringTypeHandler implements TypeHandler<Set<String>> {private static final String COMMA =",";@Overridepublic void setParameter(PreparedStatement ps, int i, Set<String> parameters, JdbcType jdbcType) throws SQLException {String str = "";if(parameters!=null){str = String.join(COMMA, parameters);}ps.setString(i, str);}@Overridepublic Set<String> getResult(ResultSet rs, String columnName) throws SQLException {String str = rs.getString(columnName);return getSetFromString(str);}@Overridepublic Set<String> getResult(ResultSet rs, int columnIndex) throws SQLException {String str = rs.getString(columnIndex);return getSetFromString(str);}@Overridepublic Set<String> getResult(CallableStatement cs, int columnIndex) throws SQLException {String str = cs.getString(columnIndex);return getSetFromString(str);}private Set<String> getSetFromString(String str){Set<String> set = new HashSet<>();if(str!=null&& !str.isEmpty()){String[] strs = str.split(COMMA);Collections.addAll(set, strs);}return set;}
}
@MappedJdbcTypes({JdbcType.BLOB}) //对应数据库类型
@MappedTypes({Map.class})
public class TokenMetadataTypeHandler implements TypeHandler<Map<String, Object>> {private ObjectMapper objectMapper;public TokenMetadataTypeHandler() {objectMapper = new ObjectMapper();/*** 此处注册json存储格式化*/ClassLoader classLoader = TokenMetadataTypeHandler.class.getClassLoader();List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);this.objectMapper.registerModules(securityModules);this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());}@Overridepublic void setParameter(PreparedStatement ps, int i, Map<String, Object> parameter, JdbcType jdbcType) throws SQLException {if(parameter!=null){ps.setString(i ,writeMap(parameter));}else{ps.setString(i, "");}}@Overridepublic Map<String, Object> getResult(ResultSet rs, String columnName) throws SQLException {String str = rs.getString(columnName);return parseMap(str);}@Overridepublic Map<String, Object> getResult(ResultSet rs, int columnIndex) throws SQLException {String str = rs.getString(columnIndex);return parseMap(str);}@Overridepublic Map<String, Object> getResult(CallableStatement cs, int columnIndex) throws SQLException {String str = cs.getString(columnIndex);return parseMap(str);}private String writeMap(Map<String, Object> data) {try {this.objectMapper.findAndRegisterModules();return this.objectMapper.writeValueAsString(data);}catch (Exception ex) {throw new IllegalArgumentException(ex.getMessage(), ex);}}private Map<String, Object> parseMap(String data) {if(data!=null&&!data.isEmpty()){try {return this.objectMapper.readValue(data, new TypeReference<Map<String, Object>>() {});}catch (Exception ex) {throw new IllegalArgumentException(ex.getMessage(), ex);}}else{return new HashMap<>();}}
}
@MappedJdbcTypes({JdbcType.VARCHAR}) //对应数据库类型
@MappedTypes({TokenSettings.class}) //java数据类型
public class TokenSettingsTypeHandler implements TypeHandler<TokenSettings> {private ObjectMapper objectMapper;public TokenSettingsTypeHandler() {objectMapper = new ObjectMapper();/*** 此处注册json存储格式化*/ClassLoader classLoader = TokenSettingsTypeHandler.class.getClassLoader();List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);this.objectMapper.registerModules(securityModules);this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());}@Overridepublic void setParameter(PreparedStatement ps, int i, TokenSettings parameter, JdbcType jdbcType) throws SQLException {if(parameter!=null&¶meter.getSettings()!=null){ps.setString(i ,writeMap(parameter.getSettings()));}else{ps.setString(i, "");}}@Overridepublic TokenSettings getResult(ResultSet rs, String columnName) throws SQLException {String str = rs.getString(columnName);return TokenSettings.withSettings(parseMap(str)).build();}@Overridepublic TokenSettings getResult(ResultSet rs, int columnIndex) throws SQLException {String str = rs.getString(columnIndex);return TokenSettings.withSettings(parseMap(str)).build();}@Overridepublic TokenSettings getResult(CallableStatement cs, int columnIndex) throws SQLException {String str = cs.getString(columnIndex);return TokenSettings.withSettings(parseMap(str)).build();}private String writeMap(Map<String, Object> data) {try {this.objectMapper.findAndRegisterModules();return this.objectMapper.writeValueAsString(data);}catch (Exception ex) {throw new IllegalArgumentException(ex.getMessage(), ex);}}private Map<String, Object> parseMap(String data) {if(data!=null&&!data.isEmpty()){try {Map<String, Object> map = this.objectMapper.readValue(data, new TypeReference<Map<String, Object>>() {});return map;}catch (Exception ex) {throw new IllegalArgumentException(ex.getMessage(), ex);}}else{return new HashMap<>();}}
}
6)在repository包下,自定义SelfJdbcRegisteredClientRepository、SelfJdbcOAuth2AuthorizationService和SeltJdbcOAuth2AuthorizationConsentService
@Repository
public class SelfJdbcRegisteredClientRepository implements RegisteredClientRepository {@AutowiredOauth2RegisteredClientMapper mapper;@Overridepublic void save(RegisteredClient registeredClient) {Assert.notNull(registeredClient, "registeredClient cannot be null");SelfRegisteredClient existingRegisteredClient = this.mapper.selectById(registeredClient.getId());if (existingRegisteredClient != null) {this.mapper.updateById(SelfRegisteredClient.covertSelfRegisteredClient(registeredClient));}else {this.mapper.insert(SelfRegisteredClient.covertSelfRegisteredClient(registeredClient));}}@Overridepublic RegisteredClient findById(String id) {return SelfRegisteredClient.covertRegisteredClient(this.mapper.selectById(id));}@Overridepublic RegisteredClient findByClientId(String clientId) {return SelfRegisteredClient.covertRegisteredClient(this.mapper.selectByClientId(clientId));}private void updateRegisteredClient(RegisteredClient registeredClient) {this.mapper.updateById(SelfRegisteredClient.covertSelfRegisteredClient(registeredClient));}}
@Service
public class SelfJdbcOAuth2AuthorizationService implements OAuth2AuthorizationService {@Autowiredprivate RegisteredClientRepository registeredClientRepository;@Autowiredprivate OAuth2AuthorizationMapper oAuth2AuthorizationMapper;@Overridepublic void save(OAuth2Authorization authorization) {Assert.notNull(authorization, "authorization cannot be null");OAuth2Authorization existingAuthorization = findById(authorization.getId());if (existingAuthorization == null) {oAuth2AuthorizationMapper.insert(SelfOAuth2Authorization.covertSelfOAuth2Authorization(authorization));}else {oAuth2AuthorizationMapper.updateById(SelfOAuth2Authorization.covertSelfOAuth2Authorization(authorization));}}@Overridepublic void remove(OAuth2Authorization authorization) {oAuth2AuthorizationMapper.deleteById(SelfOAuth2Authorization.covertSelfOAuth2Authorization(authorization));}@Overridepublic OAuth2Authorization findById(String id) {SelfOAuth2Authorization selfOAuth2Authorization = oAuth2AuthorizationMapper.selectById(id);return SelfOAuth2Authorization.covertOAuth2Authorization(selfOAuth2Authorization, registeredClientRepository);}@Overridepublic OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType) {Assert.hasText(token, "token cannot be empty");List<SqlParameterValue> parameters = new ArrayList<>();List<SelfOAuth2Authorization> result = null;Map<String, Object> map = new HashMap<>();if (tokenType == null) {map.put("state", token);byte[] tokenBytes = token.getBytes(StandardCharsets.UTF_8);map.put("authorization_code_value", tokenBytes);map.put("access_token_value", tokenBytes);map.put("oidc_id_token_value", tokenBytes);map.put("refresh_token_value", tokenBytes);map.put("user_code_value", tokenBytes);map.put("device_code_value", tokenBytes);result = oAuth2AuthorizationMapper.selectByMap(map);}else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {map.put("state", token);result = oAuth2AuthorizationMapper.selectByMap(map);}else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {map.put("authorization_code_value", token.getBytes(StandardCharsets.UTF_8));result = oAuth2AuthorizationMapper.selectByMap(map);}else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {map.put("access_token_value", token.getBytes(StandardCharsets.UTF_8));result = oAuth2AuthorizationMapper.selectByMap(map);}else if (OidcParameterNames.ID_TOKEN.equals(tokenType.getValue())) {map.put("oidc_id_token_value", token.getBytes(StandardCharsets.UTF_8));result = oAuth2AuthorizationMapper.selectByMap(map);}else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {map.put("refresh_token_value", token.getBytes(StandardCharsets.UTF_8));result = oAuth2AuthorizationMapper.selectByMap(map);}else if (OAuth2ParameterNames.USER_CODE.equals(tokenType.getValue())) {map.put("user_code_value", token.getBytes(StandardCharsets.UTF_8));result = oAuth2AuthorizationMapper.selectByMap(map);}else if (OAuth2ParameterNames.DEVICE_CODE.equals(tokenType.getValue())) {map.put("device_code_value", token.getBytes(StandardCharsets.UTF_8));result = oAuth2AuthorizationMapper.selectByMap(map);}return result!=null&&!result.isEmpty()?SelfOAuth2Authorization.covertOAuth2Authorization(result.get(0),registeredClientRepository):null;}
}
@Service
public class SeltJdbcOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {@Autowiredprivate OAuth2AuthorizationConsentMapper auth2AuthorizationConsentMapper;@Autowiredprivate RegisteredClientRepository registeredClientRepository;@Overridepublic void save(OAuth2AuthorizationConsent authorizationConsent) {Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");OAuth2AuthorizationConsent existingAuthorizationConsent = findById(authorizationConsent.getRegisteredClientId(),authorizationConsent.getPrincipalName());if (existingAuthorizationConsent == null) {auth2AuthorizationConsentMapper.insert(SelfOAuth2AuthorizationConsent.convertSelfOAuth2AuthorizationConsent(authorizationConsent));}else {auth2AuthorizationConsentMapper.updateById(SelfOAuth2AuthorizationConsent.convertSelfOAuth2AuthorizationConsent(authorizationConsent));}}@Overridepublic void remove(OAuth2AuthorizationConsent authorizationConsent) {auth2AuthorizationConsentMapper.deleteById(SelfOAuth2AuthorizationConsent.convertSelfOAuth2AuthorizationConsent(authorizationConsent));}@Overridepublic OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {Map<String, Object> map = new HashMap<>();map.put("registered_client_id", registeredClientId);map.put("principal_name", principalName);List<SelfOAuth2AuthorizationConsent> list = auth2AuthorizationConsentMapper.selectByMap(map);return list==null||list.isEmpty()?null:SelfOAuth2AuthorizationConsent.convertOAuth2AuthorizationConsent(list.get(0), registeredClientRepository);}}
7)在config包下,配置SecurityConfig,这个和lesson03子模块很像,去除自定义授权页定义即可
@Configuration
public class SecurityConfig {// 自定义授权服务器的Filter链@Bean@Order(Ordered.HIGHEST_PRECEDENCE)SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)// oidc配置.oidc(withDefaults());// 资源服务器默认jwt配置http.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults()));// 异常处理http.exceptionHandling((exceptions) -> exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")));return http.build();}// 自定义Spring Security的链路。如果自定义授权服务器的Filter链,则原先自动化配置将会失效,因此也要配置Spring Security@Bean@Order(SecurityProperties.BASIC_AUTH_ORDER)SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorize) -> authorize.requestMatchers("/demo", "/test").permitAll().anyRequest().authenticated()).formLogin(withDefaults());return http.build();}}
8)在resources包下,配置化application.yml文件(注意:要在mybatis-plus配置下注册ypeHandler)
server:port: 9000logging:level:org.springframework.security: tracespring:security:# 使用security配置授权服务器的登录用户和密码user:name: userpassword: 1234# 配置数据源datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/oauth_study?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=trueusername: rootpassword: rootdruid:initial-size: 5min-idle: 5maxActive: 20maxWait: 3000timeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000validationQuery: select 'x'testWhileIdle: truetestOnBorrow: falsetestOnReturn: falsepoolPreparedStatements: falsefilters: stat,wall,slf4jconnectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;socketTimeout=10000;connectTimeout=1200# mybatis-plus的配置
mybatis-plus:global-config:banner: falsemapper-locations: classpath:mappers/*.xmltype-aliases-package: com.demo.lesson04.entity# 将handler包下的TypeHandler注册进去type-handlers-package: com.demo.lesson04.handlerconfiguration:cache-enabled: falselocal-cache-scope: statement
9)在controller包下,定义InsertController,通过接口方式注册一个和lesson03一样信息的客户端
@RestController
public class InsertController{@AutowiredOauth2RegisteredClientMapper oauth2RegisteredClientMapper;@GetMapping("/insert")public void insert(){SelfRegisteredClient client = new SelfRegisteredClient();client.setId(UUID.randomUUID().toString());client.setClientId("oidc-client");client.setClientSecret("{noop}secret");client.setClientName("oidc-client");Set<ClientAuthenticationMethod> methodSet = new HashSet<>();client.setClientAuthenticationMethods(new HashSet<>(Collections.singleton(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())));Set<String> typeSet = new HashSet<>();typeSet.add(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());typeSet.add(AuthorizationGrantType.REFRESH_TOKEN.getValue());client.setAuthorizationGrantTypes(typeSet);client.setRedirectUris(new HashSet<>(Collections.singleton("http://localhost:8080/login/oauth2/code/oidc-client")));client.setPostLogoutRedirectUris(new HashSet<>(Collections.singleton("http://localhost:8080/")));Set<String> scopeSet = new HashSet<>();scopeSet.add(OidcScopes.OPENID);scopeSet.add(OidcScopes.PROFILE);client.setScopes(scopeSet);client.setClientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build());client.setTokenSettings(TokenSettings.builder().build());oauth2RegisteredClientMapper.insert(client);System.out.println(client);}
}
10)新建启动类Oauth2Lesson04Application,并启动
@SpringBootApplication
public class Oauth2Lesson04Application {public static void main(String[] args) {SpringApplication.run(Oauth2Lesson04Application.class, args);}
}
11)启动lesson02子模块的oauth-client子模块,作为演示客户端
12)插入客户端,访问http://localhost:9000/insert 插入一个客户端,其实就是把原先在yaml配置的客户端插入到数据库。我们会看到数据库表oauth_study.oauth2_registered_client插入一条记录
13)测试,访问:http://localhost:8080/demo 你就可以看到与《系列之四 - 客户端–oauth2-client底层原理》一样的流程,只不过其客户端信息是在数据库中(你可以一步一步操作,看看数据库三张表数据的变化)。
结语:在本章及之前章节,我们对Spring Security实现OAuth2做了一个基本了解,也通过自定义授权页面、自定义数据库客户端信息等窥探了Spring Authrization Server,下一章,我们将会讲述Spring Authrization Server的关键原理代码以及一些关键的Filter,这样对我们后面做更高级的配置有一个很好的了解。
相关文章:
基于Spring Security 6的OAuth2 系列之七 - 授权服务器--自定义数据库客户端信息
之所以想写这一系列,是因为之前工作过程中使用Spring Security OAuth2搭建了网关和授权服务器,但当时基于spring-boot 2.3.x,其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0,结果一看Spring Security也升级…...
当WebGIS遇到智慧文旅-以长沙市不绕路旅游攻略为例
目录 前言 一、旅游数据组织 1、旅游景点信息 2、路线时间推荐 二、WebGIS可视化实现 1、态势标绘实现 2、相关位置展示 三、成果展示 1、第一天旅游路线 2、第二天旅游路线 3、第三天旅游路线 4、交通、订票、住宿指南 四、总结 前言 随着信息技术的飞速发展&…...
浅析CDN安全策略防范
CDN(内容分发网络)信息安全策略是保障内容分发网络在提供高效服务的同时,确保数据传输安全、防止恶意攻击和保护用户隐私的重要手段。以下从多个方面详细介绍CDN的信息安全策略: 1. 数据加密 数据加密是CDN信息安全策略的核心之…...
Python安居客二手小区数据爬取(2025年)
目录 2025年安居客二手小区数据爬取观察目标网页观察详情页数据准备工作:安装装备就像打游戏代码详解:每行代码都是你的小兵完整代码大放送爬取结果 2025年安居客二手小区数据爬取 这段时间需要爬取安居客二手小区数据,看了一下相关教程基本…...
Python爬虫获取custom-1688自定义API操作接口
一、引言 在电子商务领域,1688作为国内领先的B2B平台,提供了丰富的API接口,允许开发者获取商品信息、店铺信息等。其中,custom接口允许开发者进行自定义操作,获取特定的数据。本文将详细介绍如何使用Python调用1688的…...
CAPL与外部接口
CAPL与外部接口 目录 CAPL与外部接口1. 引言2. CAPL与C/C++交互2.1 CAPL与C/C++交互简介2.2 CAPL与C/C++交互实现3. CAPL与Python交互3.1 CAPL与Python交互简介3.2 CAPL与Python交互实现4. CAPL与MATLAB交互4.1 CAPL与MATLAB交互简介4.2 CAPL与MATLAB交互实现5. 案例说明5.1 案…...
解析与使用 Apache HttpClient 进行网络请求和数据抓取
目录 1. 什么是 HttpClient? 2. 基本使用 3. 使用 HttpClient 爬取腾讯天气的数据 4. 爬取拉勾招聘网站的职位信息 5. 总结 前言 Apache HttpClient 是 Apache 提供的一个用于处理 HTTP 请求和响应的工具类库。它提供了一种便捷、功能强大的方式来发送 HTTP 请…...
【go语言】结构体
一、type 关键字的用法 在 go 语言中,type 关键字用于定义新的类型,他可以用来定义基础类型、结构体类型、接口类型、函数类型等。通过 type 关键字,我们可以为现有类型创建新的类型别名或者自定义新的类型。 1.1 类型别名 使用 type 可以为…...
Kotlin 委托详解
Kotlin 委托详解 引言 Kotlin 作为一种现代化的编程语言,在 Android 开发等领域得到了广泛的应用。在 Kotlin 中,委托(Delegation)是一种强大的特性,它可以让我们以更简洁的方式实现代码的复用和扩展。本文将详细解析…...
用QT做一个网络调试助手
文章目录 前言一、TCP网络调试助手介绍1. 项目概述2. 开发流程3. TCP服务器的关键流程4. TCP客户端的关键流程 二、实现UI界面1. 服务器界面2. 客户端界面 三、实现代码框架1. 服务器代码1.1 初始化服务器地址1.2 开始监听1.3 与客户端连接1.4 接收客户端信息1.5 判断客户端状态…...
Qt 5.14.2 学习记录 —— 이십이 QSS
文章目录 1、概念2、基本语法3、给控件应用QSS设置4、选择器1、子控件选择器2、伪类选择器 5、样式属性box model 6、实例7、登录界面 1、概念 参考了CSS,都是对界面的样式进行设置,不过功能不如CSS强大。 可通过QSS设置样式,也可通过C代码…...
HTML 符号详解
HTML 符号详解 引言 HTML(超文本标记语言)符号是HTML文档中用来表示特殊字符的标记。这些符号在日常网页设计和开发中扮演着重要角色,特别是在需要显示版权、商标、货币符号等特殊字符时。本文将详细介绍HTML符号的用法、类型以及如何在HTML文档中插入这些符号。 HTML符号…...
第十二章 I 开头的术语
文章目录 第十二章 I 开头的术语以 I 开头的术语被识别 (identified by)识别关系 (identifying relationship)身份 (identity)idkey隐式全局引用 (implicit global reference)隐含命名空间 (implied namespace)包含文件 (include file)传入锁 (incoming lock) 索引 (index)索引…...
用XAMPP安装PHP环境(Window系统)
视频教程 BV1jA411v791 进入XAMPP官网 Download XAMPP 找到最新版本,64位的下载,一路安装,语言只有英语德语两个(不会德语) 安装好以后启动软件,点Apache,MySql,start 在C:\xampp\…...
02.01 生产者消费者
请使用条件变量实现2生产者2消费者模型,注意1个生产者在生产的时候,另外一个生产者不能生产。 1>程序代码 #include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h>…...
区块链项目孵化与包装设计:从概念到市场的全流程指南
区块链技术的快速发展催生了大量创新项目,但如何将一个区块链项目从概念孵化成市场认可的产品,是许多团队面临的挑战。本文将从孵化策略、包装设计和市场落地三个维度,为你解析区块链项目成功的关键步骤。 一、区块链项目孵化的核心要素 明确…...
Redis|前言
文章目录 什么是 Redis?Redis 主流功能与应用 什么是 Redis? Redis,Remote Dictionary Server(远程字典服务器)。Redis 是完全开源的,使用 ANSIC 语言编写,遵守 BSD 协议,是一个高性…...
电脑优化大师-解决电脑卡顿问题
我们常常会遇到电脑运行缓慢、网速卡顿的情况,但又不知道是哪个程序在占用过多资源。这时候,一款能够实时监控网络和系统状态的工具就显得尤为重要了。今天,就来给大家介绍一款小巧实用的监控工具「TrafficMonitor」。 「TrafficMonitor 」是…...
Linux篇——权限
在生活中我们知道,一个人能够从事的工作或任务,不是取决于你是谁,而是取决于你的身份是什么,就比如同一个人,如果他是校长,那就可以说放假就放假,如果是学生,就没有这个决定的权力。…...
Python 梯度下降法(六):Nadam Optimize
文章目录 Python 梯度下降法(六):Nadam Optimize一、数学原理1.1 介绍1.2 符号定义1.3 实现流程 二、代码实现2.1 函数代码2.2 总代码 三、优缺点3.1 优点3.2 缺点 四、相关链接 Python 梯度下降法(六):Nad…...
大模型培训讲师老师叶梓分享:DeepSeek多模态大模型janus初探
以下视频内容为叶梓分享DeepSeek多模态大模型janus的部署,并验证其实际效果,包括图生文和文生图两部分。 叶梓老师人工智能培训分享DeepSeek多模态大模型janus初探 DeepSeek 的多模态大模型 Janus 是一款强大的 AI 模型,专注于图像和文本的多…...
2025最新源支付V7全套开源版+Mac云端+五合一云端
2025最新源支付V7全套开源版Mac云端五合一云端 官方1999元, 最新非网上那种功能不全带BUG开源版,可以自己增加授权或二开 拥有卓越的性能和丰富的功能。它采用全新轻量化的界面UI,让您能更方便快捷地解决知识付费和运营赞助的难题 它基于…...
Linux系统上安装与配置 MySQL( CentOS 7 )
目录 1. 下载并安装 MySQL 官方 Yum Repository 2. 启动 MySQL 并查看运行状态 3. 找到 root 用户的初始密码 4. 修改 root 用户密码 5. 设置允许远程登录 6. 在云服务器配置 MySQL 端口 7. 关闭防火墙 8. 解决密码错误的问题 前言 在 Linux 服务器上安装并配置 MySQL …...
计算机网络——流量控制
流量控制的基本方法是确保发送方不会以超过接收方处理能力的速度发送数据包。 通常的做法是接收方会向发送方提供某种反馈,如: (1)停止&等待 在任何时候只有一个数据包在传输,发送方发送一个数据包,…...
[Java基础]开发工具Idea
安装工具 IDE: 称为集成开发环境, 把代码编写,编译,执行等功能综合在一起的工具 卸载 控制面板->卸载程序->卸载->勾选清空配置->确认卸载 下载/安装 官网下载: IntelliJ IDEA – the Leading Java and Kotlin IDE 默认安装: 旗舰版安装无需任何勾选, 傻瓜安装…...
Java线程池
专栏系列文章地址:https://blog.csdn.net/qq_26437925/article/details/145290162 本文目标: 理解线程池运行原理 线程的各种属性参数关闭问题异常处理拒绝策略常见的线程池 可以分析下自身工作中用的各种线程池和参数设定 工作中用到的有 普通的 Th…...
2025年01月27日Github流行趋势
项目名称:onlook项目地址url:https://github.com/onlook-dev/onlook项目语言:TypeScript历史star数:5340今日star数:211项目维护者:Kitenite, drfarrell, iNerdStack, abhiroopc84, apps/dependabot项目简介…...
C# 数组和列表的基本知识及 LINQ 查询
数组和列表的基本知识及 LINQ 查询 一、基本知识二、引用命名空间声明三、数组3.1、一维数组3.2、二维数组3.3、不规则数组 Jagged Array 四、列表 List4.1、一维列表4.2、二维列表 五、数组和列表使用 LINQ的操作和运算5.1、一维 LIST 删除所有含 double.NaN 的行5.2、一维 LI…...
Deepseek本地部署(ollama+open-webui)
ollama 首先是安装ollama,这个非常简单 https://ollama.com/ 下载安装即可 open-webui 这个是为了提供一个ui,毕竟我们也不想在cmd和模型交互,很不方便。 第一,需要安装python3.11,必须是3.11(其他版…...
(七)Spring Cloud Alibaba 2023.x:RocketMQ 消息队列配置与实现
目录 前言 准备 安装RocketMq服务 下载rocketmq服务 下载rocketmq 控制台 项目集成 引入依赖 生产者服务配置 消费者服务配置 发送队列消息 前言 在微服务架构中,异步消息通信是实现系统解耦、提高性能和增强系统可靠性的重要手段。在 Spring Cloud Alib…...
2848、与车相交的点
2848、[简单] 与车相交的点 1、题目描述 给你一个下标从 0 开始的二维整数数组 nums 表示汽车停放在数轴上的坐标。对于任意下标 i,nums[i] [starti, endi] ,其中 starti 是第 i 辆车的起点,endi 是第 i 辆车的终点。 返回数轴上被车 任意…...
51单片机开发:温度传感器
温度传感器DS18B20: 初始化时序图如下图所示: u8 ds18b20_init(void){ds18b20_reset();return ds18b20_check(); }void ds18b20_reset(void){DS18B20_PORT 0;delay_10us(75);DS18B20_PORT 1;delay_10us(2); }u8 ds18b20_check(void){u8 time_temp0;wh…...
三甲医院大型生信服务器多配置方案剖析与应用(2024版)
一、引言 1.1 研究背景与意义 在当今数智化时代,生物信息学作为一门融合生物学、计算机科学和信息技术的交叉学科,在三甲医院的科研和临床应用中占据着举足轻重的地位。随着高通量测序技术、医学影像技术等的飞速发展,生物医学数据呈爆发式…...
【机器学习】自定义数据集 ,使用朴素贝叶斯对其进行分类
一、贝叶斯原理 贝叶斯算法是基于贝叶斯公式的,其公式为: 其中叫做先验概率,叫做条件概率,叫做观察概率,叫做后验概率,也是我们求解的结果,通过比较后验概率的大小,将后验概率最大的…...
ASP.NET Core 启动并提供静态文件
ASP.NET Core 启动并提供静态文件 即是单个可执行文件,它既运行 API 项目,也托管 前端项目(通常是前端的发布文件)。 这种方式一般是通过将 前端项目 的发布文件(例如 HTML、CSS、JavaScript)放入 Web AP…...
MySQL 导入数据
MySQL 导入数据 引言 MySQL 是一款广泛使用的开源关系型数据库管理系统,它以其稳定性和高效性被广泛应用于各种规模的应用程序中。在数据库管理过程中,数据的导入是至关重要的一个环节。本文将详细介绍如何在 MySQL 中导入数据,包括导入数据的准备、操作步骤以及注意事项。…...
MINIRAG: TOWARDS EXTREMELY SIMPLE RETRIEVAL-AUGMENTED GENERATION论文翻译
感谢阅读 注意不含评估以后的翻译原论文地址标题以及摘要介绍部分MiniRAG 框架2.1 HETEROGENEOUS GRAPH INDEXING WITH SMALL LANGUAGE MODELS2.2 LIGHTWEIGHT GRAPH-BASED KNOWLEDGE RETRIEVAL2.2.1 QUERY SEMANTIC MAPPING2.2.2 TOPOLOGY-ENHANCED GRAPH RETRIEVAL 注意不含评…...
将 OneLake 数据索引到 Elasticsearch - 第二部分
作者:来自 Elastic Gustavo Llermaly 及 Jeffrey Rengifo 本文分为两部分,第二部分介绍如何使用自定义连接器将 OneLake 数据索引并搜索到 Elastic 中。 在本文中,我们将利用第 1 部分中学到的知识来创建 OneLake 自定义 Elasticsearch 连接器…...
数据密码解锁之DeepSeek 和其他 AI 大模型对比的神秘面纱
本篇将揭露DeepSeek 和其他 AI 大模型差异所在。 目录 编辑 一本篇背景: 二性能对比: 2.1训练效率: 2.2推理速度: 三语言理解与生成能力对比: 3.1语言理解: 3.2语言生成: 四本篇小结…...
安心即美的生活方式
如果你的心是安定的,那么,外界也就安静了。就像陶渊明说的:心远地自偏。不是走到偏远无人的边荒才能得到片刻清净,不需要使用洪荒之力去挣脱生活的枷锁,这是陶渊明式的中国知识分子的雅量。如果你自己是好的男人或女人…...
基于深度学习的输电线路缺陷检测算法研究(论文+源码)
输电线路关键部件的缺陷检测对于电网安全运行至关重要,传统方法存在效率低、准确性不高等问题。本研究探讨了利用深度学习技术进行输电线路关键组件的缺陷检测,目的是提升检测的效率与准确度。选用了YOLOv8模型作为基础,并通过加入CA注意力机…...
手写防抖函数、手写节流函数
文章目录 1 手写防抖函数2 手写节流函数 1 手写防抖函数 函数防抖是指在事件被触发n秒后再执行回调,如果在这n秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。 function debou…...
UE 导入sbsar插件
Substance 3D 插件支持直接在 Unreal Engine 5 和 Unreal Engine 4 中使用 Substance 材质。无论您是在处理游戏、可视化,还是在移动设备、桌面或 XR 上进行部署,Substance 都能提供独特的体验,并优化功能以提高生产力。 Substance 资源平台…...
pytorch实现简单的情感分析算法
人工智能例子汇总:AI常见的算法和例子-CSDN博客 在PyTorch中实现中文情感分析算法通常涉及以下几个步骤:数据预处理、模型定义、训练和评估。下面是一个简单的实现示例,使用LSTM模型进行中文情感分析。 1. 数据预处理 首先,我…...
Baklib揭示内容中台实施最佳实践的策略与实战经验
内容概要 在当前数字化转型的浪潮中,内容中台的概念日益受到关注。它不再仅仅是一个内容管理系统,而是企业提升运营效率与灵活应对市场变化的重要支撑平台。内容中台的实施离不开最佳实践的指导,这些实践为企业在建设高效内容中台时提供了宝…...
11.[前端开发]Day11-HTML+CSS阶段练习(仿小米和考拉页面)
一、小米穿戴设备(浮动) 完整代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"vie…...
设计模式学习(二)
结构型 适配器模式 定义 它允许将一个类的接口转换成客户端期望的另一个接口。适配器模式通常用于使不兼容的接口能够一起工作。 适配器模式的角色 目标接口(Target):客户端期望的接口。适配者(Adaptee)ÿ…...
【Docker】快速部署 Nacos 注册中心
【Docker】快速部署 Nacos 注册中心 引言 Nacos 注册中心是一个用于服务发现和配置管理的开源项目。提供了动态服务发现、服务健康检查、动态配置管理和服务管理等功能,帮助开发者更轻松地构建微服务架构。 仓库地址 https://github.com/alibaba/nacos 步骤 拉取…...
大白话讲清楚embedding原理
Embedding(嵌入)是一种将高维数据(如单词、句子、图像等)映射到低维连续向量的技术,其核心目的是通过向量表示捕捉数据之间的语义或特征关系。以下从原理、方法和应用三个方面详细解释Embedding的工作原理。 一、Embe…...
pandas中的apply方法使用
apply 用于对 DataFrame 或 Series 中的数据进行逐行或逐列的操作。它可以接受一个函数(通常是 lambda 函数或自定义函数),并将该函数应用到每一行或每一列上。apply语法: DataFrame.apply(func, axis0, rawFalse, result_typeNo…...