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

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Engage with a sea of insights in this enlightening article, highly esteemed within the encouraging DEV Community. Programmers of every skill level are invited to participate and enrich our shared knowledge.

A simple "thank you" can uplift someone's spirits. Express your appreciation in the comments section!

On DEV, sharing knowledge smooths our journey and strengthens our community bonds. Found this useful? A brief thank you to the author can mean a lot.

Okay