使い方: 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 スクリプトを手動で実行する必要はありません。 |
クライアントエンティティ
次のリストは、RegisteredClient
ドメインオブジェクトからマップされた情報を永続化するために使用される Client
エンティティを示しています。
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;
}
}
認可エンティティ
次のリストは、OAuth2Authorization
ドメインオブジェクトからマップされた情報を永続化するために使用される Authorization
エンティティを示しています。
認可同意エンティティ
次のリストは、OAuth2AuthorizationConsent
ドメインオブジェクトからマップされた情報を永続化するために使用される AuthorizationConsent
エンティティを示しています。
Spring Data リポジトリを作成する
各コアサービスのインターフェースを綿密に調べ、Jdbc
実装を確認することで、各インターフェースの JPA バージョンをサポートするために必要なクエリの最小限のセットを導き出すことができます。
クライアントリポジトリ
次のリストは、id
および clientId
フィールドによって Client
を見つけることができる ClientRepository
を示しています。
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);
}
認可リポジトリ
次のリストは、id
フィールドと state
、authorizationCodeValue
、accessTokenValue
、refreshTokenValue
、userCodeValue
、deviceCodeValue
トークンフィールドによって Authorization
を見つけることができる AuthorizationRepository
を示しています。また、トークンフィールドの組み合わせを照会することもできます。
認可同意リポジトリ
次のリストは、複合主キーを形成する registeredClientId
および principalName
フィールドによって AuthorizationConsent
を検索および削除できる AuthorizationConsentRepository
を示しています。
コアサービスの実装
上記のエンティティとリポジトリを使用して、コアサービスの実装を開始できます。Jdbc
実装を確認することで、列挙型の文字列値との間で変換を行い、属性、設定、メタデータ、クレームフィールドの JSON データを読み書きするための内部ユーティリティの最小限のセットを導き出すことができます。
JSON データを固定長のテキスト列に書き込むことは、Jdbc 実装で問題があることが証明されていることに注意してください。これらの例では引き続きこれを行いますが、これらのフィールドを別のテーブルまたは任意の長さのデータ値をサポートするデータストアに分割する必要がある場合があります。 |
登録済みクライアントリポジトリ
次のリストは、ClientRepository
を使用して Client
を永続化し、RegisteredClient
ドメインオブジェクトとの間でマップする JpaRegisteredClientRepository
を示しています。
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
}
}
認可サービス
次のリストは、AuthorizationRepository
を使用して Authorization
を永続化し、OAuth2Authorization
ドメインオブジェクトとの間でマッピングする JpaOAuth2AuthorizationService
を示しています。
認可同意サービス
次のリストは、AuthorizationConsentRepository
を使用して AuthorizationConsent
を永続化し、OAuth2AuthorizationConsent
ドメインオブジェクトとの間でマッピングする JpaOAuth2AuthorizationConsentService
を示しています。