操作指南:使用 JPA 實作核心服務
本指南說明如何使用 JPA 實作 核心服務 的 Spring Authorization Server。本指南的目的是提供一個起點,讓您自行實作這些服務,並可根據您的需求進行修改。
定義資料模型
本指南為資料模型提供了一個起點,並使用最簡單的結構和資料類型。為了提出初始結構描述,我們先檢閱核心服務使用的 領域物件。
除了令牌、狀態、元數據、設定和宣告值之外,我們對所有欄位使用 JPA 預設的 255 欄位長度。實際上,您使用的欄位長度甚至類型可能需要自訂。建議您在部署到生產環境之前進行實驗和測試。 |
用戶端結構描述
RegisteredClient
領域物件包含一些多值欄位和一些設定欄位,這些欄位需要儲存任意的鍵/值資料。以下列表顯示了 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
領域物件更為複雜,包含多個多值欄位,以及眾多任意長度的令牌值、元數據、設定和宣告值。內建的 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
領域物件是最簡單的模型,除了複合鍵之外,僅包含單個多值欄位。以下列表顯示了 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
領域物件對應而來的資訊。
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
領域物件對應而來的資訊。
授權同意實體
以下列表顯示了 AuthorizationConsent
實體,用於持久化從 OAuth2AuthorizationConsent
領域物件對應而來的資訊。
建立 Spring Data 儲存庫
透過仔細檢查每個核心服務的介面並檢閱 Jdbc
實作,我們可以推導出支援 JPA 版本之每個介面所需的最小查詢集。
用戶端儲存庫
以下列表顯示了 ClientRepository
,它可以透過 id
和 clientId
欄位找到 Client
。
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
,它可以透過 id
欄位以及 state
、authorizationCodeValue
、accessTokenValue
、refreshTokenValue
、userCodeValue
和 deviceCodeValue
令牌欄位找到 Authorization
。它也允許查詢令牌欄位的組合。
授權同意儲存庫
以下列表顯示了 AuthorizationConsentRepository
,它可以透過構成複合主鍵的 registeredClientId
和 principalName
欄位找到並刪除 AuthorizationConsent
。
實作核心服務
有了上述 實體 和 儲存庫,我們可以開始實作核心服務。透過檢閱 Jdbc
實作,我們可以推導出最小的內部實用程式集,用於在列舉的字串值之間進行轉換,以及讀取和寫入屬性、設定、元數據和宣告欄位的 JSON 資料。
請記住,將 JSON 資料寫入固定長度的文字欄位已被證明在 Jdbc 實作中存在問題。雖然這些範例繼續這樣做,但您可能需要將這些欄位拆分到單獨的表格或資料儲存區,以支援任意長度的資料值。 |
已註冊用戶端儲存庫
以下列表顯示了 JpaRegisteredClientRepository
,它使用 ClientRepository
來持久化 Client
,並對應到 RegisteredClient
領域物件和從其對應而來。
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
領域物件和從其對應而來。
授權同意服務
以下列表顯示了 JpaOAuth2AuthorizationConsentService
,它使用 AuthorizationConsentRepository
來持久化 AuthorizationConsent
,並對應到 OAuth2AuthorizationConsent
領域物件和從其對應而來。