此版本仍在开发中,尚不被认为是稳定的。对于最新的稳定版本,请使用 Spring Authorization Server 1.5.2! |
作方法:使用 JPA 实现核心服务
本指南展示了如何使用JPA实现Spring Authorization Server的核心服务。本指南的目的是提供一个自己实现这些服务的起点,目的是您可以进行修改以满足您的需求。
定义数据模型
本指南为数据模型提供了一个起点,并使用最简单的结构和数据类型。为了提出初始架构,我们首先查看核心服务使用的域对象。
除了Tokens、状态、元数据、设置和声明值外,我们对所有列都使用 JPA 默认列长度 255。实际上,可能需要自定义所使用的列的长度甚至类型。建议在部署到生产环境之前进行试验和测试。 |
客户端架构
这RegisteredClient
domain 对象包含一些多值字段和一些需要存储任意键/值数据的设置字段。以下列表显示了client
图式。
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)
);
授权架构
这OAuth2Authorization
domain 对象更为复杂,包含多个多值字段以及大量任意长的Tokens值、元数据、设置和声明值。内置的 JDBC 实现利用扁平化结构,该结构更倾向于性能而不是规范化,我们在这里也采用了这种结构。
很难找到一个在所有情况下都适用于所有数据库提供商的扁平化数据库模式。您可能需要规范化或大量更改以下模式以满足您的需要。 |
以下列表显示了authorization
图式。
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)
);
授权同意架构
这OAuth2AuthorizationConsent
domain 对象是最容易建模的对象,除了复合键外,还仅包含一个多值字段。以下列表显示了authorizationconsent
图式。
CREATE TABLE authorizationConsent (
registeredClientId varchar(255) NOT NULL,
principalName varchar(255) NOT NULL,
authorities varchar(1000) NOT NULL,
PRIMARY KEY (registeredClientId, principalName)
);
创建 JPA 实体
前面的架构示例为我们需要创建的实体的结构提供了参考。
以下实体的注释最少,只是示例。 它们允许动态创建模式,因此不需要手动执行上述 sql 脚本。 |
客户实体
以下列表显示了Client
实体,用于保留从RegisteredClient
domain 对象。
import java.time.Instant;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@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;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public Instant getClientIdIssuedAt() {
return clientIdIssuedAt;
}
public void setClientIdIssuedAt(Instant clientIdIssuedAt) {
this.clientIdIssuedAt = clientIdIssuedAt;
}
public String getClientSecret() {
return clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public Instant getClientSecretExpiresAt() {
return clientSecretExpiresAt;
}
public void setClientSecretExpiresAt(Instant clientSecretExpiresAt) {
this.clientSecretExpiresAt = clientSecretExpiresAt;
}
public String getClientName() {
return clientName;
}
public void setClientName(String clientName) {
this.clientName = clientName;
}
public String getClientAuthenticationMethods() {
return clientAuthenticationMethods;
}
public void setClientAuthenticationMethods(String clientAuthenticationMethods) {
this.clientAuthenticationMethods = clientAuthenticationMethods;
}
public String getAuthorizationGrantTypes() {
return authorizationGrantTypes;
}
public void setAuthorizationGrantTypes(String authorizationGrantTypes) {
this.authorizationGrantTypes = authorizationGrantTypes;
}
public String getRedirectUris() {
return redirectUris;
}
public void setRedirectUris(String redirectUris) {
this.redirectUris = redirectUris;
}
public String getPostLogoutRedirectUris() {
return this.postLogoutRedirectUris;
}
public void setPostLogoutRedirectUris(String postLogoutRedirectUris) {
this.postLogoutRedirectUris = postLogoutRedirectUris;
}
public String getScopes() {
return scopes;
}
public void setScopes(String scopes) {
this.scopes = scopes;
}
public String getClientSettings() {
return clientSettings;
}
public void setClientSettings(String clientSettings) {
this.clientSettings = clientSettings;
}
public String getTokenSettings() {
return tokenSettings;
}
public void setTokenSettings(String tokenSettings) {
this.tokenSettings = tokenSettings;
}
}
授权实体
以下列表显示了Authorization
实体,用于保留从OAuth2Authorization
domain 对象。
授权同意实体
以下列表显示了AuthorizationConsent
实体,用于保留从OAuth2AuthorizationConsent
domain 对象。
创建 Spring Data 存储库
通过仔细检查每个核心服务的接口并查看Jdbc
实现,我们可以派生出支持每个接口的 JPA 版本所需的最小查询集。
客户端存储库
以下列表显示了ClientRepository
,它能够找到一个Client
通过id
和clientId
领域。
import java.util.Optional;
import sample.jpa.entity.client.Client;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ClientRepository extends JpaRepository<Client, String> {
Optional<Client> findByClientId(String clientId);
}
授权存储库
以下列表显示了AuthorizationRepository
,它能够找到一个Authorization
通过id
字段以及state
,authorizationCodeValue
,accessTokenValue
,refreshTokenValue
,userCodeValue
和deviceCodeValue
token 字段。
它还允许查询标记字段的组合。
授权同意存储库
以下列表显示了AuthorizationConsentRepository
,它能够查找并删除AuthorizationConsent
通过registeredClientId
和principalName
构成复合主键的字段。
实现核心服务
有了上述实体和存储库,我们就可以开始实现核心服务了。
通过查看Jdbc
实现时,我们可以派生出一组最小的内部实用程序,用于在枚举的字符串值之间进行转换,以及读取和写入属性、设置、元数据和声明字段的 JSON 数据。
请记住,将 JSON 数据写入具有固定长度的文本列已被证明是有问题的Jdbc 实现。
虽然这些示例继续这样做,但您可能需要将这些字段拆分为支持任意长数据值的单独表或数据存储。 |
已注册的客户端存储库
以下列表显示了JpaRegisteredClientRepository
,它使用ClientRepository
用于持久化Client
和映射到RegisteredClient
domain 对象。
RegisteredClientRepository
实现import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import sample.jpa.entity.client.Client;
import sample.jpa.repository.client.ClientRepository;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@Component
public class JpaRegisteredClientRepository implements RegisteredClientRepository {
private final ClientRepository clientRepository;
private final ObjectMapper objectMapper = new ObjectMapper();
public JpaRegisteredClientRepository(ClientRepository clientRepository) {
Assert.notNull(clientRepository, "clientRepository cannot be null");
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) {
Assert.notNull(registeredClient, "registeredClient cannot be null");
this.clientRepository.save(toEntity(registeredClient));
}
@Override
public RegisteredClient findById(String id) {
Assert.hasText(id, "id cannot be empty");
return this.clientRepository.findById(id).map(this::toObject).orElse(null);
}
@Override
public RegisteredClient findByClientId(String clientId) {
Assert.hasText(clientId, "clientId cannot be empty");
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<Map<String, Object>>() {
});
} 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); // Custom authorization grant type
}
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); // Custom client authentication method
}
}
授权服务
以下列表显示了JpaOAuth2AuthorizationService
,它使用AuthorizationRepository
用于持久化Authorization
和映射到OAuth2Authorization
domain 对象。
授权同意服务
以下列表显示了JpaOAuth2AuthorizationConsentService
,它使用AuthorizationConsentRepository
用于持久化AuthorizationConsent
和映射到OAuth2AuthorizationConsent
domain 对象。