Notice
Recent Posts
Recent Comments
Link
관리 메뉴

look-forest

스프링 OAuth2 인가/리소스 서버 본문

Spring/Spring Security (feat. JWT, OAuth2)

스프링 OAuth2 인가/리소스 서버

studyHub 2024. 11. 17. 17:27

앞서 네이버와 구글이 제공하는 OAuth2 규격에 맞춰 클라이언트가 되어 보았다.
이번엔 우리가 직접 OAuth2 서비스를 제공하는 <스프링 OAuth2 인가 서버> 와 <스프링 OAuth2 리소스 서버>를 구축해보자.

 

OAuth2 client를 이용함에 있어 인가/리소스 서버의 로직과 구조를 이해하는 것이 목적이므로 상세 코드는 다루지 않겠다.

 

구현 프로젝트

1. OAuth2 인가 서버 (네이버/구글의 OAuth2 로그인 제공 서버와 동일 개념)

2. OAuth2 리소스 서버 (인가 서버에서 JWT를 발급 받은 후 유저의 데이터를 받는 부분)

3. OAuth2 클라이언트 (우리가 구현한 OAuth2 인가/리소스 서버의 인증 서비스를 사용할 제 3자 플랫폼 역할)


OAuth2 인가 서버

동작 흐름

1. 세션 기반 스프링 시큐리티 로그인 기능 (like 네이버 자체의 로그인/회원가입 기능)

2. 제 3자가 OAuth2 서비스를 이용할 수 있도록 등록 (like 네아로, GCP 신청)

3. 로그인 및 코드 발급 프로세스

4. 코드 확인 후 Access 토큰 발급

모식도

구현

https://github.com/jaelyangChoi/OAuth2-Authorization-Server

1. 회원가입 및 로그인 구성

로그인의 경우 Spring Security를 이용하므로 UserDetailsService를 구현

 

2. OAuth2 인가 서버에 필요한 Bean 등록

Register, 코드 요청 엔드포인트, JWT 요청 엔드포인트와 같이 소셜 로그인을 수행하기 위한 여러 엔드포인트를 활성화 시키기 위해서는 SecurityConfig에 아래 Bean들을 등록해야 한다.

  • AuthorizationServerSettings
    OAuth2 인가 서버의 기본 설정을 정의하는 Bean. 활성화시 기본값으로 설정되는데, 굳이 커스텀할 필요는 없다.
  • SecurityFilterChain
    OAuth2 인가 서버용 SecurityFilterChain의 등록. 우선순위가 높아야 인가 로직이 활성화된다.

//OAuth2 인가 서버가 동작을 수행할 시큐리티 필터 체인
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) //인가 서버가 동작하도록 우선순위를 가장 높게 해야한다.
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
    //인가 서버 활성화
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

    //code 발급을 위한 OIDC 설정
    http
            .getConfigurer(OAuth2AuthorizationServerConfigurer.class)
            .oidc(Customizer.withDefaults());

    //로그인 창 띄워주기
    http
            .exceptionHandling(exceptions -> exceptions
                    .defaultAuthenticationEntryPointFor(
                            new LoginUrlAuthenticationEntryPoint("/login"),
                            new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                    ));

    return http.build();
}

//OAuth2 인가 서버의 기본 설정을 정의하는 Bean
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
    return AuthorizationServerSettings.builder().build();
}

 

 

3. Register 등록 구현

네아로를 사용하기 위해 네이버 개발자 센터에서 신청하듯,
Register에 해당하는 3자 플랫폼이 우리 서비스의 OAuth2 소셜 로그인을 사용하기 위한 신청 로직과 화면 구현.

(사용 신청한 플랫폼에게만 인가를 주고, 어떤 플랫폼이 접근하고 우리 정보를 가져가는지 트래킹하기 위해 구성)

 

사용할 필드
ClientName : 3자 플랫폼이 설정한 어플리케이션 이름
ClientId : 서비스측에서 랜덤값 (PK)
ClientSecret : 서비스측에서 랜덤값
ClientIssuedAt : ClientId 생성 시점
ClientSecretExpiresAt : ClientSecret 만료시간 (보통 부여 안함)
ClientAuthenticationMethods : 3자 플랫폼이 사용할 인증 방식
AuthoriationGrantTypes : OAuth2 인증 방식 4종류 중 한가지
RedirectUris : 3자 플랫폼이 코드를 받기 위해 허용 uri
PostLogoutRedirectUris : OAuth2 로그아웃 후 리디렉션 장소
Scopes : 3자 플랫폼이 가질 권한 범위
ClientSettings : 첫 로그인시 동의 표시와 같은 설정을 JSON으로 보관
TokenSettings : CODE나 JWT 활성 시간과 같은 설정을 JSON으로 보관

 

1) Register 등록부 구현

신청 화면에서 신청하면 신청 내용이 DB에 저장되도록 Entity, Contoller, Service, Repository 등 구성.

Entity는 Spring Security에서 공식으로 올려둔 테이블 기반으로 작성하면 된다.

 

2) Register 사용부 구현

OAuth2 인가 서버 Config가 Register 정보를 사용할 수 있도록,  

  CODE, JWT 요청 로직에서 Register 정보를 사용할 수 있도록 RegisteredClientRepository 인터페이스를 구현해야 한다. (구현부를 Bean으로 등록하면 인가 Config가 알아서 찾아 사용한다)

 

기본적으로 제공하는 두가지 구현체가 있지만

  • memoryRegisteredClientRepository
  • JdbcRegisteredClientRepository

인메모리의 경우 메모리 오버플로우 및 스케일 아웃 문제가 있다.
(하나의 서버 메모리에 저장되기 때문에, 스케일 아웃 시 로드밸런싱을 통해 접근하게 되면 인증 정보 공유 불가)

 

JPA를 사용하므로 Jdbc 구현체가 아니라 커스텀으로 JPA 구현부를 만들겠다. (공식 문서 내용을 약간 수정하면 됨)

 

 

4. OAuth2AuthorizationService 구현 (Authorization 과정 저장)

3자 플랫폼이 사용 등록 후, Code와 JWT 요청에 해당하는 엔드포인트로 접근하여 각각 데이터를 받아간다. 위 두 로직을 구성하기 전에, 두 로직에서 발생하는 결과물들(발급한 코드, 토큰, 발급 받아간 3자 플랫폼, 유저)을 저장하고 관리할 AuthorizationEntity와 서비스단을 구현해야 한다.

 

1) AuthorizationEntity 및 AuthorizationRepository 생성

2) OAuth2AuthorizationService 구현

위에서 작성한 Entity와 Repository를 OAuth2 인가 Config가 사용할 수 있도록 OAuth2AuthorizationService 인터페이스를 구현해야 한다. OAuth2AuthorizationService 인터페이스의 기본 구현체로
InMemoryOAuth2AuthorizationService, JdbcOAuth2AuthorizationService를 제공하지만, JPA 방식을 커스텀해서 구현.

@Component
public class JpaOAuth2AuthorizationService implements OAuth2AuthorizationService {

    private final AuthorizationRepository authorizationRepository;
    private final RegisteredClientRepository registeredClientRepository;
 
    @Override
    public void save(OAuth2Authorization authorization) {
    }

    @Override
    public void remove(OAuth2Authorization authorization) {
    }

    @Override
    public OAuth2Authorization findById(String id) {
    }

    @Override
    public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType) {
    }
}

 

모식도 리마인드

5. Code 발급부

3자 플랫폼에서 ‘소셜 로그인’ 버튼을 누르게 된다면 발생하게 되는 인가 서버의 로그인창, 코드 발급을 수행부 구현.

※ 3자 플랫폼에서 소셜 로그인 과정

  1. 3자 플랫폼에서 소셜 로그인 하기 버튼을 클릭 (스프링 OAuth2 클라이언트)
    3자 플랫폼에 해당하는 <OAuth2 클라이언트>에서 소셜 로그인을 제공하는 <OAuth2 인가 서버>로 보내는 요청
    https://주소/oauth2/authorize?response_type=code&client_id=등록아이디&scope=profile&redirect_uri=http://localhost:8080/login/oauth2/code/서비스
  2. 로그인창 표시 (스프링 OAuth2 인가 서버)
    로그인을 진행하기 위한 로그인창을 인가 서버에서 응답.
    이때 스프링 OAuth2 인가 서버의 경우 스프링 시큐리티에서 설정한 로그인창이 발생.
  3. 로그인 후 동의 화면 표시 (처음, 동의를 안 한 경우)
    특정 scope(범위)에 대해 3자 플랫폼에게 제공한다는 동의 사항
  4. 코드 발급
    로그인이 완료 되었기 때문에 “스프링 OAuth2 인가 서버”에서 처음 받은 redirect_uri로 CODE를 발급.

코드 발급부 구현
기본적인 로그인창과 엔드포인트는 "2. OAuth2 인가 서버에 필요한 Bean 등록" 과정을 통해 자동으로 활성화 되지만, 동의 화면에 대한 결과 저장의 경우 직접 구현해서 등록해야 한다.
 
동의 화면에서 발생한 결과를 저장하는 테이블 Entity, Repository, Service단 구성


1) ConsentEntity, ConsentRepository 등록

2) OAuth2AuthorizationConsentService 구현

위에서 작성한 ConsentEntity를 OAuth2 인가 Config가 사용할 수 있도록 OAuth2AuthorizationConsentService 인터페이스를 구현해야 한다. 인메모리, JDBC 방식의 구현체를 기본 제공하지만, JPA 방식을 커스텀해 사용할 수 있다.

@Component
@RequiredArgsConstructor
public class JpaOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {

    private final ConsentRepository consentRepository;
    private final RegisteredClientRepository registeredClientRepository;
    
    @Override
    public void save(OAuth2AuthorizationConsent authorizationConsent) {
        
    }

    @Override
    public void remove(OAuth2AuthorizationConsent authorizationConsent) {

    }

    @Override
    public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
        return null;
    }
}

 

 

6. Token 발급부

수행 과정

  1. 3자 플랫폼이 CODE 및 등록 clientId, clientSecret를 가지고 접근
  2. CODE, clientId, clientSecret 검증 후 JWT 발급 (OAuth2 인가 서버)
    이때 JWT의 claim에는 ROLE과 같은 정보를 담고 있으며 JWT 서명은 RSA 방식을 주로 사용

토큰 (JWT) 발급부 구현 목록

기본적인 엔드포인트의 경우   "2. OAuth2 인가 서버에 필요한 Bean 등록" 과정을 통해 자동으로 활성화 되지만,
토큰 발급시 서명에 대한 알고리즘은 Bean으로 등록해야 한다.

이때 서명의 경우 대칭키, 비대칭키로 나뉘며 비대칭키 방식을 지향한다.

비대칭 키를 지향하는 이유
인가 서버에서 발급한 토큰을 리소스 서버에서 검증(우리 인가 서버에서 발급한 것인지)하여 리소스 접근을 허락한다.

이때 대칭키(암복호화 동일키 사용)를 사용하면 인가 서버 및 리소스 서버의 변수에 키값이 포함되어야 하는데, 키 관리지점이 두개로 나뉘어 관리가 어려워지게 된다.(해킹에 대한 많은 접근 지점, 키 값 변경 시 모든 서버 값 변경 필요)

반면 비대칭 키를 사용할 경우, 키 관리를 인가 서버에서 전담하여 관리가 쉬워진다.
인가 서버가 토큰 발급 시 비밀키로 암호화 후 공개키를 엔드포인트에서 제공하면
리소스 서버에서는 공개키로 해독을 진행함과 동시에 인가 서버에서 발급한 키임을 확인할 수 있다. 

 

 

JWT 생성기 등록

스프링 인가 서버가 사용할 수 있도록 키 생성기를 비대칭키 방식으로 구현해서 Security Config에 등록하면 된다.

이 때 공개키를 제공하는 엔드포인트는 /oauth2/jwks 이다.

 

//비대칭 키 발급부
@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 (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

private static KeyPair generateRsaKey() {
    KeyPair keyPair;
    try {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        keyPair = keyPairGenerator.generateKeyPair();
    } catch (Exception e) {
        throw new IllegalStateException(e);
    }
    return keyPair;
}

 


OAuth2 리소스 서버

 

 

 


OAuth2 클라이언트

 

 


참고 자료 & 이미지 출처
https://www.devyummi.com/page?id=66e07a57d545c21fedc00aee
 

개발자 유미 | 커뮤니티

 

www.devyummi.com

https://spring.io/projects/spring-authorization-server

 

Spring Authorization Server

Spring Authorization Server is a framework that provides implementations of the OAuth 2.1 and OpenID Connect 1.0 specifications and other related specifications. It is built on top of Spring Security to provide a secure, light-weight, and customizable foun

spring.io