[Spring] OAuth2 인증 서버 구축하기 : JDBC 구현체로 구현하기

Kai·2024년 3월 13일
0

스프링과 OAuth2

목록 보기
11/11

☕ 개요


이번 글은 커스텀 UserDetailsService를 만들었다는 전제하에 시작한다. 커스텀 UserDetailsService에 대한 내용은 이 글을 참고하자.

이번 글에서는 RegisteredClientRepositoryOAuth2AuthorizationService의 구현체인 JdbcRegisteredClientRepositoryJdbcOAuth2AuthorizationService를 이용해서 인증/인가 기능을 구현해보도록 하겠다.

JDBC 구현체를 사용하는 이유

RegisteredClientRepositoryOAuth2AuthorizationService는 In-Memory구현체와 Jdbc구현체를 제공하는데, In-Memory구현체를 사용하면 서버의 재시작과 같은 상황에서 토큰이나 클라이언트의 정보가 휘발될 수 있다. 그래서 실제 운영환경에서는 DB에 이러한 정보를 저장하는 것이 필수적이라고 할 수 있다.
그래서 Spring-security에서도 Jdbc구현체를 기본적으로 제공하고, 이를 사용하면 간단하게 DB와 연동할 수 있게 된다.


테이블 생성

  • oauth2_authorization 테이블 생성

    CREATE TABLE oauth2_authorization (
       id varchar(100) NOT NULL,
       registered_client_id varchar(100) NOT NULL,
       principal_name varchar(200) NOT NULL,
       authorization_grant_type varchar(100) NOT NULL,
       authorized_scopes varchar(1000) DEFAULT NULL,
       attributes blob DEFAULT NULL,
       state varchar(500) DEFAULT NULL,
       authorization_code_value blob DEFAULT NULL,
       authorization_code_issued_at timestamp DEFAULT NULL,
       authorization_code_expires_at timestamp DEFAULT NULL,
       authorization_code_metadata blob DEFAULT NULL,
       access_token_value blob DEFAULT NULL,
       access_token_issued_at timestamp DEFAULT NULL,
       access_token_expires_at timestamp DEFAULT NULL,
       access_token_metadata blob DEFAULT NULL,
       access_token_type varchar(100) DEFAULT NULL,
       access_token_scopes varchar(1000) DEFAULT NULL,
       oidc_id_token_value blob DEFAULT NULL,
       oidc_id_token_issued_at timestamp DEFAULT NULL,
       oidc_id_token_expires_at timestamp DEFAULT NULL,
       oidc_id_token_metadata blob DEFAULT NULL,
       refresh_token_value blob DEFAULT NULL,
       refresh_token_issued_at timestamp DEFAULT NULL,
       refresh_token_expires_at timestamp DEFAULT NULL,
       refresh_token_metadata blob DEFAULT NULL,
       user_code_value blob DEFAULT NULL,
       user_code_issued_at timestamp DEFAULT NULL,
       user_code_expires_at timestamp DEFAULT NULL,
       user_code_metadata blob DEFAULT NULL,
       device_code_value blob DEFAULT NULL,
       device_code_issued_at timestamp DEFAULT NULL,
       device_code_expires_at timestamp DEFAULT NULL,
       device_code_metadata blob DEFAULT NULL,
       PRIMARY KEY (id)
    );
  • oauth2_registered_client 테이블 생성

    CREATE TABLE oauth2_registered_client (
       id varchar(100) NOT NULL,
       client_id varchar(100) NOT NULL,
       client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
       client_secret varchar(200) DEFAULT NULL,
       client_secret_expires_at timestamp DEFAULT NULL,
       client_name varchar(200) NOT NULL,
       client_authentication_methods varchar(1000) NOT NULL,
       authorization_grant_types varchar(1000) NOT NULL,
       redirect_uris varchar(1000) DEFAULT NULL,
       post_logout_redirect_uris varchar(1000) DEFAULT NULL,
       scopes varchar(1000) NOT NULL,
       client_settings varchar(2000) NOT NULL,
       token_settings varchar(2000) NOT NULL,
       PRIMARY KEY (id)
    );

    각각은 인증 정보 또는 토큰의 정보를 저장할 테이블과 인증 클라이언트를 저장할 테이블에 해당한다.

JdbcOAuth2AuthorizationService 빈 생성


    @Bean
    public JdbcOAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository);
    }

이렇게 Bean을 하나 만들어준다.


JdbcRegisteredClientRepository 빈 생성

    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientName("Your client name")
                .clientId("your-client")
                .clientSecret(passwordEncoder().encode("your-secret"))
                .clientAuthenticationMethods(methods -> {
                    methods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
                })
                .authorizationGrantTypes(types -> {
                    types.add(AuthorizationGrantType.AUTHORIZATION_CODE);
                    types.add(AuthorizationGrantType.REFRESH_TOKEN);
                })
                .redirectUris(uri -> {
                    uri.add("http://localhost:3000");
                })
                .postLogoutRedirectUris(uri -> {
                    uri.add("http://localhost:3000");
                })
                .scopes(scope -> {
                    scope.add(OidcScopes.OPENID);
                    scope.add(OidcScopes.PROFILE);
                })
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
                .build();

        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcOperations);
        try {
            registeredClientRepository.save(registeredClient);
        } catch (IllegalArgumentException e) {
            log.warn("이미 존재하는 클라이언트 : {}", e.getMessage());
        }

        return registeredClientRepository;
    }

여기서, 초기 클라이언트를 생성하는 로직은 선택사항이니 참고바란다.

이렇게 설정한 후, 서버를 실행하고 로그인 및 토큰을 통한 API호출을 해보면 모두 정상적으로 동작하는 것을 확인할 수 있다.
캡쳐화면은 따로 기록하지 않겠다.


📌 주의사항


사실 처음에는 공식문서에서 안내하고 있는 JPA 연동 방법을 따라서 구현해보려고 했는데, 주요 클래스들을 커스터마이징할 경우에 무수히 많은 파싱에러를 마주했었다. 그래서 구현할 때 굉장히 골치가 아팠었고, Jdbc구현체를 활용하는 것 + Return값 또는 매개변수로 유저나 권한정보를 넘겨줄 때는 무조건 Spring-security에서 기본적으로 제공하는 클래스로 변환하는 로직을 추가해주었다. 그랬더니 파싱에러들이 모두 해결이 됐었다. ㅎㅎ;;
혹시나, 인증서버를 구축하면서 파싱에러들을 마주하면 커스텀한 클래스 사용을 피하는 방법을 사용해보길 바란다.


🙏 참고


0개의 댓글