📌 글에서 사용한 코드 : 깃헙
지난 글들에 이이서 이번 글에서는 클라이언트를 관리하는 RegisteredClientRepository
를 커스터마이징하고, MySQL과 연동해서 동작하도록 서버를 만들어보겠다.
바~로 드간다.
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientName("Your client name")
.clientId("your-client")
.clientSecret("{noop}your-secret") // 실제 운영환경에서는 임의의 문자열을 사용하고, 코드에 올리면 안됨
.clientAuthenticationMethods(methods -> {
methods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
})
.authorizationGrantTypes(types -> {
types.add(AuthorizationGrantType.AUTHORIZATION_CODE);
types.add(AuthorizationGrantType.REFRESH_TOKEN);
})
.redirectUris(uri -> {
uri.add("http://localhost:3000");
})
.postLogoutRedirectUri("http://localhost:3000")
.scopes(scope -> {
scope.add(OidcScopes.OPENID);
scope.add(OidcScopes.PROFILE);
})
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
.build();
return new InMemoryRegisteredClientRepository(oidcClient);
}
기존에 설정해두었던 InMemoryRegisteredClientRepository
빈을 제거한다.
import jakarta.persistence.*;
import lombok.*;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import java.time.Instant;
@Getter
@Entity
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Client {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(unique = true)
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 ClientSettings clientSettings;
@Column(length = 2000)
private TokenSettings tokenSettings;
}
기본적으로 제공되는 RegisteredClient
클래스를 대체하기 위한 엔티티를 만들어준다.
DB에 미리 테이블을 생성해두거나, ddl-auto
옵션을 활용하여 테이블이 자동으로 생성되도록 해두자.
import org.springframework.data.jpa.repository.JpaRepository;
import study.springoauth2authserver.entity.Client;
import java.util.Optional;
public interface ClientRepository extends JpaRepository<Client, String> {
Optional<Client> findByClientId(String clientId);
}
JpaRepository
를 상속받아서 Client를 관리할 Repository를 하나 만들어주었다.
import lombok.RequiredArgsConstructor;
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.stereotype.Component;
import study.springoauth2authserver.entity.Client;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static org.springframework.security.oauth2.core.AuthorizationGrantType.*;
import static org.springframework.security.oauth2.core.ClientAuthenticationMethod.*;
import static org.springframework.util.StringUtils.collectionToCommaDelimitedString;
import static org.springframework.util.StringUtils.commaDelimitedListToSet;
@Component
@RequiredArgsConstructor
public class ClientUtils {
public RegisteredClient toObject(Client client) {
Set<String> clientAuthenticationMethods = commaDelimitedListToSet(client.getClientAuthenticationMethods());
Set<String> authorizationGrantTypes = commaDelimitedListToSet(client.getAuthorizationGrantTypes());
Set<String> redirectUris = commaDelimitedListToSet(client.getRedirectUris());
Set<String> clientScopes = commaDelimitedListToSet(client.getScopes());
Set<String> postLogoutUris = commaDelimitedListToSet(client.getPostLogoutRedirectUris());
RegisteredClient.Builder registeredClient = 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(postLogoutUris))
.scopes((scopes) -> scopes.addAll(clientScopes))
.clientSettings(client.getClientSettings())
.tokenSettings(client.getTokenSettings());
return registeredClient.build();
}
public 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(collectionToCommaDelimitedString(clientAuthenticationMethods));
entity.setAuthorizationGrantTypes(collectionToCommaDelimitedString(authorizationGrantTypes));
entity.setRedirectUris(collectionToCommaDelimitedString(registeredClient.getRedirectUris()));
entity.setPostLogoutRedirectUris(collectionToCommaDelimitedString(registeredClient.getPostLogoutRedirectUris()));
entity.setScopes(collectionToCommaDelimitedString(registeredClient.getScopes()));
entity.setClientSettings(registeredClient.getClientSettings());
entity.setTokenSettings(registeredClient.getTokenSettings());
return entity;
}
private AuthorizationGrantType resolveAuthorizationGrantType(String authorizationGrantType) {
if (AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) {
return AUTHORIZATION_CODE;
} else if (CLIENT_CREDENTIALS.getValue().equals(authorizationGrantType)) {
return CLIENT_CREDENTIALS;
} else if (REFRESH_TOKEN.getValue().equals(authorizationGrantType)) {
return REFRESH_TOKEN;
}
return new AuthorizationGrantType(authorizationGrantType);
}
private ClientAuthenticationMethod resolveClientAuthenticationMethod(String clientAuthenticationMethod) {
if (CLIENT_SECRET_BASIC.getValue().equals(clientAuthenticationMethod)) {
return CLIENT_SECRET_BASIC;
} else if (CLIENT_SECRET_POST.getValue().equals(clientAuthenticationMethod)) {
return CLIENT_SECRET_POST;
} else if (NONE.getValue().equals(clientAuthenticationMethod)) {
return NONE;
}
return new ClientAuthenticationMethod(clientAuthenticationMethod);
}
}
위에서 생성한 Client
엔티티와 RegisteredClient
는 다른 클래스이므로, 이 둘을 변환해줄 기능이 필요하다.
처음에는 ObjectMapper
를 사용했었는데 파싱이슈가 너무 많아서, 이렇게 직접 할당해주는 방식으로 변경하였다.
import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.stereotype.Component;
import study.springoauth2authserver.entity.Client;
import study.springoauth2authserver.util.ClientUtils;
@Component
@RequiredArgsConstructor
public class CustomRegisteredClientRepository implements RegisteredClientRepository {
private final ClientRepository clientRepository;
private final ClientUtils clientUtils;
@Override
public void save(RegisteredClient registeredClient) {
Client entity = clientUtils.toEntity(registeredClient);
clientRepository.save(entity);
}
@Override
public RegisteredClient findById(String id) {
Client client = clientRepository.findById(id).orElseThrow();
return clientUtils.toObject(client);
}
@Override
public RegisteredClient findByClientId(String clientId) {
Client client = clientRepository.findByClientId(clientId).orElseThrow();
return clientUtils.toObject(client);
}
}
RegisteredClientRepository
를 상속받아서 동일한 역할을 할 Repository클래스를 하나 만들어준다.
이 클래스를 Bean으로 등록하면, 이제 Jpa & MySQL을 통해서 Client를 관리할 수 있게 된다.
http://localhost:9000/oauth2/authorize?response_type=code&client_id=your-client&scope=openid&redirect_uri=http://localhost:3000
위 경로로 접속한 후, 로그인이 잘 되어서, 임시 코드값이 정상적으로 발급되는 것을 확인하였다.
위에서 발급받은 코드를 기반으로 토큰까지 잘 발급되었다.