DEV Community

NeNoVen
NeNoVen

Posted on

oauth-authorization-server

1. api-client , resource server

https://www.baeldung.com/spring-security-oauth-auth-server

baeldung gihub

https://github.com/Baeldung/spring-security-oauth/tree/master/oauth-authorization-server

2. api-client server 소스분석

2.1 yml 설정

server:
  port: 8080

spring:
  security:
    oauth2:
      client:
        registration:
          articles-client-oidc: #클라이언트 인증
            provider: spring
            client-id: articles-client
            client-secret: secret
            authorization-grant-type: authorization_code
            redirect-uri: "http://127.0.0.1:8080/login/oauth2/code/{registrationId}"
            scope: openid
            client-name: articles-client-oidc
          articles-client-authorization-code: #클라이언트 코드로 액세스 토큰 요청시 사용
            provider: spring
            client-id: articles-client
            client-secret: secret
            authorization-grant-type: authorization_code
            redirect-uri: "http://127.0.0.1:8080/authorized"
            scope: articles.read
            client-name: articles-client-authorization-code
        provider:
          spring:
            issuer-uri: http://auth-server:9000
Enter fullscreen mode Exit fullscreen mode

2.1.1 요청 응답 흐름 (시퀀스)

  1. 사용자 로그인 요청: 사용자가 애플리케이션에 로그인을 시도합니다.
  2. 리다이렉션: 클라이언트 애플리케이션은 사용자를 인증 서버의 로그인 페이지로 리다이렉션합니다. 이 때 client-id, redirect-uri, scope 등의 정보가 포함됩니다. 이 정보는 registration 섹션에서 정의됩니다.
  3. 사용자 인증: 사용자는 인증 서버에서 로그인을 진행합니다. 이 과정에서 issuer-uri (인증 서버 주소)가 사용됩니다.
  4. 인증 코드 발급: 로그인이 성공하면 인증 서버는 redirect-uri로 사용자를 다시 리다이렉션하며, 이 때 인증 코드를 함께 전달합니다.
  5. 인증 코드를 토큰으로 교환: 클라이언트 애플리케이션은 받은 인증 코드를 사용하여 인증 서버에 토큰을 요청합니다. 여기서 client-idclient-secret이 사용됩니다.
  6. 토큰 응답: 인증 서버는 클라이언트에게 액세스 토큰(및 필요시 리프레시 토큰)을 발급합니다.
  7. 리소스 서버 접근: 클라이언트는 받은 토큰을 사용하여 보호된 리소스에 액세스합니다. 예를 들어, scope: articles.read를 사용하여 특정 API에 접근할 수 있습니다.

2.1.2 설정 파일 내의 역할

  • client-idclient-secret: 클라이언트의 신원을 인증 서버에 증명하는데 사용됩니다.
  • redirect-uri: 사용자가 인증 후 리다이렉션될 URI입니다. 인증 코드를 받는 데 사용됩니다.
  • scope: 클라이언트가 요청할 수 있는 권한의 범위를 정의합니다.
  • issuer-uri: 인증 서버의 주소입니다. 사용자 인증 및 토큰 발급에 사용됩니다.

2.1.3 상세설명

  1. articles-client-oidc: 최초 클라이언트 인증
    • 이 설정은 사용자가 최초로 로그인할 때 사용됩니다.
    • 사용자가 애플리케이션에 로그인을 요청하면, 애플리케이션은 사용자를 OAuth 2.0 인증 서버의 로그인 페이지로 리다이렉션합니다.
    • articles-client-oidc 설정에 포함된 client-id, client-secret, redirect-uri, scope 등의 정보가 이 과정에서 사용됩니다.
    • 사용자가 인증 서버에서 성공적으로 인증을 완료하면, 인증 서버는 redirect-uri로 사용자를 리디렉션하고 인증 코드를 제공합니다.
  2. articles-client-authorization-code: 리소스 서버에 대한 요청
    • 인증 코드를 받은 후, 애플리케이션은 이 코드를 사용하여 인증 서버에 액세스 토큰을 요청합니다. 이 때 articles-client-authorization-code 설정이 사용됩니다.
    • client-id, client-secret는 이 과정에서 토큰 교환을 위해 사용되며, redirect-uri는 이전 단계에서 인증 코드를 받기 위해 사용된 주소입니다.
    • 애플리케이션이 액세스 토큰을 받으면, 이 토큰을 사용하여 보호된 리소스에 접근할 수 있습니다. 이 경우 scope: articles.read에 따라 특정 리소스에 대한 액세스 권한이 부여됩니다.

요약하자면, articles-client-oidc 설정은 사용자 인증을 위한 초기 로그인 과정에 사용되며, articles-client-authorization-code 설정은 인증된 사용자가 보호된 리소스에 접근하기 위해 필요한 액세스 토큰을 얻는 과정에 사용

2.2 소스

@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
          .authorizeRequests(authorizeRequests ->
            authorizeRequests.anyRequest().authenticated()
          )
          .oauth2Login(oauth2Login ->
            oauth2Login.loginPage("/oauth2/authorization/articles-client-oidc"))
          .oauth2Client(withDefaults());
        return http.build();
    }
}
Enter fullscreen mode Exit fullscreen mode

oauth2Login.loginPage("/oauth2/authorization/articles-client-oidc"))

해당 페이지는 클라이언트 서버에서 제공되는 페이지이며, security5에 제공되는 페이지로 사용된거.

@RestController
public class ArticlesController {

    private WebClient webClient;

    @GetMapping(value = "/articles")
    public String[] getArticles(
      @RegisteredOAuth2AuthorizedClient("articles-client-authorization-code") OAuth2AuthorizedClient authorizedClient
    ) {
        return this.webClient
          .get()
          .uri("http://127.0.0.1:8090/articles")
          .attributes(oauth2AuthorizedClient(authorizedClient))
          .retrieve()
          .bodyToMono(String[].class)
          .block();
    }
}
Enter fullscreen mode Exit fullscreen mode

OAuth2AuthorizedClient 클래스 형식의 요청에서 OAuth 인증 토큰을 가져옵니다 . 

이는 적절한 식별과 함께 @RegisterdOAuth2AuthorizedClient 주석을 사용하여 Spring에 의해 자동으로 바인딩됩니다 . 우리의 경우 이전에 .yml 파일 에서 구성한 article-client-authorizaiton-code 에서 가져왔습니다

3. 테스트 호출

브라우저에서 호출 http://127.0.0.1:8080/articles

인증 url 로 리다이렉션 http://auth-server:9000/login

4. 리소스 서버

보안구성 - 인증서버 설정

인증서버에 설정한 ProviderSettings 값을 issuer-uri 로 설정

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://auth-server:9000
Enter fullscreen mode Exit fullscreen mode

웹보안 구성 설정 : 리소스에 대한 권한 설정( 읽기, 모든 요청에 대한 승인요청)

@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.mvcMatcher("/articles/**")
          .authorizeRequests() //1.요청에 대한 인증을 시작합니다. 이 단계에서는 이후에 정의될 인증 규칙들이 적용됩니다
          .mvcMatchers("/articles/**")//2.특정 경로(/articles/**)에 대한 추가적인 규칙을 정의. 
                                                                            // 해당 경로에 대한 보안 규칙을 세분화
          .access("hasAuthority('SCOPE_articles.read')")//3./articles/** 경로에 대한 접근을 제어
                    // 'SCOPE_articles.read' 권한을 가지고 있어야만 해당 경로에 접근 가능
                    // 이 권한은 일반적으로 OAuth 2.0 토큰에 정의된 스코프에서 파생됩니다.
          .and()
          .oauth2ResourceServer() //3. OAuth 2.0 리소스 서버를 활성화하고 JWT (JSON Web Token)를 사용하여 인증을 수행
          .jwt(); //  들어오는 요청이 올바른 JWT를 포함하고 있는지 검증하여 해당 요청이 유효한지 확인
        return http.build();
    }
}
Enter fullscreen mode Exit fullscreen mode

5. 인증서버

@Configuration
@Import(OAuth2AuthorizationServerConfiguration.class)
public class AuthorizationServerConfig {
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
          .clientId("articles-client")
          .clientSecret("{noop}secret")
          .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
          .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
          .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
          .redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc")
          .redirectUri("http://127.0.0.1:8080/authorized")
          .scope(OidcScopes.OPENID)
          .scope("articles.read")
          .build();
        return new InMemoryRegisteredClientRepository(registeredClient);
    }
}
Enter fullscreen mode Exit fullscreen mode
  • RegisteredClient.withId(UUID.randomUUID().toString()): 클라이언트 ID를 생성합니다. 여기서는 고유한 UUID를 사용합니다.
  • .clientId("articles-client"): OAuth 클라이언트의 식별자로 "articles-client"를 지정합니다. Spring은 이를 사용하여 리소스에 액세스하려는 클라이언트를 식별합니다.
  • .clientSecret("{noop}secret"): 클라이언트의 비밀번호를 설정합니다. {noop}은 패스워드 인코더가 사용되지 않음을 의미합니다.
  • .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC): 클라이언트 인증 방식을 CLIENT_SECRET_BASIC으로 설정합니다. 이는 클라이언트 ID와 비밀번호를 사용하여 인증을 수행하는 방식입니다.
  • .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) (인증 부여 유형): 인증 코드 부여 유형을 지원합니다. 이는 OAuth 2.0의 표준 플로우 중 하나입니다.
  • .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) (인증 부여 유형): 리프레시 토큰 부여 유형을 지원합니다. 이를 통해 클라이언트는 액세스 토큰을 갱신할 수 있습니다.
  • .redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc"): OAuth 인증 후 사용자를 리디렉션할 URI를 설정합니다.
  • .redirectUri("http://127.0.0.1:8080/authorized"): 추가적인 리디렉션 URI를 설정합니다.
  • .scope(OidcScopes.OPENID): OpenID Connect 스코프를 사용합니다.
  • .scope("articles.read"): 사용자 정의 스코프인 "articles.read"를 설정합니다. 이 스코프는 클라이언트가 특정 자원에 접근할 수 있도록 허용합니다.
  • .build(): RegisteredClient 구성을 완성합니다.

5.1 상세

.redirectUri 설정에 대해 설명드리겠습니다. 이 설정은 OAuth 2.0 인증 프로세스에서 매우 중요한 부분입니다.

5.1.1. 인증 코드 흐름 (Authorization Code Flow):
- .redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc"): 이 리디렉션 URI는 일반적으로 인증 코드 흐름에 사용됩니다. 사용자가 OAuth 인증을 시작하면, 사용자는 이 URI로 리디렉션됩니다. 여기서 사용자는 로그인하고 동의를 제공한 후, 인증 서버는 이 URI로 인증 코드를 리디렉션합니다. 이 인증 코드는 나중에 액세스 토큰을 얻는 데 사용됩니다.
5.1.2. 액세스 토큰 요청:
- .redirectUri("http://127.0.0.1:8080/authorized"): 이 URI는 액세스 토큰을 받기 위한 추가적인 리디렉션 포인트로 사용될 수 있습니다. 사용자가 인증 코드를 사용하여 액세스 토큰을 요청할 때, 인증 서버는 이 URI로 사용자를 리디렉션할 수 있습니다.

리디렉션 URI의 순서는 일반적으로 중요하지 않습니다. 중요한 것은 클라이언트 애플리케이션이 이 URI들 중 하나를 사용하여 인증 서버에 리디렉션을 요청하는 것입니다. 클라이언트 애플리케이션이 인증 요청 시에 사용하는 redirect_uri 매개변수의 값은 등록된 리디렉션 URI 중 하나와 정확히 일치해야 합니다.

요약하자면, .redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc")는 사용자가 로그인하고 인증 서버가 인증 코드를 반환하는 데 사용되며, .redirectUri("http://127.0.0.1:8080/authorized")는 추가적인 리디렉션 옵션으로, 특정 상황에서 사용될 수 있습니다. 이 두 URI는 인증 프로세스의 다른 단계에서 사용되며, 그 순서는 특별한 의미를 가지지 않습니다.

5.2 구성

기본 OAuth 보안을 적용하고 기본 양식 로그인 페이지를 생성하도록 Bean을 구성

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    return http.formLogin(Customizer.withDefaults()).build();
}
Enter fullscreen mode Exit fullscreen mode

각 인증 서버에는 보안 도메인 간의 적절한 경계를 유지하기 위해 토큰에 대한 서명 키가 필요합니다. 2048바이트 RSA 키를 생성해 보겠습니다.

@Bean
public JWKSource<SecurityContext> jwkSource() {
    RSAKey rsaKey = generateRsa();
    JWKSet jwkSet = new JWKSet(rsaKey);
    return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

private static RSAKey generateRsa() {
    KeyPair keyPair = generateRsaKey();
    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
    return new RSAKey.Builder(publicKey)
      .privateKey(privateKey)
      .keyID(UUID.randomUUID().toString())
      .build();
}

private static KeyPair generateRsaKey() {
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    keyPairGenerator.initialize(2048);
    return keyPairGenerator.generateKeyPair();
}
Enter fullscreen mode Exit fullscreen mode

서명 키를 제외하고 각 인증 서버에는 고유한 발급자 URL도 있어야 합니다. ProviderSettings 빈을 생성하여 포트 9000 에서 http://auth-server 에 대한 localhost 별칭으로 설정하겠습니다 .

@Bean
public ProviderSettings providerSettings() {
    return ProviderSettings.builder()
      .issuer("http://auth-server:9000")
      .build();
}
Enter fullscreen mode Exit fullscreen mode

또한 /etc/hosts 파일에 " 127.0.0.1 auth-server " 항목을 추가하겠습니다 . 이를 통해 로컬 컴퓨터에서 클라이언트와 인증 서버를 실행할 수 있으며 둘 사이의 세션 쿠키 덮어쓰기 문제를 피할 수 있습니다.

그런 다음 @EnableWebSecurity 주석이 달린 구성 클래스 를 사용하여 Spring 웹 보안 모듈을 활성화합니다

@EnableWebSecurity
public class DefaultSecurityConfig {

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(authorizeRequests ->
          authorizeRequests.anyRequest().authenticated()
        )
          .formLogin(withDefaults());
        return http.build();
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode
  • 모든 요청에 대해 인증을 요구하기 위해 AuthorizeRequests.anyRequest().authenticated()를 호출
  • formLogin(defaults()) 메서드 를 호출하여 양식 기반 인증을 제공하고

테스트에 사용할 예제 사용자 집합을 정의

@Bean
UserDetailsService users() {
    UserDetails user = User.withDefaultPasswordEncoder()
      .username("admin")
      .password("password")
      .build();
    return new InMemoryUserDetailsManager(user);
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)