• 基于Spring Security 6的OAuth2 系列之七 - 授权服务器--自定义数据库客户端信息

基于Spring Security 6的OAuth2 系列之七 - 授权服务器--自定义数据库客户端信息

2025-04-24 21:00:36 0 阅读

之所以想写这一系列,是因为之前工作过程中使用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 (
	-- 唯一标识id
    id varchar(100) NOT NULL,
    -- 注册客户端id
    client_id varchar(100) NOT NULL,
    -- 注册客户端签发时间(默认是当前时间)
    client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
    -- 注册客户端密钥
    client_secret varchar(200) DEFAULT NULL,
    -- 注册客户端过期时间
    client_secret_expires_at timestamp DEFAULT NULL,
    -- 注册客户端名称
    client_name varchar(200) NOT NULL,
    -- 注册客户端验证方法
    client_authentication_methods varchar(1000) NOT NULL,
    -- 注册客户端授权模式:授权码模式、客户端模式等
    authorization_grant_types varchar(1000) NOT NULL,
    -- 注册客户端回调地址
    redirect_uris varchar(1000) DEFAULT NULL,
    -- 注册客户端首页
    post_logout_redirect_uris varchar(1000) DEFAULT NULL,
    -- 注册客户端授权范围
    scopes varchar(1000) NOT NULL,
    -- 注册客户端配置
    client_settings varchar(2000) NOT NULL,
    -- token配置
    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实现类

从源码中,我们知道其读取的接口分别是RegisteredClientRepositoryOAuth2AuthorizationServiceOAuth2AuthorizationConsentService。而这几个接口分别都有内存实现和数据库实现的类,如下图以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.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.securitygroupId>
        <artifactId>spring-security-oauth2-authorization-serverartifactId>
    dependency>
    
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
    dependency>
    
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
    dependency>
    
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plus-spring-boot3-starterartifactId>
    dependency>
    
    <dependency>
        <groupId>org.apache.commonsgroupId>
        <artifactId>commons-pool2artifactId>
    dependency>
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>druid-spring-boot-starterartifactId>
    dependency>
    
    <dependency>
        <groupId>com.fasterxml.jackson.datatypegroupId>
        <artifactId>jackson-datatype-jsr310artifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.securitygroupId>
        <artifactId>spring-security-casartifactId>
    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 type
                    type = 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 method
                    clientAuthenticationMethod = 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());
    }

    @Override
    public void setParameter(PreparedStatement ps, int i, ClientSettings parameter, JdbcType jdbcType) throws SQLException {
        if(parameter!=null&&parameter.getSettings()!=null){
            ps.setString(i ,writeMap(parameter.getSettings()));
        }else{
            ps.setString(i, "");
        }

    }

    @Override
    public ClientSettings getResult(ResultSet rs, String columnName) throws SQLException {
        String str = rs.getString(columnName);
        return ClientSettings.withSettings(parseMap(str)).build();
    }

    @Override
    public ClientSettings getResult(ResultSet rs, int columnIndex) throws SQLException {
        String str = rs.getString(columnIndex);
        return ClientSettings.withSettings(parseMap(str)).build();
    }

    @Override
    public 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 =",";

    @Override
    public 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);
    }

    @Override
    public Set<String> getResult(ResultSet rs, String columnName) throws SQLException {
        String str = rs.getString(columnName);
        return getSetFromString(str);
    }

    @Override
    public Set<String> getResult(ResultSet rs, int columnIndex) throws SQLException {
        String str = rs.getString(columnIndex);
        return getSetFromString(str);
    }

    @Override
    public 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());
    }

    @Override
    public 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, "");
        }
    }

    @Override
    public Map<String, Object> getResult(ResultSet rs, String columnName) throws SQLException {
        String str = rs.getString(columnName);
        return parseMap(str);
    }

    @Override
    public Map<String, Object> getResult(ResultSet rs, int columnIndex) throws SQLException {
        String str = rs.getString(columnIndex);
        return parseMap(str);
    }

    @Override
    public 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());
    }

    @Override
    public void setParameter(PreparedStatement ps, int i, TokenSettings parameter, JdbcType jdbcType) throws SQLException {
        if(parameter!=null&&parameter.getSettings()!=null){
            ps.setString(i ,writeMap(parameter.getSettings()));
        }else{
            ps.setString(i, "");
        }

    }

    @Override
    public TokenSettings getResult(ResultSet rs, String columnName) throws SQLException {
        String str = rs.getString(columnName);
        return TokenSettings.withSettings(parseMap(str)).build();
    }

    @Override
    public TokenSettings getResult(ResultSet rs, int columnIndex) throws SQLException {
        String str = rs.getString(columnIndex);
        return TokenSettings.withSettings(parseMap(str)).build();
    }

    @Override
    public 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 {

    @Autowired
    Oauth2RegisteredClientMapper mapper;

    @Override
    public 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));
        }

    }

    @Override
    public RegisteredClient findById(String id) {
        return SelfRegisteredClient.covertRegisteredClient(this.mapper.selectById(id));
    }

    @Override
    public 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 {

    @Autowired
    private RegisteredClientRepository registeredClientRepository;

    @Autowired
    private OAuth2AuthorizationMapper oAuth2AuthorizationMapper;

    @Override
    public 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));
        }
    }

    @Override
    public void remove(OAuth2Authorization authorization) {
        oAuth2AuthorizationMapper.deleteById(SelfOAuth2Authorization.covertSelfOAuth2Authorization(authorization));
    }

    @Override
    public OAuth2Authorization findById(String id) {
        SelfOAuth2Authorization selfOAuth2Authorization = oAuth2AuthorizationMapper.selectById(id);
        return SelfOAuth2Authorization.covertOAuth2Authorization(selfOAuth2Authorization, registeredClientRepository);
    }

    @Override
    public 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 {

    @Autowired
    private OAuth2AuthorizationConsentMapper auth2AuthorizationConsentMapper;

    @Autowired
    private RegisteredClientRepository registeredClientRepository;

    @Override
    public 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));
        }
    }

    @Override
    public void remove(OAuth2AuthorizationConsent authorizationConsent) {
        auth2AuthorizationConsentMapper.deleteById(SelfOAuth2AuthorizationConsent.convertSelfOAuth2AuthorizationConsent(authorizationConsent));
    }

    @Override
    public 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: 9000

logging:
  level:
    org.springframework.security: trace

spring:
  security:
    # 使用security配置授权服务器的登录用户和密码
    user:
      name: user
      password: 1234

  # 配置数据源
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/oauth_study?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: root
    druid:
      initial-size: 5
      min-idle: 5
      maxActive: 20
      maxWait: 3000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: select 'x'
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: false
      filters: stat,wall,slf4j
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;socketTimeout=10000;connectTimeout=1200

# mybatis-plus的配置
mybatis-plus:
  global-config:
    banner: false
  mapper-locations: classpath:mappers/*.xml
  type-aliases-package: com.demo.lesson04.entity
  # 将handler包下的TypeHandler注册进去
  type-handlers-package: com.demo.lesson04.handler
  configuration:
    cache-enabled: false
    local-cache-scope: statement

9)在controller包下,定义InsertController,通过接口方式注册一个和lesson03一样信息的客户端

@RestController
public class InsertController{

    @Autowired
    Oauth2RegisteredClientMapper 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,这样对我们后面做更高级的配置有一个很好的了解。

本文地址:https://www.vps345.com/533.html

搜索文章

Tags

PV计算 带宽计算 流量带宽 服务器带宽 上行带宽 上行速率 什么是上行带宽? CC攻击 攻击怎么办 流量攻击 DDOS攻击 服务器被攻击怎么办 源IP 服务器 linux 运维 游戏 云计算 进程 操作系统 进程控制 Ubuntu ssh deepseek Ollama 模型联网 API CherryStudio javascript 前端 chrome edge python MCP 数据库 centos oracle 关系型 安全 分布式 llama 算法 opencv 自然语言处理 神经网络 语言模型 react.js 前端面试题 node.js 持续部署 macos adb Dell R750XS 科技 ai java 人工智能 个人开发 harmonyos 华为 开发语言 typescript 计算机网络 ubuntu numpy rust http 网络 nginx 监控 自动化运维 阿里云 网络安全 网络协议 django fastapi flask web3.py 笔记 C 环境变量 进程地址空间 flutter golang 后端 IIS .net core Hosting Bundle .NET Framework vs2022 udp unity 经验分享 ollama llm 深度学习 机器学习 chatgpt 大模型 llama3 Chatglm 开源大模型 php android zotero WebDAV 同步失败 代理模式 面试 性能优化 jdk intellij-idea 架构 pycharm 串口服务器 vue.js audio vue音乐播放器 vue播放音频文件 Audio音频播放器自定义样式 播放暂停进度条音量调节快进快退 自定义audio覆盖默认样式 YOLO pytorch sql KingBase mcp mcp-proxy mcp-inspector fastapi-mcp agent sse 银河麒麟 kylin v10 麒麟 v10 spring boot websocket tomcat ESP32 LDAP ide nuxt3 vue3 docker 实时音视频 filezilla 无法连接服务器 连接被服务器拒绝 vsftpd 331/530 c++ c语言 统信 国产操作系统 虚拟机安装 .net gitlab 多线程服务器 Linux网络编程 自动化 蓝耘科技 元生代平台工作流 ComfyUI conda pillow spring json html5 firefox kubernetes 容器 学习方法 程序人生 windows 搜索引擎 DeepSeek-R1 API接口 github 创意 社区 Flask FastAPI Waitress Gunicorn uWSGI Uvicorn RTSP xop RTP RTSPServer 推流 视频 kvm 无桌面 命令行 Hyper-V WinRM TrustedHosts matlab YOLOv8 NPU Atlas800 A300I pro asi_bench tcp/ip Deepseek ecm bpm 游戏服务器 Minecraft 电脑 web安全 安全架构 redis 课程设计 大数据 cursor MCP server C/S LLM windows日志 c# 媒体 agi AIGC 计算机视觉 ansible playbook gpu算力 H3C 命名管道 客户端与服务端通信 mysql virtualenv arm debian 硬件架构 系统架构 uni-app 云原生 服务器无法访问 ip地址无法访问 无法访问宝塔面板 宝塔面板打不开 Reactor 设计模式 C++ es jvm 目标检测 华为云 物联网 springsecurity6 oauth2 授权服务器 前后端分离 openEuler vscode AI编程 jar html FunASR ASR 佛山戴尔服务器维修 佛山三水服务器维修 go file server http server web server ssl https android studio 交互 集成学习 集成测试 Docker Compose docker compose docker-compose 代码调试 ipdb 远程连接 rdp 实验 远程工作 负载均衡 消息队列 TRAE Docker Hub docker pull 镜像源 daemon.json Linux vim UOS 统信操作系统 yum oceanbase rc.local 开机自启 systemd 麒麟 重启 排查 系统重启 日志 原因 kylin 深度优先 图论 并集查找 换根法 树上倍增 ddos AI MNN DeepSeek Qwen jenkins ci/cd shell 备份SQL Server数据库 数据库备份 傲梅企业备份网络版 远程桌面 gaussdb ruoyi DeepSeek行业应用 Heroku 网站部署 xss pppoe radius hugo microsoft arm开发 AI agent 思科模拟器 思科 Cisco Linux PID java-ee kind react next.js 部署 部署next.js JAVA IDEA Java 报错 医疗APP开发 app开发 机器人 googlecloud X11 Xming bash 小程序 开源 spring cloud idm stm32 qt EMQX MQTT 通信协议 运维开发 弹性计算 虚拟化 KVM 计算虚拟化 弹性裸金属 漏洞 学习 宝塔面板 同步 备份 建站 安全威胁分析 vscode 1.86 SSH 豆瓣 追剧助手 迅雷 nas 微信 内存 unity3d 安装教程 GPU环境配置 Ubuntu22 CUDA PyTorch Anaconda安装 eureka aws fpga开发 服务器繁忙 备选 网站 api 调用 示例 银河麒麟桌面操作系统 Kylin OS 国产化 postman mock mock server 模拟服务器 mock服务器 Postman内置变量 Postman随机数据 prometheus 监控k8s集群 集群内prometheus 向日葵 git elasticsearch IIS服务器 IIS性能 日志监控 maven intellij idea 腾讯云 mongodb mosquitto 智能路由器 外网访问 内网穿透 端口映射 r语言 数据挖掘 数据可视化 数据分析 word图片自动上传 word一键转存 复制word图片 复制word图文 复制word公式 粘贴word图文 粘贴word公式 华为od sqlite dubbo TCP服务器 qt项目 qt项目实战 qt教程 openssl 密码学 模拟退火算法 国标28181 视频监控 监控接入 语音广播 流程 SIP SDP 编辑器 根服务器 kafka hibernate sqlserver ukui 麒麟kylinos openeuler 微服务 游戏程序 AI大模型 大模型入门 大模型教程 webrtc ffmpeg 音视频 remote-ssh 需求分析 规格说明书 嵌入式硬件 单片机 温湿度数据上传到服务器 Arduino HTTP springboot Nuxt.js ollama下载加速 apache 孤岛惊魂4 恒源云 tcp excel big data gitee express okhttp CORS 跨域 雨云 NPS 博客 oneapi 大模型微调 open webui 爬虫 Headless Linux pdf asp.net大文件上传 asp.net大文件上传下载 asp.net大文件上传源码 ASP.NET断点续传 asp.net上传文件夹 asp.net上传大文件 .net core断点续传 华为认证 网络工程师 交换机 远程登录 telnet Dify visualstudio av1 电视盒子 机顶盒ROM 魔百盒刷机 zookeeper 3d 数学建模 网络结构图 k8s live555 rtsp rtp 驱动开发 硬件工程 嵌入式实习 WSL win11 无法解析服务器的名称或地址 Samba SWAT 配置文件 服务管理 网络共享 v10 软件 armbian u-boot Cline 企业微信 Linux24.04 deepin minio ecmascript nextjs reactjs 流式接口 URL Agent ftp Cursor ssrf 失效的访问控制 HTML audio 控件组件 vue3 audio音乐播放器 Audio标签自定义样式默认 vue3播放音频文件音效音乐 自定义audio播放器样式 播放暂停调整声音大小下载文件 MI300x openwrt ux 多线程 LLM Web APP Streamlit hadoop 其他 opensearch helm xrdp string模拟实现 深拷贝 浅拷贝 经典的string类问题 三个swap 开发环境 SSL证书 Python 网络编程 聊天服务器 套接字 TCP 客户端 Socket svn 源码剖析 rtsp实现步骤 流媒体开发 数据集 pygame 小游戏 五子棋 ios odoo 服务器动作 Server action 能力提升 面试宝典 技术 IT信息化 银河麒麟操作系统 rpc 远程过程调用 Windows环境 直播推流 rsyslog 微信公众平台 C语言 僵尸进程 ipython FTP服务器 联想开天P90Z装win10 DigitalOcean GPU服务器购买 GPU服务器哪里有 GPU服务器 jmeter 软件测试 缓存 统信UOS bonding 链路聚合 GaN HEMT 氮化镓 单粒子烧毁 辐射损伤 辐照效应 压力测试 mount挂载磁盘 wrong fs type LVM挂载磁盘 Centos7.9 flash-attention C++软件实战问题排查经验分享 0xfeeefeee 0xcdcdcdcd 动态库加载失败 程序启动失败 程序运行权限 标准用户权限与管理员权限 权限 命令 skynet MySql DOIT 四博智联 边缘计算 mac 防火墙 NAT转发 NAT Server 1024程序员节 Unity Dedicated Server Host Client 无头主机 stm32项目 ip wsl 技能大赛 网络攻击模型 embedding epoll k8s资源监控 annotations自动化 自动化监控 监控service 监控jvm GCC Linux环境 wsgiref Web 服务器网关接口 程序 测试工具 list 数据结构 常用命令 文本命令 目录命令 鸿蒙 计算机 移动魔百盒 thingsboard postgresql unix USB转串口 CH340 .netcore 飞牛NAS 飞牛OS MacBook Pro 端口测试 harmonyOS面试题 iDRAC R720xd LORA 大语言模型 NLP ardunio BLE 邮件APP 免费软件 can 线程池 Kali 虚拟机 Ubuntu Server Ubuntu 22.04.5 npm 嵌入式 linux驱动开发 iventoy VmWare OpenEuler css3 XFS xfs文件系统损坏 I_O error ssh远程登录 自动驾驶 磁盘监控 iot 游戏引擎 linux上传下载 jupyter 健康医疗 互联网医院 dell服务器 zabbix gitea rabbitmq Jellyfin vmware 卡死 服务器主板 AI芯片 信息与通信 NFS deepseek r1 系统安全 浏览器开发 AI浏览器 服务器配置 生物信息学 我的世界服务器搭建 nac 802.1 portal 前端框架 devops 王者荣耀 asm yaml Ultralytics 可视化 Wi-Fi make命令 makefile文件 超融合 Spring Security 我的世界 我的世界联机 数码 iphone etl tidb GLIBC ISO镜像作为本地源 rocketmq 鸿蒙系统 uv 文件系统 路径解析 云电竞 云电脑 todesk VMware安装mocOS VMware macOS系统安装 安卓 云服务 HarmonyOS Next wireshark BMC IPMI 带外管理 软链接 硬链接 视觉检测 职场和发展 硬件 设备 GPU PCI-Express 镜像 WebUI DeepSeek V3 微信小程序 db ros jetty undertow dify sqlite3 ceph CrewAI log4j Docker引擎已经停止 Docker无法使用 WSL进度一直是0 镜像加速地址 ruby Erlang OTP gen_server 热代码交换 事务语义 ui 流水线 脚本式流水线 efficientVIT YOLOv8替换主干网络 TOLOv8 navicat gcc jina 无人机 ROS cnn DenseNet 产测工具框架 IMX6ULL 管理框架 mamba Vmamba 深度求索 私域 知识库 visual studio code dns是什么 如何设置电脑dns dns应该如何设置 Linux的基础指令 less 宝塔面板访问不了 宝塔面板网站访问不了 宝塔面板怎么配置网站能访问 宝塔面板配置ip访问 宝塔面板配置域名访问教程 宝塔面板配置教程 pip 计算机外设 信号处理 freebsd composer MacOS录屏软件 Xinference RAGFlow glibc 远程控制 rustdesk WSL2 IM即时通讯 QQ 剪切板对通 HTML FORMAT VMware安装Ubuntu Ubuntu安装k8s 测试用例 功能测试 frp AI写作 AI作画 串口驱动 CH341 uart 485 聊天室 腾讯云大模型知识引擎 ocr 银河麒麟服务器操作系统 系统激活 KylinV10 麒麟操作系统 Vmware k8s集群资源管理 云原生开发 c 算力 RAGFLOW Radius 编程 性能分析 muduo 个人博客 camera Arduino 电子信息 bcompare Beyond Compare 模拟器 教程 数据库架构 数据管理 数据治理 数据编织 数据虚拟化 鲲鹏 昇腾 npu 图形化界面 windwos防火墙 defender防火墙 win防火墙白名单 防火墙白名单效果 防火墙只允许指定应用上网 防火墙允许指定上网其它禁止 高效远程协作 TrustViewer体验 跨设备操作便利 智能远程控制 Ubuntu 24 常用命令 Ubuntu 24 Ubuntu vi 异常处理 烟花代码 烟花 元旦 实时互动 aarch64 编译安装 HPC 智能手机 EMUI 回退 降级 升级 junit selenium SEO 自动化测试 性能测试 n8n dity make 显示管理器 lightdm gdm 中间件 可信计算技术 阻塞队列 生产者消费者模型 服务器崩坏原因 树莓派 VNC 工作流 workflow laravel Linux无人智慧超市 LInux多线程服务器 QT项目 LInux项目 单片机项目 vue css grafana 直流充电桩 充电桩 NAS Termux HarmonyOS IPv4 子网掩码 公网IP 私有IP apt SSH 密钥生成 SSH 公钥 私钥 生成 中兴光猫 换光猫 网络桥接 自己换光猫 p2p netty 低代码 ArkUI 多端开发 智慧分发 应用生态 鸿蒙OS sentinel Xterminal 单元测试 HTTP 服务器控制 ESP32 DeepSeek iperf3 带宽测试 firewall AD域 致远OA OA服务器 服务器磁盘扩容 ShenTong 游戏机 抗锯齿 Netty 即时通信 NIO 技术共享 线程 黑客 vasp安装 智能硬件 查询数据库服务IP地址 SQL Server 程序员 语音识别 AutoDL 加解密 Yakit yaklang 换源 国内源 Debian HCIE 数通 gpt rclone AList webdav fnOS IMX317 MIPI H265 VCU tensorflow code-server SVN Server tortoise svn crosstool-ng wordpress 无法访问wordpess后台 打开网站页面错乱 linux宝塔面板 wordpress更换服务器 SysBench 基准测试 流量运营 MS Materials 业界资讯 matplotlib wsl2 elk eclipse 大数据平台 虚拟局域网 历史版本 下载 安装 银河麒麟高级服务器 外接硬盘 Kylin echarts 信息可视化 网页设计 gradle selete 高级IO 华为机试 多层架构 解耦 transformer proxy模式 AISphereButler HAProxy deekseek ragflow safari Mac 系统 AI-native Docker Desktop kamailio sip VoIP tcpdump 图像处理 nvidia ESXi Dell HPE 联想 浪潮 rust腐蚀 框架搭建 微信分享 Image wxopensdk 显卡驱动 回显服务器 UDP的API使用 vSphere vCenter Java Applet URL操作 服务器建立 Socket编程 网络文件读取 llama.cpp 实战案例 序列化反序列化 .net mvc断点续传 Ubuntu DeepSeek DeepSeek Ubuntu DeepSeek 本地部署 DeepSeek 知识库 DeepSeek 私有化知识库 本地部署 DeepSeek DeepSeek 私有化部署 iBMC UltraISO g++ g++13 web 信号 CVE-2024-7347 云服务器 VPS gateway 策略模式 单例模式 模拟实现 宠物 毕业设计 免费学习 宠物领养 宠物平台 web3 小艺 Pura X autodl 软件定义数据中心 sddc RTMP 应用层 反向代理 虚幻 双系统 fd 文件描述符 状态模式 矩阵 飞书 Windows IMM Qwen2.5-coder 离线部署 工业4.0 QT 5.12.12 QT开发环境 Ubuntu18.04 prompt GRUB引导 Linux技巧 cocoapods xcode docker搭建nacos详解 docker部署nacos docker安装nacos 腾讯云搭建nacos centos7搭建nacos threejs 3D 传统数据库升级 银行 LLMs rtsp服务器 rtsp server android rtsp服务 安卓rtsp服务器 移动端rtsp服务 大牛直播SDK springboot远程调试 java项目远程debug docker远程debug java项目远程调试 springboot远程 单一职责原则 SenseVoice IPMITOOL 硬件管理 opcua opcda KEPServer安装 onlyoffice P2P HDLC yolov8 小智AI服务端 xiaozhi TTS cd 目录切换 uniapp VR手套 数据手套 动捕手套 动捕数据手套 微信开放平台 微信公众号配置 移动云 游戏开发 hexo FTP 服务器 SSH 服务 SSH Server OpenSSH Server 多进程 远程 执行 sshpass 操作 nfs wps SSL 域名 Anolis nginx安装 环境安装 linux插件下载 mariadb 7z 自定义客户端 SAS 输入法 僵尸世界大战 游戏服务器搭建 毕昇JDK webstorm Trae IDE AI 原生集成开发环境 Trae AI linux安装配置 mcu Open WebUI rnn xpath定位元素 微信小程序域名配置 微信小程序服务器域名 微信小程序合法域名 小程序配置业务域名 微信小程序需要域名吗 微信小程序添加域名 etcd 数据安全 RBAC EasyConnect 半虚拟化 硬件虚拟化 Hypervisor Kali Linux 渗透测试 信息收集 h.264 seatunnel RustDesk自建服务器 rustdesk服务器 docker rustdesk 群晖 黑客技术 micropython esp32 mqtt 项目部署到linux服务器 项目部署过程 RoboVLM 通用机器人策略 VLA设计哲学 vlm fot robot 视觉语言动作模型 具身智能 本地部署 pyqt 开机自启动 rag ragflow 源码启动 pgpool vscode1.86 1.86版本 ssh远程连接 合成模型 扩散模型 图像生成 SSE open Euler dde 迁移指南 田俊楠 网卡的名称修改 eth0 ens33 实习 ue4 着色器 ue5 cpp-httplib 网工 outlook DevEco Studio OpenHarmony 真机调试 TrinityCore 魔兽世界 cuda cudnn anaconda sysctl.conf vm.nr_hugepages adobe bug clickhouse 服务器管理 配置教程 服务器安装 网站管理 崖山数据库 YashanDB 视频编解码 redhat Ubuntu 24.04.1 轻量级服务器 python3.11 dash 正则表达式 金仓数据库 2025 征文 数据库平替用金仓 sdkman 文件分享 软件工程 iis Linux awk awk函数 awk结构 awk内置变量 awk参数 awk脚本 awk详解 W5500 OLED u8g2 token sas chfs ubuntu 16.04 环境迁移 lio-sam SLAM 远程看看 远程协助 HiCar CarLife+ CarPlay QT RK3588 Node-Red 编程工具 流编程 知识图谱 CPU prometheus数据采集 prometheus数据模型 prometheus特点 网络穿透 相机 cpu 实时 使用 高效日志打印 串口通信日志 服务器日志 系统状态监控日志 异常记录日志 5G 3GPP 卫星通信 毕设 相差8小时 UTC 时间 mysql离线安装 ubuntu22.04 mysql8.0 源码 GoogLeNet Typore OD机试真题 华为OD机试真题 服务器能耗统计 dns 三级等保 服务器审计日志备份 bat hive Hive环境搭建 hive3环境 Hive远程模式 多个客户端访问 IO多路复用 TCP相关API webgl 智能音箱 智能家居 Python基础 Python教程 Python技巧 bootstrap centos-root /dev/mapper yum clean all df -h / du -sh 考研 软考 在线office linux 命令 sed 命令 基础入门 Claude XCC Lenovo chrome 浏览器下载 chrome 下载安装 谷歌浏览器下载 AnythingLLM AnythingLLM安装 繁忙 解决办法 替代网站 汇总推荐 AI推理 CDN easyui langchain Clion Nova ResharperC++引擎 Centos7 远程开发 dba tailscale derp derper 中转 主板 电源 网卡 交叉编译 线性代数 电商平台 大文件分片上传断点续传及进度条 如何批量上传超大文件并显示进度 axios大文件切片上传详细教 node服务器合并切片 vue3大文件上传报错提示错误 大文件秒传跨域报错cors 环境配置 压测 ECS 本地知识库部署 DeepSeek R1 模型 yum源切换 更换国内yum源 springcloud 基础环境 Linux find grep ubuntu20.04 开机黑屏 代理 飞牛nas fnos flink 沙盒 word 软件需求 多路转接 PVE linux环境变量 vr USB网络共享 mm-wiki搭建 linux搭建mm-wiki mm-wiki搭建与使用 mm-wiki使用 mm-wiki详解 Playwright mq Unity插件 x64 SIGSEGV xmm0 hosts 李心怡 Google pay Apple pay xml TrueLicense Linux的权限 链表 Ark-TS语言 ssh漏洞 ssh9.9p2 CVE-2025-23419 docker部署Python Invalid Host allowedHosts iftop 网络流量监控 粘包问题 DNS cmos UDP AI代码编辑器 VMware创建虚拟机 idea 服务器时间 perf DBeaver 数据仓库 kerberos 网络爬虫 大模型推理 大模型学习 minicom 串口调试工具 音乐服务器 Navidrome 音流 Ubuntu共享文件夹 共享目录 Linux共享文件夹 ping++ 搭建个人相关服务器 neo4j openstack Xen 在线预览 xlsx xls文件 在浏览器直接打开解析xls表格 前端实现vue3打开excel 文件地址url或接口文档流二进 Attention TCP协议 增强现实 沉浸式体验 应用场景 技术实现 案例分析 AR RAG 检索增强生成 文档解析 大模型垂直应用 匿名管道 Logstash 日志采集 架构与原理 虚幻引擎 LInux DocFlow 开发 milvus ubuntu24 vivado24 阿里云ECS 论文阅读 自动化编程 怎么卸载MySQL MySQL怎么卸载干净 MySQL卸载重新安装教程 MySQL5.7卸载 Linux卸载MySQL8.0 如何卸载MySQL教程 MySQL卸载与安装 edge浏览器 Deepseek-R1 私有化部署 推理模型 lsb_release /etc/issue /proc/version uname -r 查看ubuntu版本 欧标 OCPP 物联网开发 ubuntu24.04.1 lua 社交电子 vue-i18n 国际化多语言 vue2中英文切换详细教程 如何动态加载i18n语言包 把语言json放到服务器调用 前端调用api获取语言配置文件 ros2 moveit 机器人运动 RAID RAID技术 磁盘 存储 CentOS Stream CentOS gnu 域名服务 DHCP 符号链接 配置 音乐库 飞牛 实用教程 蓝桥杯 fast kali 共享文件夹 YOLOv12 大模型应用 嵌入式Linux IPC IO模型 searxng 网络药理学 生信 PPI String Cytoscape CytoHubba midjourney rime 裸金属服务器 弹性裸金属服务器 灵办AI 状态管理的 UDP 服务器 Arduino RTOS Redis Desktop 干货分享 黑客工具 密码爆破 Windsurf 元服务 应用上架 信创 信创终端 中科方德 程序员创富 nlp trae 执法记录仪 智能安全帽 smarteye 聚类 mybatis 语法 EtherNet/IP串口网关 EIP转RS485 EIP转Modbus EtherNet/IP网关协议 EIP转RS485网关 EIP串口服务器 ai小智 语音助手 ai小智配网 ai小智教程 esp32语音助手 diy语音助手 热榜 强化学习 数据库系统 dock 加速 C# MQTTS 双向认证 emqx 政务 分布式系统 监控运维 Prometheus Grafana gpt-3 文心一言 分析解读 办公自动化 自动化生成 pdf教程 做raid 装系统 内网服务器 内网代理 内网通信 EtherCAT转Modbus ECT转Modbus协议 EtherCAT转485网关 ECT转Modbus串口网关 EtherCAT转485协议 ECT转Modbus网关 arcgis VM搭建win2012 win2012应急响应靶机搭建 攻击者获取服务器权限 上传wakaung病毒 应急响应并溯源 挖矿病毒处置 应急响应综合性靶场 ip命令 新增网卡 新增IP 启动网卡 人工智能生成内容 风扇控制软件 金融 网络用户购物行为分析可视化平台 大数据毕业设计 产品经理 火绒安全 MDK 嵌入式开发工具 论文笔记 sublime text MacMini 迷你主机 mini Apple 剧本 docker搭建pg docker搭建pgsql pg授权 postgresql使用 postgresql搭建 PX4 运维监控 uni-file-picker 拍摄从相册选择 uni.uploadFile H5上传图片 微信小程序上传图片 pyautogui 拓扑图 bot Docker leetcode 推荐算法 IO 离线部署dify VS Code spark HistoryServer Spark YARN jobhistory eNSP 企业网络规划 华为eNSP 网络规划 项目部署 AD 域管理 网站搭建 serv00 grub 版本升级 扩容 wpf VSCode AP配网 AK配网 小程序AP配网和AK配网教程 WIFI设备配网小程序UDP开 服务器数据恢复 数据恢复 存储数据恢复 raid5数据恢复 磁盘阵列数据恢复 磁盘镜像 服务器镜像 服务器实时复制 实时文件备份 keepalived sonoma 自动更新 GIS 遥感 WebGIS minecraft xshell termius iterm2 数据库开发 database 大大通 第三代半导体 碳化硅 ai工具 trea java-rocketmq ldap OpenSSH Kylin-Server 分布式训练 ArcTS 登录 ArcUI GridItem arkUI 服务网格 istio 内网环境 deep learning js Cookie chrome devtools chromedriver WebRTC 键盘 win服务器架设 windows server seleium 目标跟踪 OpenVINO 推理应用 SRS 流媒体 直播 鸿蒙开发 移动开发 嵌入式系统开发 ABAP 代理服务器 车载系统 影刀 #影刀RPA# 图形渲染 黑苹果 系统开发 binder framework 源码环境 sequoiaDB 存储维护 NetApp存储 EMC存储 捆绑 链接 谷歌浏览器 youtube google gmail 雨云服务器 CLion 服务器部署ai模型 figma docker命令大全 远程服务 conda配置 conda镜像源 alias unalias 别名 软负载 risc-v firewalld 混合开发 JDK regedit 开机启动 AI Agent 字节智能运维 大模型部署 swoole curl wget 端口 查看 ss visual studio 北亚数据恢复 oracle数据恢复 本地化部署 docker部署翻译组件 docker部署deepl docker搭建deepl java对接deepl 翻译组件使用 京东云 西门子PLC 通讯 VLAN 企业网络 上传视频至服务器代码 vue3批量上传多个视频并预览 如何实现将本地视频上传到网页 element plu视频上传 ant design vue vue3本地上传视频及预览移除 大模型面经 自动化任务管理 私有化 宕机切换 服务器宕机 rpa docker run 数据卷挂载 交互模式 玩机技巧 软件分享 软件图标 triton 模型分析