스프링 보안 새 인증 서버(0.3.1) - 3부
먼저 엔터티부터 시작하겠습니다.
RegisteredClient
에서 사용하는 라이브러리에서 제공하는 RegisteredClientRepository
클래스를 검사하여 엔터티를 모델링하는 방법을 알 수 있습니다.다음은
RegisteredClient
클래스의 필드입니다.이를 기반으로 클라이언트 엔터티를 원하는 대로 모델링할 수 있지만 결국에는 클라이언트 엔터티에서
RegisteredClient
를 구성할 수 있어야 합니다. 우리는 UserDetails
에 대한 인터페이스가 없기 때문에 단순히 인터페이스를 구현하고 이 엔터티가 RegisteredClient
라고 말할 수 없지만 대신 내가 한 것은 RegisteredClient
를 빌드하는 팩토리 클래스를 제공하는 것입니다. 엔티티에서.클라이언트 엔터티:
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Client {
@Id
private String id;
private String secret;
@ElementCollection(fetch = FetchType.EAGER)
private Set<AuthorizationGrantType> grantTypes;
private ClientAuthenticationMethod authenticationMethod;
@OneToMany(mappedBy = "client", fetch = FetchType.EAGER)
private Set<ClientRedirectUrl> redirectUris;
@ManyToMany(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER)
@JoinTable(name = "client_scope_mapping",
joinColumns = @JoinColumn(name = "client_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "scope_id", referencedColumnName = "id")
)
private Collection<ClientScope> scopes;
public Client(CreateClientRequest request) {
this.id = request.getId();
this.secret = request.getSecret();
this.grantTypes = request.getGrantTypes();
this.authenticationMethod = request.getAuthenticationMethod();
}
public static RegisteredClient toRegisteredClient(Client client) {
RegisteredClient.Builder builder = RegisteredClient.withId(client.getId())
.clientId(client.getId())
.clientSecret(client.getSecret())
.clientAuthenticationMethod(client.getAuthenticationMethod())
.authorizationGrantTypes(
authorizationGrantTypes -> authorizationGrantTypes.addAll(client.getGrantTypes()))
.redirectUris(
redirectUris -> redirectUris.addAll(client.getRedirectUris()
.stream()
.map(ClientRedirectUrl::getUrl)
.collect(Collectors.toSet())))
.scopes(scopes -> scopes.addAll(client.getScopes()
.stream()
.map(ClientScope::getScope)
.collect(Collectors.toSet())));
return builder.build();
}
}
각 클라이언트에 대한 보조금 유형을 데이터베이스에
@ElementCollection
로 직렬화된 AuthorizationGrantType
세트를 저장하는 tinyblob
로 저장하기로 결정했습니다. 사용 가능한 모든 보조금 유형을 보관할 별도의 테이블을 갖도록 선택할 수 있습니다. 그런 다음 각 클라이언트에 필요한 것을 조인하면 조인 테이블도 필요할 것입니다.그런 다음 redirectUris(
ClientRedirectUrl
)가 있는 @OneToMany와 범위(ClientScope
)가 있는 @ManyToMany가 있습니다.ClientRedirectUrl 엔티티:
@Entity
@Getter
@NoArgsConstructor
@Table(name = "client_redirect_uri")
public class ClientRedirectUrl {
@Id
private String id;
private String url;
@Setter
@ManyToOne
@JoinColumn(name = "client_id", referencedColumnName = "id")
private Client client;
public ClientRedirectUrl(String url, Client client) {
this.id = RandomStringUtils.randomAlphanumeric(10);
this.url = url;
this.client = client;
}
}
ClientScope 엔터티:
@Entity
@Getter
@NoArgsConstructor
@Setter
@Table(name = "client_scope")
public class ClientScope {
@Id
private String id;
private String scope;
public ClientScope(String scope) {
this.id = RandomStringUtils.randomAlphanumeric(10);
this.scope = scope;
}
}
이것은 최종 스키마 다이어그램입니다.
다음으로, 첫 번째 파트에서 생성한 Oauth2Config 클래스에서 메모리 내 클라이언트 리포지토리를 제공하는 빈을 제거해야 합니다. 대신
RegisteredClientRepository
를 구현할 @Service 를 제공할 것입니다.고객 서비스:
@Service
@RequiredArgsConstructor
public class ClientService implements RegisteredClientRepository {
private final ClientRepository clientRepository;
private final ClientRedirectUrlRepository clientRedirectUrlRepository;
private final ClientScopeRepository clientScopeRepository;
@Override
public void save(RegisteredClient registeredClient) {
}
@Override
public RegisteredClient findById(String id) {
var client = this.clientRepository.findById(id).orElseThrow();
return Client.toRegisteredClient(client);
}
@Override
public RegisteredClient findByClientId(String clientId) {
var client = this.clientRepository.findById(clientId).orElseThrow();
return Client.toRegisteredClient(client);
}
public ClientResponse createClient(CreateClientRequest request) {
var scopes = request.getScopes().stream().map(ClientScope::new).collect(Collectors.toSet());
scopes.forEach(this.clientScopeRepository::save);
var client = new Client(request);
client.setScopes(scopes);
this.clientRepository.save(client);
client.setRedirectUris(request.getRedirectUris().stream()
.map(url -> new ClientRedirectUrl(url, client))
.collect(Collectors.toSet()));
client.getRedirectUris().forEach(this.clientRedirectUrlRepository::save);
return new ClientResponse(client);
}
}
이제 Oauth2Config 클래스는 다음과 같습니다.
@Configuration
@RequiredArgsConstructor
public class Oauth2Config {
private final ClientService clientService;
@Bean
@Order(HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http
// Redirect to the login page when not authenticated from the
// authorization endpoint
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login"))
);
return http.build();
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder().build();
}
}
또한 새 클라이언트를 생성하기 위한 엔드포인트를 생성하고 기본 구성에서 이에 대한 요청도 허용해야 했기 때문에 이제 다음과 같이 보입니다.
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final UserService userService;
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeRequests()
.antMatchers("/users/**").permitAll()
.antMatchers("/clients/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf().ignoringAntMatchers("/users/**", "/clients/**")
.and()
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
var provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userService);
provider.setPasswordEncoder(NoOpPasswordEncoder.getInstance()); //temporary
return new ProviderManager(provider);
}
@Bean
public UserDetailsService userDetailsService() {
return this.userService;
}
}
필자의 경우처럼 와일드카드에 대해 permitAll을 허용하는 것은 좋은 생각이 아닐 수 있지만 다시 필요에 따라 수정할 수 있습니다.
인증을 다시 테스트해 보겠습니다. 먼저 클라이언트를 생성합니다.
그런 다음 사용자를 만듭니다.
그런 다음 권한을 부여합니다.
http://localhost:8080/oauth2/authorize?response_type=code&client_id=client&scope=openid&redirect_uri=http://spring.io&code_challenge=YHRCg0i58poWtvPg_xiSHFBqCahjxCqTyUbhYTAk5ME&code_challenge_method=S256
자신의 code_challenge를 사용하려면 검증자가 필요합니다.
토큰 받기:
작동합니다. 인증 매처 구성, 비밀번호 인코더 또는 인코더 부족과 같이 변경해야 할 몇 가지 분명한 사항이 있지만 요점을 알 수 있습니다.
당신은 전체 코드here를 찾을 수 있지만, 오픈 소스 버전을 만들려고 할 것이기 때문에 나중에 변경될 것이라는 공정한 경고가 있습니다.
Reference
이 문제에 관하여(스프링 보안 새 인증 서버(0.3.1) - 3부), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/nerminkarapandzic/spring-security-new-authorization-server-031-part-3-3331텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)