Spring Authorization Server 使用 JPA 实现核心服务指南

Spring Authorization Server 使用 JPA 实现核心服务指南

【免费下载链接】spring-authorization-server Spring Authorization Server 【免费下载链接】spring-authorization-server 项目地址: https://gitcode.com/gh_mirrors/sp/spring-authorization-server

概述

Spring Authorization Server 是 Spring Security 团队提供的 OAuth 2.1 和 OpenID Connect 1.0 规范实现。在实际生产环境中,我们通常需要将授权服务器的核心服务(客户端注册、授权管理、授权同意管理)持久化到数据库中。本文将详细介绍如何使用 JPA(Java Persistence API)实现这些核心服务。

核心服务架构

mermaid

数据模型设计

客户端数据模型

CREATE TABLE client (
    id varchar(255) NOT NULL,
    clientId varchar(255) NOT NULL,
    clientIdIssuedAt timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
    clientSecret varchar(255) DEFAULT NULL,
    clientSecretExpiresAt timestamp DEFAULT NULL,
    clientName varchar(255) NOT NULL,
    clientAuthenticationMethods varchar(1000) NOT NULL,
    authorizationGrantTypes varchar(1000) NOT NULL,
    redirectUris varchar(1000) DEFAULT NULL,
    postLogoutRedirectUris varchar(1000) DEFAULT NULL,
    scopes varchar(1000) NOT NULL,
    clientSettings varchar(2000) NOT NULL,
    tokenSettings varchar(2000) NOT NULL,
    PRIMARY KEY (id)
);

授权数据模型

CREATE TABLE authorization (
    id varchar(255) NOT NULL,
    registeredClientId varchar(255) NOT NULL,
    principalName varchar(255) NOT NULL,
    authorizationGrantType varchar(255) NOT NULL,
    authorizedScopes varchar(1000) DEFAULT NULL,
    attributes varchar(4000) DEFAULT NULL,
    state varchar(500) DEFAULT NULL,
    authorizationCodeValue varchar(4000) DEFAULT NULL,
    authorizationCodeIssuedAt timestamp DEFAULT NULL,
    authorizationCodeExpiresAt timestamp DEFAULT NULL,
    authorizationCodeMetadata varchar(2000) DEFAULT NULL,
    accessTokenValue varchar(4000) DEFAULT NULL,
    accessTokenIssuedAt timestamp DEFAULT NULL,
    accessTokenExpiresAt timestamp DEFAULT NULL,
    accessTokenMetadata varchar(2000) DEFAULT NULL,
    accessTokenType varchar(255) DEFAULT NULL,
    accessTokenScopes varchar(1000) DEFAULT NULL,
    refreshTokenValue varchar(4000) DEFAULT NULL,
    refreshTokenIssuedAt timestamp DEFAULT NULL,
    refreshTokenExpiresAt timestamp DEFAULT NULL,
    refreshTokenMetadata varchar(2000) DEFAULT NULL,
    oidcIdTokenValue varchar(4000) DEFAULT NULL,
    oidcIdTokenIssuedAt timestamp DEFAULT NULL,
    oidcIdTokenExpiresAt timestamp DEFAULT NULL,
    oidcIdTokenMetadata varchar(2000) DEFAULT NULL,
    oidcIdTokenClaims varchar(2000) DEFAULT NULL,
    userCodeValue varchar(4000) DEFAULT NULL,
    userCodeIssuedAt timestamp DEFAULT NULL,
    userCodeExpiresAt timestamp DEFAULT NULL,
    userCodeMetadata varchar(2000) DEFAULT NULL,
    deviceCodeValue varchar(4000) DEFAULT NULL,
    deviceCodeIssuedAt timestamp DEFAULT NULL,
    deviceCodeExpiresAt timestamp DEFAULT NULL,
    deviceCodeMetadata varchar(2000) DEFAULT NULL,
    PRIMARY KEY (id)
);

授权同意数据模型

CREATE TABLE authorizationConsent (
    registeredClientId varchar(255) NOT NULL,
    principalName varchar(255) NOT NULL,
    authorities varchar(1000) NOT NULL,
    PRIMARY KEY (registeredClientId, principalName)
);

JPA 实体实现

客户端实体

@Entity
@Table(name = "`client`")
public class Client {
    @Id
    private String id;
    private String clientId;
    private Instant clientIdIssuedAt;
    private String clientSecret;
    private Instant clientSecretExpiresAt;
    private String clientName;
    
    @Column(length = 1000)
    private String clientAuthenticationMethods;
    
    @Column(length = 1000)
    private String authorizationGrantTypes;
    
    @Column(length = 1000)
    private String redirectUris;
    
    @Column(length = 1000)
    private String postLogoutRedirectUris;
    
    @Column(length = 1000)
    private String scopes;
    
    @Column(length = 2000)
    private String clientSettings;
    
    @Column(length = 2000)
    private String tokenSettings;
    
    // Getter 和 Setter 方法
}

授权实体

@Entity
@Table(name = "`authorization`")
public class Authorization {
    @Id
    private String id;
    private String registeredClientId;
    private String principalName;
    private String authorizationGrantType;
    
    @Column(length = 1000)
    private String authorizedScopes;
    
    @Column(length = 4000)
    private String attributes;
    
    @Column(length = 500)
    private String state;
    
    @Column(length = 4000)
    private String authorizationCodeValue;
    
    private Instant authorizationCodeIssuedAt;
    private Instant authorizationCodeExpiresAt;
    
    @Column(length = 2000)
    private String authorizationCodeMetadata;
    
    @Column(length = 4000)
    private String accessTokenValue;
    
    private Instant accessTokenIssuedAt;
    private Instant accessTokenExpiresAt;
    
    @Column(length = 2000)
    private String accessTokenMetadata;
    
    private String accessTokenType;
    
    @Column(length = 1000)
    private String accessTokenScopes;
    
    @Column(length = 4000)
    private String refreshTokenValue;
    
    private Instant refreshTokenIssuedAt;
    private Instant refreshTokenExpiresAt;
    
    @Column(length = 2000)
    private String refreshTokenMetadata;
    
    @Column(length = 4000)
    private String oidcIdTokenValue;
    
    private Instant oidcIdTokenIssuedAt;
    private Instant oidcIdTokenExpiresAt;
    
    @Column(length = 2000)
    private String oidcIdTokenMetadata;
    
    @Column(length = 2000)
    private String oidcIdTokenClaims;
    
    @Column(length = 4000)
    private String userCodeValue;
    
    private Instant userCodeIssuedAt;
    private Instant userCodeExpiresAt;
    
    @Column(length = 2000)
    private String userCodeMetadata;
    
    @Column(length = 4000)
    private String deviceCodeValue;
    
    private Instant deviceCodeIssuedAt;
    private Instant deviceCodeExpiresAt;
    
    @Column(length = 2000)
    private String deviceCodeMetadata;
    
    // Getter 和 Setter 方法
}

Repository 层实现

客户端 Repository

public interface ClientRepository extends CrudRepository<Client, String> {
    Optional<Client> findByClientId(String clientId);
}

授权 Repository

public interface AuthorizationRepository extends CrudRepository<Authorization, String> {
    Optional<Authorization> findByState(String state);
    Optional<Authorization> findByAuthorizationCodeValue(String authorizationCode);
    Optional<Authorization> findByAccessTokenValue(String accessToken);
    Optional<Authorization> findByRefreshTokenValue(String refreshToken);
    Optional<Authorization> findByUserCodeValue(String userCode);
    Optional<Authorization> findByDeviceCodeValue(String deviceCode);
}

核心服务实现

RegisteredClientRepository 实现

@Component
public class JpaRegisteredClientRepository implements RegisteredClientRepository {
    private final ClientRepository clientRepository;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public JpaRegisteredClientRepository(ClientRepository clientRepository) {
        this.clientRepository = clientRepository;
        
        ClassLoader classLoader = JpaRegisteredClientRepository.class.getClassLoader();
        List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
        this.objectMapper.registerModules(securityModules);
        this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
    }

    @Override
    public void save(RegisteredClient registeredClient) {
        this.clientRepository.save(toEntity(registeredClient));
    }

    @Override
    public RegisteredClient findById(String id) {
        return this.clientRepository.findById(id).map(this::toObject).orElse(null);
    }

    @Override
    public RegisteredClient findByClientId(String clientId) {
        return this.clientRepository.findByClientId(clientId).map(this::toObject).orElse(null);
    }

    private RegisteredClient toObject(Client client) {
        Set<String> clientAuthenticationMethods = StringUtils.commaDelimitedListToSet(
                client.getClientAuthenticationMethods());
        Set<String> authorizationGrantTypes = StringUtils.commaDelimitedListToSet(
                client.getAuthorizationGrantTypes());
        Set<String> redirectUris = StringUtils.commaDelimitedListToSet(
                client.getRedirectUris());
        Set<String> postLogoutRedirectUris = StringUtils.commaDelimitedListToSet(
                client.getPostLogoutRedirectUris());
        Set<String> clientScopes = StringUtils.commaDelimitedListToSet(
                client.getScopes());

        RegisteredClient.Builder builder = RegisteredClient.withId(client.getId())
                .clientId(client.getClientId())
                .clientIdIssuedAt(client.getClientIdIssuedAt())
                .clientSecret(client.getClientSecret())
                .clientSecretExpiresAt(client.getClientSecretExpiresAt())
                .clientName(client.getClientName())
                .clientAuthenticationMethods(authenticationMethods ->
                        clientAuthenticationMethods.forEach(authenticationMethod ->
                                authenticationMethods.add(resolveClientAuthenticationMethod(authenticationMethod))))
                .authorizationGrantTypes((grantTypes) ->
                        authorizationGrantTypes.forEach(grantType ->
                                grantTypes.add(resolveAuthorizationGrantType(grantType))))
                .redirectUris((uris) -> uris.addAll(redirectUris))
                .postLogoutRedirectUris((uris) -> uris.addAll(postLogoutRedirectUris))
                .scopes((scopes) -> scopes.addAll(clientScopes));

        Map<String, Object> clientSettingsMap = parseMap(client.getClientSettings());
        builder.clientSettings(ClientSettings.withSettings(clientSettingsMap).build());

        Map<String, Object> tokenSettingsMap = parseMap(client.getTokenSettings());
        builder.tokenSettings(TokenSettings.withSettings(tokenSettingsMap).build());

        return builder.build();
    }

    private Client toEntity(RegisteredClient registeredClient) {
        List<String> clientAuthenticationMethods = new ArrayList<>(registeredClient.getClientAuthenticationMethods().size());
        registeredClient.getClientAuthenticationMethods().forEach(clientAuthenticationMethod ->
                clientAuthenticationMethods.add(clientAuthenticationMethod.getValue()));

        List<String> authorizationGrantTypes = new ArrayList<>(registeredClient.getAuthorizationGrantTypes().size());
        registeredClient.getAuthorizationGrantTypes().forEach(authorizationGrantType ->
                authorizationGrantTypes.add(authorizationGrantType.getValue()));

        Client entity = new Client();
        entity.setId(registeredClient.getId());
        entity.setClientId(registeredClient.getClientId());
        entity.setClientIdIssuedAt(registeredClient.getClientIdIssuedAt());
        entity.setClientSecret(registeredClient.getClientSecret());
        entity.setClientSecretExpiresAt(registeredClient.getClientSecretExpiresAt());
        entity.setClientName(registeredClient.getClientName());
        entity.setClientAuthenticationMethods(StringUtils.collectionToCommaDelimitedString(clientAuthenticationMethods));
        entity.setAuthorizationGrantTypes(StringUtils.collectionToCommaDelimitedString(authorizationGrantTypes));
        entity.setRedirectUris(StringUtils.collectionToCommaDelimitedString(registeredClient.getRedirectUris()));
        entity.setPostLogoutRedirectUris(StringUtils.collectionToCommaDelimitedString(registeredClient.getPostLogoutRedirectUris()));
        entity.setScopes(StringUtils.collectionToCommaDelimitedString(registeredClient.getScopes()));
        entity.setClientSettings(writeMap(registeredClient.getClientSettings().getSettings()));
        entity.setTokenSettings(writeMap(registeredClient.getTokenSettings().getSettings()));

        return entity;
    }

    private Map<String, Object> parseMap(String data) {
        try {
            return this.objectMapper.readValue(data, new TypeReference<>() {});
        } catch (Exception ex) {
            throw new IllegalArgumentException(ex.getMessage(), ex);
        }
    }

    private String writeMap(Map<String, Object> data) {
        try {
            return this.objectMapper.writeValueAsString(data);
        } catch (Exception ex) {
            throw new IllegalArgumentException(ex.getMessage(), ex);
        }
    }

    private static AuthorizationGrantType resolveAuthorizationGrantType(String authorizationGrantType) {
        if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) {
            return AuthorizationGrantType.AUTHORIZATION_CODE;
        } else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(authorizationGrantType)) {
            return AuthorizationGrantType.CLIENT_CREDENTIALS;
        } else if (AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(authorizationGrantType)) {
            return AuthorizationGrantType.REFRESH_TOKEN;
        }
        return new AuthorizationGrantType(authorizationGrantType); // 自定义授权类型
    }

    private static ClientAuthenticationMethod resolveClientAuthenticationMethod(String clientAuthenticationMethod) {
        if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue().equals(clientAuthenticationMethod)) {
            return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
        } else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientAuthenticationMethod)) {
            return ClientAuthenticationMethod.CLIENT_SECRET_POST;
        } else if (ClientAuthenticationMethod.NONE.getValue().equals(clientAuthenticationMethod)) {
            return ClientAuthenticationMethod.NONE;
        }
        return new ClientAuthenticationMethod(clientAuthenticationMethod); // 自定义客户端认证方法
    }
}

配置和使用

配置数据源和 JPA

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/oauth2_db
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQLDialect

启用 JPA 实现

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig {

    @Bean
    public RegisteredClientRepository registeredClientRepository(ClientRepository clientRepository) {
        return new JpaRegisteredClientRepository(clientRepository);
    }

    @Bean
    public OAuth2AuthorizationService authorizationService(AuthorizationRepository authorizationRepository) {
        return new JpaOAuth2AuthorizationService(authorizationRepository);
    }

    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(
            AuthorizationConsentRepository authorizationConsentRepository) {
        return new JpaOAuth2AuthorizationConsentService(authorizationConsentRepository);
    }
}

性能优化建议

数据库索引优化

-- 客户端表索引
CREATE INDEX idx_client_client_id ON client(clientId);
CREATE INDEX idx_client_client_name ON client(clientName);

-- 授权表索引
CREATE INDEX idx_authorization_registered_client_id ON authorization(registeredClientId);
CREATE INDEX idx_authorization_principal_name ON authorization(principalName);
CREATE INDEX idx_authorization_state ON authorization(state);
CREATE INDEX idx_authorization_access_token ON authorization(accessTokenValue);
CREATE INDEX idx_authorization_refresh_token ON authorization(refreshTokenValue);
CREATE INDEX idx_authorization_auth_code ON authorization(authorizationCodeValue);

-- 授权同意表索引
CREATE INDEX idx_auth_consent_principal ON authorizationConsent(principalName);

缓存策略

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("clients", "authorizations", "consents");
    }
}

@Component
public class CachedJpaRegisteredClientRepository implements RegisteredClientRepository {
    
    private final RegisteredClientRepository delegate;
    private final Cache clientsCache;
    
    public CachedJpaRegisteredClientRepository(RegisteredClientRepository delegate, CacheManager cacheManager) {
        this.delegate = delegate;
        this.clientsCache = cacheManager.getCache("clients");
    }
    
    @Override
    @Cacheable(value = "clients", key = "#id")
    public RegisteredClient findById(String id) {
        return delegate.findById(id);
    }
    
    @Override
    @Cacheable(value = "clients", key = "#clientId")
    public RegisteredClient findByClientId(String clientId) {
        return delegate.findByClientId(clientId);
    }
    
    @Override
    @CacheEvict(value = "clients", allEntries = true)
    public void save(RegisteredClient registeredClient) {
        delegate.save(registeredClient);
    }
}

常见问题与解决方案

问题1:JSON 数据长度限制

解决方案:对于可能超过数据库列长度限制的 JSON 数据,可以考虑以下策略:

  1. 使用 @Lob 注解替代 @Column(length)
  2. 将大字段拆分到单独的表中
  3. 使用专门的 JSON 列类型(如 PostgreSQL 的 jsonb

问题2:多值字段处理

解决方案:使用 Spring 的 StringUtils 工具类进行逗号分隔字符串和集合之间的转换:

// 集合转字符串
String scopesString = StringUtils.collectionToCommaDelimitedString(scopesCollection);

// 字符串转集合
Set<String> scopesSet = StringUtils.commaDelimitedListToSet(scopesString);

问题3:自定义授权类型和认证方法

解决方案:在转换方法中处理自定义类型:

private static AuthorizationGrantType resolveAuthorizationGrantType(String authorizationGrantType) {
    // 标准类型检查
    if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) {
        return AuthorizationGrantType.AUTHORIZATION_CODE;
    }
    // 其他标准类型...
    
    // 自定义类型
    return new AuthorizationGrantType(authorizationGrantType);
}

总结

通过本文的详细指南,您已经了解了如何使用 JPA 实现 Spring Authorization Server 的核心服务。关键要点包括:

  1. 数据模型设计:合理设计数据库表结构,考虑字段长度和多值字段处理
  2. 实体映射:使用 JPA 注解正确映射领域对象到数据库表
  3. 类型转换:正确处理枚举类型、集合类型和 JSON 数据的序列化/反序列化
  4. 性能优化:通过索引和缓存策略提升系统性能
  5. 扩展性:支持自定义授权类型和认证方法

这种实现方式提供了灵活的持久化方案,可以根据实际业务需求进行定制和优化。在实际生产环境中,建议进行充分的性能测试和压力测试,确保系统能够满足高并发场景的需求。

【免费下载链接】spring-authorization-server Spring Authorization Server 【免费下载链接】spring-authorization-server 项目地址: https://gitcode.com/gh_mirrors/sp/spring-authorization-server

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值