web/SpringBoot

[SpringBoot] OAuth2.1 모듈로 인증 서버 만들기 - 1

뽀리님 2024. 8. 28. 17:50

회사에서 기존 모듈 분석을 하다가, OAuth2 모듈이 있길래 정리해보는 블로깅..

 

나는 사실 OAuth2 의 개념을 명확하겐 알지 못했다, 그냥 JWT 를 발급해주는 모듈이구나 정도로 이해하는 수준? ㅋㅋ

 

이제서야 개념을 정리해본다.

 

✔️ OAuth?

OAuth(”Open Authorization”)는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로 사용되는, 접근 위임을 위한 개방형 표준이다.

 

사용자가 자신의 계정 정보를 공유하지 않고, 다른 서비스에 대한 접근 권한을 안전하게 제공하는 서비스
OAuth를 지원하는 서비스는 사용자로부터 인증을 받은 후, 해당 서비스에 대한 접근 권한을 받는다. 
 
OAuth를 한 마디로 요약해 보자면,

‘인증은 유저가 직접, 권한은 서비스에게’

 

즉 Third-Party 프로그램 에게 리소스 소유자를 대신하여 리소스 서버에서 제공하는 자원에 대한 접근 권한을 위임하는 방식을 제공한다. 예를들어 구글, 페이스북, 카카오, 네이버 등에서 제공하는 간편 로그인 기능도 OAuth2 프로토콜 기반의 사용자 인증 기능을 제공하는 대표적인 예다.

 

✔️ OAuth2?


OAuth(Open Authorization) 2.0는 HTTP 기반의 인증을 위한 업계 표준 프로토콜이다.
이러한 OAuth 2.0은 RFC-6749의 문서로 표준으로 사용되고 있다. 모든 OAuth 2.0 구현은 해당 문서 스펙의 Role과 Flow를 기반으로한다.

 

✔️ 역할

이름 설명
Resource Owner 리소스 소유자입니다. 본인의 정보에 접근할 수 있는 자격을 승인하는 주체입니다. 예시로 구글 로그인을 할 사용자를 말합니다. Resource Owner는 클라이언트를 인증(Authorize)하는 역할을 수행하고, 인증이 완료되면 동의를 통해 권한 획득 자격(Authorization Grant)을 클라이언트에게 부여합니다.
Client Resource Owner의 리소스를 사용하고자 접근 요청을 하는 어플리케이션 입니다.
Resource Server Resource Owner의 정보가 저장되어 있는 서버입니다.
Authorization Server 권한 서버입니다. 인증/인가를 수행하는 서버로 클라이언트의 접근 자격을 확인하고 
Access Token을 발급하여 권한을 부여하는 역할을 수행합니다.

 

 

 

참조 https://sabarada.tistory.com/248

 

 

 

그림으로 보면 대략적으로 이렇다. 사실 처음에 뭐가 뭔지 잘 몰랐는데 한마디로 축약하자면

OAuth2 인증 모듈은 클라이언트 애플리케이션이 안전하게 사용자 자격 증명을 직접 다루지 않고도, 제3자 서버에 접근할 수 있도록 허용하는 표준화된 권한 부여 프레임워크.

 

라고 생각하면 되겠다.

 

 

 

✔️ OAuth2.0 지원되는 endpoint

 

 

1. Authorization

GET /oauth2/authorize

 

 

클라이언트 애플리케이션이 사용자를 인증하고 권한을 요청하는 엔드포인트. 이 엔드포인트를 통해 클라이언트는 사용자의 동의를 받아 Authorization Code를 얻는다.

 

  • 주요 파라미터
  • response_type: code (Authorization Code Grant), token (Implicit Grant)
  • client_id: 클라이언트 애플리케이션의 ID
  • redirect_uri: 인증 후 리디렉션할 URI
  • scope: 요청하는 권한의 범위
  • state: CSRF 공격을 방지하기 위한 파라미터


2. Token 

POST /oauth2/token


클라이언트 애플리케이션이 Authorization Code를 교환하여 액세스 토큰을 발급받거나, 다른 Grant Type을 통해 토큰을 직접 발급받는 엔드포인트

  • 주요파라미터
  • grant_type: authorization_code, client_credentials, password, refresh_token 등이 지원됨
  • code: Authorization Code (Authorization Code Grant)
  • redirect_uri: Authorization Code를 획득할 때 사용된 리디렉션 URI
  • client_id: 클라이언트 애플리케이션의 ID
  • client_secret: 클라이언트 시크릿 (클라이언트 인증 시 사용)
  • username, password: Resource Owner Password Credentials Grant에서 사용
  • refresh_token: 리프레시 토큰을 통한 액세스 토큰 갱신

 

3. Token Revocation

POST /oauth2/revoke

 

클라이언트가 발급된 액세스 토큰이나 리프레시 토큰을 무효화하는 데 사용하는 엔드포인트

주요 파라미터:

  • token: 취소할 토큰 (액세스 토큰 또는 리프레시 토큰)
  • token_type_hint: access_token 또는 refresh_token 중 하나

4. Token Introspection

POST /oauth2/ introspect

 

리소스 서버 또는 클라이언트가 토큰의 유효성을 확인하기 위해 사용되는 엔드포인트. 이 엔드포인트는 토큰의 상태와 메타데이터를 반환

주요 파라미터:

  • token: 유효성을 확인할 토큰
  • token_type_hint: access_token 또는 refresh_token 중 하나

 

5. UserInfo Endpoint

GET /oauth2/userinfo

POST /oauth2/userinfo
Header : Authorization: Bearer

액세스 토큰을 사용하여 인증된 사용자의 프로필 정보를 가져오는 엔드포인트. 주로 OpenID Connect(OIDC)와 함께 사용됨

6. JWK Set Endpoint

GET /oauth2/userinfo
URL: /jwks


JWT (Json Web Token)의 서명을 검증하기 위해 공개 키를 제공하는 엔드포인트. 클라이언트나 리소스 서버는 이 엔드포인트를 사용하여 토큰 서명을 검증

 

7.Authorization Server Metadata Endpoint

GET oauth2/.well-known/oauth-authorization-server


인증 서버의 메타데이터를 제공하는 엔드포인트로, 인증 서버에 대한 설정과 지원되는 기능들을 제공. OpenID Connect에서는 /.well-known/openid-configuration 엔드포인트가 사용됨

 

 

 

✔️ OAuth2.0 승인 방식 종류


1. Authorization Code Grant (권한 부여 승인 방식)

: 클라이언트가 다른 사용자 대신 특정 리소스에 접근을 요청할 때 사용. 리소스 접근을 위한 사용자 명과 비밀번호, 권한 서버에 요청해서 받은 권한 코드를 함께 사용해 리소스에 대한 Access Token을 받는 방식.

 


2. Implicit Grant (암묵적 승인 방식)

: 권한 부여 코드 승인 방식과 유사하지만 차이점이 권한 코드 교환 단계 없이 Access Token을 즉시 반환해 이를 인증에 이용하는 방식.

 


3. Resource Owner Password Credentials Grant (자원 소유자 자격 증명 승인 방식)

: 클라이언트가 사용자 이름과 암호를 직접 Authorization Server에 전달하여 Access Token에 대한 사용자의 자격 증명을 교환하는 방식.

 


4. Client Credentials Grant (클라이언트 자격 증명 승인 방식)

: 클라이언트가 컨텍스트 외부에서 Access Token을 얻어 특정 리소스에 접근을 요청할 때 사용하는 방식

 

 

기존 모듈은 Resource Owner Password Credentials Grant 로 되어있었고, 이 방식만 좀 더 설명하자면, 프로세스는 대략 이렇다.

참고 https://zibro.tistory.com/8

1) 인증을 진행함. Username, Password를 통해서 자격 증명이 진행
2) 넘겨받은 정보 기반으로 권한 서버에 Access Token 정보를 요청
3) Access Token 정보를 응답 받음. 이때 Refresh Token 정보도 넘겨줄 수 있음
4) Access Token 기반으로 Resource Server와 통신

 

 

 

Postman을 통한 인증 진행 (/oauth/token)

 

이미지에는 나와있지 않지만 Header 값에 Authorization 을 추가해줘야한다(Client ID + Client Secret 을 Base64 인코딩한 값 ex.Basic ..)

 

출력결과

{
    "access_token": "...",
    "token_type": "bearer",
    "refresh_token": "...",
    "expires_in": 43199,
    "scope": "openid",
    "passwordModifiedDate": "",
    "eid": 100021,
    "reverseGraphs": [],
    "cno": "",
    "organization": "",
    "eno": "1234",
    "name": "테스트",
    "rid": 1001,
    "cid": 0,
    "jti": "6664b966-9541-4529-8d28-2e2f119a793c"
}

 

 

BUT
이번에 새롭게 인증 모듈을 만들면서 2.1로 업그레이드 하여 만들어보고자 한다.



 

 

✔️ OAuth 2.1?

OAuth 2.1은 The OAuth 2.0 Authorization Framework - RFC6749에 명시된 OAuth 2.0 스펙을 보안적으로 더욱 개선시킨 버전이다.

 

이번에 새롭게 만드는 OAuth2.1 모듈에서는  Implicit Grant 방식과  Resource Owner Password Credentials Grant 방식은 보안 취약성으로 인해 Decreated 되었다.

 

 

  • Authorization Code Grant with PKCE 사용

OAuth 2.1에서는 Authorization Code Grant와 PKCE를 사용하는 것이 권장된다. 


  • 대체 인증 흐름으로 전환

가능하면 password 그랜트 타입을 사용하는 대신, OAuth 2.1에서 권장하는 다른 인증 흐름으로 전환하는 것이 좋다. 예를 들어, 사용자에게 Authorization Code Grant with PKCE를 통해 인증하도록 유도하는 것이 보안적으로 더 안전한 방법이다.

 

 

그래서 이번엔 OAuth2.1 로 인증서버를 만들어보고자 한다.

 

우리가 흔히 카카오나, 네이버,구글 페이스북같은 SNS 인증을 할때 쓰는게 OAuth 인증이다.

대표적으로 카카오를 예를 들어보면 다음과 같이 인증을 한다.

참조 https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api

 

 

 

일단 내가 필요한 기능이다. 카카오랑 비슷하게 가려고 한다.

리소스(서비스) 서버와 인증서버를 같이 가져갈지, 따로 갈지는 고민중인데 여기서는 일단 한 곳에서 처리하는걸로 가정하고 구현해보겠다.

 

  • ID,PW 입력 → 사용자인증 (/login)
  • 등록된 Application(Client)인지 확인 → 인가코드 발급(/oauth2/authorize)
  • 발급된 인가코드로 Access_token 토큰발급 (/oauth2/token)

 

 

참조 https://velog.io/@namhm23/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95

 

 

 

 

 

이 게시물에 앞서서 예전에 RestFul API 를 구현하기 위해  SpringBoot Security + JWT 토큰을 이용하여 처리를 정리했었다.

https://ssmyefrin.tistory.com/14

 

Spring Boot 3.1.x 으로 RestFul API 서버 만들기(2)

지난 시간에 이어 오늘은 JWT 생성과, Spring security 필터 설정을 간단히 해보겠다. 일단 JWT를 알아보기전에 익혀두어야 할 개념이 있다. (아랫글 참조) https://ssmyefrin.tistory.com/11 HTTP 인증에 대한 처

ssmyefrin.tistory.com

 

사실 사용자들이 ID/PW로 로그인하도록 구현하는 경우 이 방식을써도 딱히 상관이 없다.

OAuth2 는 주로 타사 애플리케이션이 리소스 소유자의 자원에 접근할 수 있도록 허가를 부여하는 업계 표준 프로토콜이며. 주로 리소스 서버와 클라이언트 간의 액세스 권한을 처리하는 데 사용되기 때문이다.

 

하지만 기존(asis)에 인증서버가 OAuth2로 되어있기도 했고,

사용자들에게 제공되는 백오피스 애플리케이션이 OAuth2 기반 인증/인가 시스템과 통합되어 

@EnableResourceServer 리소스 서버로 설정되어있어

OAuth 방식으로 구현해보려 한다.(겸사겸사 공부한단 생각으로....) 

 

 

 

 

✔️ OAuth2.1을 쓰기위해선 최신 스프링 버전이 필요하다.

참고로 해당 게시물에서 나는 SpringBoot 버전을 3.3.x 으로 업그레이드 하고 JDK도 21로 변경하였다.

 

여기서 OAuth2 서버로 만들기위해 Gradle에 의존성을 추가해준다.

인증서버과 리소스 서버를 동일한곳에서 처리해주기 위해 한꺼번에 추가하였다.

implementation 'org.springframework.boot:spring-boot-starter-oauth2-authorization-server'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.security:spring-security-oauth2-client'

 

그리고 Security Config에 추가를 해준다.

@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
    // OAuth2 보안구성 정의
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(httpSecurity);
    
    httpSecurity.httpBasic(HttpBasicConfigurer::disable)
                .csrf(CsrfConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable);
    httpSecurity.oauth2ResourceServer((oauth2) -> {
        oauth2.jwt(Customizer.withDefaults());
    })
    // 리소스 서버 설정
    .authorizeHttpRequests( authorize -> authorize.requestMatchers(AUTH_WHITELIST).permitAll()
                                        .anyRequest().authenticated())
    .exceptionHandling(c -> c.authenticationEntryPoint(entryPoint));
    return httpSecurity.build();


}

 

하지만 이렇게 구현하면 Error가 난다.

 

Unsatisfied dependency expressed through method 'setFilterChains' parameter 0: Error creating bean with name 'filterChain' defined in class path resource [com/denall/tv/config/SecurityConfig.class]: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'filterChain' threw exception with message: Can't configure mvcMatchers after anyRequest

 

이 경우 단일 SecurityFilterChain이 모든 요청을 처리한다. 만약 이 체인에서 여러 설정이 충돌하거나 우선순위가 잘못 설정된 경우, 예기치 않은 동작을 초래할 수 있다.

 

➤ 여러 개의 SecurityFilterChain 

Spring Security는 여러 개의 SecurityFilterChain을 정의할 수 있다, 각 체인은 서로 다른 URL 패턴에 대해 적용될 수 있다. Spring Security는 요청 URL에 따라 어떤 SecurityFilterChain이 적용될지를 결정한다.

 

➤ 체인 우선순위

기본적으로 SecurityFilterChain은 순서대로 평가되며, 첫 번째로 일치하는 체인이 적용된다. 여러 체인이 겹치는 경우, 우선순위에 따라 체인이 선택된다.

각각의 체인은 서로 다른 목적을 가지고 있을 수 있으며, URL 패턴이나 다른 조건에 따라 특정 요청에 대해 특정 체인이 적용된다.

 

 

따라서 각 SecurityFilterChain의 역할을 명확하게 할 필요가 있단 얘기다.

하나는 OAuth2 보안구성 정의, 또 하나는 리소스 서버 구성 정의와 같이 서로 다른 요청을 처리하도록 설정하였다.

 

/**
 * OAuth2 보안구성 정의
 * @param httpSecurity
 * @return
 * @throws Exception
 */
@Bean
public SecurityFilterChain authorizeFilterChain(HttpSecurity httpSecurity) throws Exception {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(httpSecurity);
    return httpSecurity.build();
}

/**
 * Resource Server 설정
 * @param httpSecurity
 * @return
 * @throws Exception
 */
@Bean
public SecurityFilterChain resourceFilterChain(HttpSecurity httpSecurity) throws Exception {
    return httpSecurity.csrf(csrf -> csrf.disable())
                       .httpBasic(basic -> basic.disable())
                       .authorizeHttpRequests(authorize -> authorize.requestMatchers(AUTH_WHITELIST).permitAll()
                                                            .anyRequest().authenticated())
            .oauth2ResourceServer((oauth2) -> {
                oauth2.jwt(Customizer.withDefaults()); // jwt 검증부분(꼭 리소스에 추가해야한다 아니면 기본적으로 토큰검증이 되지않음.)
            })
            /*.formLogin(form -> form
                    .loginPage("/login")  // 사용자 정의 로그인 페이지 경로
                    .successHandler(customAuthenticationSuccessHandler)
                    .permitAll()
            )*/
            .exceptionHandling(c -> c.authenticationEntryPoint(entryPoint))
            .build();

}

 

여기서 중요한 부분은 

OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();

 

이부분 이다 .

OAuth2 인증 서버의 다양한 설정을 정의할 수 있다.

공식문서에 잘나와있다.

(https://docs.spring.io/spring-authorization-server/reference/configuration-model.html)

 

@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
    // OAuth2AuthorizationServerConfigurer 생성
    OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();

    // HttpSecurity에 Authorization Server Configurer를 적용
    http.apply(authorizationServerConfigurer);

    // Authorization Server 구성 설정
    authorizationServerConfigurer
            .registeredClientRepository(registeredClientRepository)       // 등록된 클라이언트 저장소 설정
            .authorizationService(authorizationService)                   // 인증 서비스 설정
            .authorizationConsentService(authorizationConsentService)     // 인증 동의 서비스 설정
            .authorizationServerSettings(authorizationServerSettings)     // 인증 서버 설정
            .tokenGenerator(tokenGenerator)                               // 토큰 생성기 설정
            .clientAuthentication(clientAuthentication -> { })            // 클라이언트 인증 설정
            .authorizationEndpoint(authorizationEndpoint -> { })          // 인증 엔드포인트 설정
            .deviceAuthorizationEndpoint(deviceAuthorizationEndpoint -> { }) // 디바이스 인증 엔드포인트 설정
            .deviceVerificationEndpoint(deviceVerificationEndpoint -> { })   // 디바이스 검증 엔드포인트 설정
            .tokenEndpoint(tokenEndpoint -> { })                          // 토큰 엔드포인트 설정
            .tokenIntrospectionEndpoint(tokenIntrospectionEndpoint -> { }) // 토큰 인트로스펙션 엔드포인트 설정
            .tokenRevocationEndpoint(tokenRevocationEndpoint -> { })      // 토큰 취소 엔드포인트 설정
            .authorizationServerMetadataEndpoint(authorizationServerMetadataEndpoint -> { }) // 인증 서버 메타데이터 엔드포인트 설정
            .oidc(oidc -> oidc                                            // OpenID Connect 설정
                    .providerConfigurationEndpoint(providerConfigurationEndpoint -> { }) // OIDC 프로바이더 설정 엔드포인트 설정
                    .logoutEndpoint(logoutEndpoint -> { })                       // 로그아웃 엔드포인트 설정
                    .userInfoEndpoint(userInfoEndpoint -> { })                   // 사용자 정보 엔드포인트 설정
                    .clientRegistrationEndpoint(clientRegistrationEndpoint -> { }) // 클라이언트 등록 엔드포인트 설정
            );

    // SecurityFilterChain 객체를 빌드하고 반환
    return http.build();
}

 

대략 이런방식으로 OAuth2 설정을 해줄 수 있다.

 

나는 여기서 토큰생성기와, 클라이언트 인증 설정을 따로 Custom 해줄 것이다.

 

 

CustomRegisteredClientRepository 

나는 클라이언트 저장소 설정을 따로 커스텀 하기 위해 CustomRegisteredClientRepository 컴포넌트를 만들었다.

 

그전에 RegisteredClient 개념부터 짚고 넘어가자.

 

RegisteredClient??
인가 서버에 등록된 클라이언트를 의미한다.
OAuth2.0 또는 OAuth2.1 을 통해 인증 및 권한 부여를 요청하는 클라이언트를 구성하는데 사용된다.
예를 들어, 클라이언트가 authorization_code 또는 client_credentials 와 같은 권한 부여 흐름을 시작하려면 먼저 클라이언트를 권한 부여 서버에 등록해야한다.
클라이언트 등록시 클라이언트는 고유한 client_id, client_secret 및 고유한 클라이언트 식별자와 연결된 메타 데이터를 할당한다.
클라이언트의 주요 목적은 보호된 리소스에 대한 액세스를 요청하는 것으로 클라이언트는 먼저 권한 부여 서버를 인증하고 액세스 토큰과 교환을 요청한다.

 

참고 https://bin-repository.tistory.com/168

 

 

▸ 구성요소

 

  • 클라이언트 ID와 비밀 키 (Client ID and Secret): 클라이언트 애플리케이션이 등록되면 고유한 client_id와 보통 client_secret도 함께 등록한다. 이 자격 증명은 클라이언트를 인증 서버에 인증하는 데 사용된다.
  • 리다이렉트 URI (Redirect URIs): 클라이언트는 인증 서버에 하나 이상의 리다이렉트 URI를 등록해야 한다. 이 URI는 사용자가 권한을 부여한 후 인증 코드나 액세스 토큰을 클라이언트에게 반환하는 데 사용된다.
  • 그랜트 타입 (Grant Types): 클라이언트 특정 그랜트 타입 (예: Authorization Code, Implicit, Client Credentials 등)
  • 스코프 (Scopes): 클라이언트 리소스 서버에 대한 접근 수준을 정의
  • 클라이언트 메타데이터 (Client Metadata): 클라이언트의 이름, 설명, 로고와 같은 추가 메타데이터 등록 포함

 

 

  Client 를 DB로 관리하기위해 테이블을 생성

 

1. Client 테이블 Create

CREATE TABLE `oauth2_client` (

`id` varchar(36) COLLATE utf8_bin NOT NULL,

`client_id` varchar(255) COLLATE utf8_bin NOT NULL,

`client_id_issued_at` timestamp NULL DEFAULT NULL,

`client_secret` varchar(255) COLLATE utf8_bin DEFAULT NULL,

`client_secret_expires_at` timestamp NULL DEFAULT NULL,

`client_name` varchar(255) COLLATE utf8_bin DEFAULT NULL,

`client_authentication_methods` varchar(1000) COLLATE utf8_bin DEFAULT NULL,

`authorization_grant_types` varchar(1000) COLLATE utf8_bin DEFAULT NULL,

`redirect_uris` varchar(1000) COLLATE utf8_bin DEFAULT NULL,

`post_logout_redirect_uris` varchar(1000) COLLATE utf8_bin DEFAULT NULL,

`scopes` varchar(1000) COLLATE utf8_bin DEFAULT NULL,

`client_settings` json DEFAULT NULL,

`token_settings` json DEFAULT NULL,

PRIMARY KEY (`id`),

UNIQUE KEY `client_id` (`client_id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

 

 

2. 데이터 등록

대충요렇다

 

 

3. CustomRegisteredClientRepository 생성

@Component
@RequiredArgsConstructor
@Slf4j
public class CustomRegisteredClientRepository implements RegisteredClientRepository {
    private final ClientRepository clientRepository;
    private final ClientUtils clientUtils;

    @Override
    public void save(RegisteredClient registeredClient) {
        log.info("=== Client SAVE");
        ClientEntity entity = clientUtils.toEntity(registeredClient);
        clientRepository.save(entity);
    }

    @Override
    public RegisteredClient findById(String id) {
        log.info("=== Client findById");
        ClientEntity client = clientRepository.findById(id).orElseThrow();
        return clientUtils.toObject(client);
    }

    @Override
    public RegisteredClient findByClientId(String clientId) {
        log.info("== clientId:"+clientId);
        log.info("== context:"+ SecurityContextHolder.getContext());
        ClientEntity client = clientRepository.findByClientId(clientId).orElseThrow();
        return clientUtils.toObject(client);
    }
}

 

 

4. ClientUtil 생성 ( ClientEntity <-> RegisteredClient )

@Component
@RequiredArgsConstructor
@Slf4j
public class ClientUtils {

    public RegisteredClient toObject(ClientEntity 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(ClientSettings.builder().requireAuthorizationConsent(false).build()) // true : 동의 화면 처리 , false : 자동 동의
                .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofMinutes(15)).build()) // 15분
                ;


        //Map<String, Object> clientSettingsMap = parseMap(client.getClientSettings());
        //registeredClient.clientSettings(ClientSettings.withSettings(clientSettingsMap).build());

        //Map<String, Object> tokenSettingsMap = parseMap(client.getTokenSettings());
        //registeredClient.tokenSettings(TokenSettings.withSettings(tokenSettingsMap).build());

        return registeredClient.build();
    }

    public ClientEntity 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()));

        ClientEntity entity = new ClientEntity();
        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(writeMap(registeredClient.getClientSettings().getSettings()));
        entity.setTokenSettings(writeMap(registeredClient.getTokenSettings().getSettings()));

        return entity;
    }

 

ClientEntity <-> RegisteredClient 를 객체로 변환하면서 동시에 Client 나 Token 세팅도 가능하다.

 

5. Repository 생성

public interface ClientRepository extends JpaRepository<ClientEntity, String> {
    Optional<ClientEntity> findByClientId(String clientId);
}

 

 

그리고 기본적으로 Spring Securiy 설정에 아래와 같이 PasswordEncoder를 설정해줄수 있다.

 

6. PasswordEncoder 설정

/**
 * default password Encoder
 * @return
 */
@Bean
public PasswordEncoder passwordEncoder() {
    return new KISASeedPasswordEncoder();
}

 

따로 설정하지 않으면 client_credentials 로 토큰발급시 에러가 난다

OAuth2가 Client Secret Key 를 암호화해서 저장해야 한다고 얘기하는내용이다.

2024-08-28T16:30:36.726+09:00 ERROR 1916 --- [dtv-auth] [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

java.lang.IllegalArgumentException: You have entered a password with no PasswordEncoder. If that is your intent, it should be prefixed with `{noop}`.

 

Client ID와 Client Secret Key로 추후 다른 요청시 Header에 Authorization  값을 세팅해야 하므로 양방향대칭 알고리즘인 KISA_SEED_CRC를 적용하였다.

 

KISA 내용은 아랫글 참조

https://ssmyefrin.tistory.com/57

 

[암호화] 개인정보 암호화하기

프로젝트를 진행하며  개인정보를 암호화해야 할일이 생겼다.개인정보 암호화엔 어떤 알고리즘을 사용해야 하며, 종류는 어떤게 있는지 정리하며 적어본다.    먼저 복호화가 가능한지에

ssmyefrin.tistory.com

 

요렇게 설정하고 

서버를 구동한뒤 아래와 같이 /oauth2/token  를 날려보자.

 

POST /oauth2/token
Content-type : x-www-form-urlencoded
- Header 
 	Authorization : Basic ###
- Body
	grant_type : client_credentials

 

 

 

 

아래와 같이 잘나오는걸 확인할 수 있다.

 

다음시간엔 Login을 통해 인가코드를 받아온뒤 Token 까지 발급하는걸 해보겠다.

 

 

참조

https://do5do.tistory.com/20
https://sabarada.tistory.com/248
https://sg-choi.tistory.com/647
https://tonylim.tistory.com/405
https://tech.kakaopay.com/post/spring-oauth2-authorization-server-practice/
https://blog.bespinglobal.com/post/server-서버-인증-이해하기-3부-oauth-기본편/
https://tonylim.tistory.com/405
https://medium.com/riiid-teamblog-kr/spring-boot-3-1-2-spring-boot-starter-oauth2-authorization-server-적용기-9945f588d58