Spring/Spring Security

[Spring Security] 6-3. OAuth2 Login: Advanced Configuration

noahkim_ 2025. 7. 17. 00:43

0. 설정

HttpSecurity.oauth2Login()

  • OAuth2 또는 OpenID Connect를 통한 로그인 기능을 설정할 수 있도록 도와주는 DSL
  • 커스터마이징을 지원하는 옵션들을 제공

 

기본 설정) oauth2login

더보기
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .oauth2Login(oauth2 -> oauth2
            .clientRegistrationRepository(this.clientRegistrationRepository())
            .authorizedClientRepository(this.authorizedClientRepository())
            .authorizedClientService(this.authorizedClientService())
            .loginPage("/login")
            .authorizationEndpoint(authorization -> authorization
                .baseUri(this.authorizationRequestBaseUri())
                .authorizationRequestRepository(this.authorizationRequestRepository())
                .authorizationRequestResolver(this.authorizationRequestResolver())
            )
            .redirectionEndpoint(redirection -> redirection
                .baseUri(this.authorizationResponseBaseUri())
            )
            .tokenEndpoint(token -> token
                .accessTokenResponseClient(this.accessTokenResponseClient())
            )
            .userInfoEndpoint(userInfo -> userInfo
                .userAuthoritiesMapper(this.userAuthoritiesMapper())
                .userService(this.oauth2UserService())
                .oidcUserService(this.oidcUserService())
            )
        );

	return http.build();
}

 

1. 주요 구성요소

구성 요소 설명 기본 구현체
clientRegistrationRepository() 클라이언트 등록 정보 저장소
InMemoryClientRegistrationRepository
authorizedClientRepository() 인증된 사용자-클라이언트 정보 저장소
HttpSessionOAuth2AuthorizedClientRepository
authorizedClientService() 인증된 사용자-클라이언트 정보 저장소
InMemoryOAuth2AuthorizedClientService

 

2. OAuth 2.0 Login Page

DefaultLoginPageGeneratingFilter

  • 기본 로그인 페이지를 자동으로 생성해줌
  • 등록된 OAuth2 클라이언트를 링크로 보여줌 
  • /oauth2/authorization/{registerationId} (ClientRegistration.clientName값 사용)

 

HTML) 기본 로그인 페이지

더보기
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Please sign in</title>
    <link href="/default-ui.css" rel="stylesheet" />
  </head>

  <body>
    <div class="content">
      <h2>Login with OAuth 2.0</h2>

      <table class="table table-striped">
        <tr><td><a href="/oauth2/authorization/kakao">kakao</a></td></tr>
      </table>
    </div>
  </body>
</html>

 

설정) oauth2Login

더보기
.oauth2Login(oauth2 -> oauth2
    .loginPage("/login/oauth2")
    .authorizationEndpoint(authorization -> 
        authorization.baseUri("/login/oauth2/authorization")
    )
)
  • loginPage(): 커스텀 로그인 페이지 URL
  • authorization.baseUri(): OAuth 인증 시작 주소

 

코드) 커스텀 로그인 페이지 컨트롤러

더보기
@Controller
public class LoginController {
    @GetMapping("/login/oauth2")
    public String loginPage() {
        return "login"; // login.html 또는 login.jsp
    }
}

 

2. Endpoint

엔드포인트 역할 설명
관련 필터 / 컴포넌트
Authorization Endpoint user-agent를 통해 인가 코드 받기
OAuth2AuthorizationRequestRedirectFilter
Redirection Endpoint 인증 서버가 인증 결과를 클라이언트로 리디렉션 OAuth2LoginAuthenticationFilter
Token Endpoint 인가 코드를 가지고 토큰 요청 
(access token, id token, refresh token 등)
OAuth2LoginAuthenticationProvider
UserInfo Endpoint access_token으로 사용자 정보 조회 OAuth2UserService

 

Authorization Endpoint

설정) authorization Endpoint

더보기
.authorizationEndpoint(authorization -> authorization
    .baseUri(...) // default: /oauth2/authorization/{registrationId}
    .authorizationRequestRepository(...)
    .authorizationRequestResolver(...)
)
  • baseUri(): 사용자가 로그인 버튼을 누를 때 처음 요청되는 엔드포인트.
  • authorizationRequestRepository: 요청 저장소 (쿠키, 세션 등).
  • authorizationRequestResolver: 동적으로 Authorization 요청 생성하는 커스텀 로직

 

 

Redirect Endpoint

설정) Redirect URI

더보기
@Bean
public SecurityFilterChain securityFilterChain() {
    http
        .csrf(csrf -> csrf.disable())
        .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .oauth2Login(oauth2 -> oauth2
            .redirectionEndpoint(redirection -> redirection.baseUri("/login/oauth2/callback/*"))
        )
        
    return http.build();
}
  • baseUri는 ClientRegistration 객체의 redirectUri 속성과 매칭되어야 함

 

설정) ClientRegistration 객체의 redirectUri 속성

더보기
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
    ClientRegistration kakaoRegistration = ClientRegistration.withRegistrationId("kakao")
		//...
        .redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}") // ← 여기!
        .build();

    return new InMemoryClientRegistrationRepository(kakaoRegistration);
}
  security:
    oauth2:
      client:
        registration:
          kakao:
            provider: kakao
            client-id: ...
            client-secret: ...
            client-name: kakao
            client-authentication-method: client_secret_post
            authorization-grant-type: authorization_code
            scope: ...
            redirect-uri: http://localhost:8080/kakao/redirect/uri

 

UserInfo Endpoint

항목 GrantedAuthoritiesMapper
OAuth2UserService / OidcUserService 커스터마이징
주요 목적 단순 권한 문자열 변환
동적으로 GrantAuthority 구성
권한 매핑 방식 SCOPE_ 기반 → ROLE_ 변환 외부 API를 통한 유저 정보 조회
DB 확인
회원가입 및 권한 설정
사용자 정보 접근 OAuth2User.getAttributes()
access_token
userRequest
userInfo
(id_token)
회원가입 처리
유저 정보 요청 위임
✅ DefaultOAuth2UserService / OidcUserService
확장성 낮음 (권한만 다룸)
높음 (인증 후 전체 흐름 커스터마이징 가능)

 

Mapping User Authorities
  • OAuth2/OIDC 로그인 후, 인증된 사용자에 대한 권한을 매핑하는 방법
  • UserEndpoint 응답 객체의 scope은 "SCOPE_" 접두어를 가짐
  • 이를 도메인 규칙에 적용하기 위해 사용됨

 

설정) GrantedAuthoritiesMapper

더보기
.userInfoEndpoint(userInfo -> userInfo
    .userAuthoritiesMapper(userGrantedAuthoritiesMapper())
)

 

커스터마이징) GrantedAuthoritiesMapper

더보기
private GrantedAuthoritiesMapper userAuthoritiesMapper() {
    return authorities -> {
        Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

        for (GrantedAuthority authority : authorities) {
            if (authority instanceof OidcUserAuthority oidcUserAuthority) {
                OidcIdToken idToken = oidcUserAuthority.getIdToken();
                OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();

                // 예: 이메일 주소 기반으로 ROLE_ADMIN 부여
                String email = (String) userInfo.getClaims().get("email");
                if ("admin@example.com".equals(email)) {
                    mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
                }

            } else if (authority instanceof OAuth2UserAuthority oauth2UserAuthority) {
                Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();

                // 예: GitHub의 login 속성을 기반으로 ROLE 부여
                String login = (String) userAttributes.get("login");
                if ("admin".equals(login)) {
                    mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
                }
            }
        }

        return mappedAuthorities;
    };
}

 

OAuth2UserService
  • Access Token으로 UserInfo Endpoint에 요청을 보내 사용자 정보를 받아오는 서비스

 

설정) OAuth2UserService

더보기
DefaultOAuth2UserService service = new DefaultOAuth2UserService();
service.setRestOperations(customizedRestTemplate);
service.setRequestEntityConverter(customRequestEntityConverter());
  • RestTemplate (errorHandler, interceptor 등 설정 가능)
  • OAuth2UserRequestEntityConverter

 

OidcUserService
  • OIDC 로그인 시 id_token 검증 및 userinfo를 요청하는 서비스
  • ID Token 파싱: 사용자 신원을 id_token claim에서 얻음
  • UserInfo Endpoint 요청 (선택)

 

설정) OidcUserService

더보기
http.oauth2Login(oauth2 -> oauth2
    .userInfoEndpoint(userInfo -> userInfo
        .oidcUserService(this.OidcUserService())
    )
);

 

커스터마이징) OidcUserService

더보기
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
    final OidcUserService delegate = new OidcUserService();

    return (userRequest) -> {
        // 1. 기본 사용자 정보 로딩
        OidcUser oidcUser = delegate.loadUser(userRequest);

        // 2. access token 획득
        OAuth2AccessToken accessToken = userRequest.getAccessToken();

        // 3. 토큰으로 추가 권한 정보 가져오기 (예: REST API 호출)
        // 예: RestTemplate or WebClient 사용 → API: /userinfo/roles
        Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

        // 예시로 직접 매핑
        mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
        if ("admin@example.com".equals(oidcUser.getEmail())) {
            mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
        }

        // 4. 권한이 반영된 새 OidcUser 생성
        String userNameAttributeName = userRequest
            .getClientRegistration()
            .getProviderDetails()
            .getUserInfoEndpoint()
            .getUserNameAttributeName();

        if (StringUtils.hasText(userNameAttributeName)) {
            oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo(), userNameAttributeName);
        } else {
            oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
        }

        return oidcUser;
    };
}

 

Token Endpoint

ID Token Signature Verification
  • OpenID Connect에서는 사용자가 인증을 완료하면 id_token이 발급됨 (JWT 토큰. JWS 방식으로 서명됨)
  • 클라이언트는 이 토큰을 검증해서 진짜 인증된 사용자임을 확인해야 함
  • JwtDecoder로 검증함 (OidcIdTokenDecorderFactory로부터 생성됨)

 

표) Jws Algorithm

더보기
항목 RS256 HS256
서명 방식 비대칭키 (공개키 기반)
대칭키 (하나의 비밀키 공유)
사용 키 공개키(verify) / 개인키(sign)
공통 비밀키 (client-secret)
검증 방식 클라이언트가 서버에서 제공한 공개키로 서명 검증
클라이언트가 비밀키를 사용해 직접 서명 검증
키 전달 방식 jwks_uri를 통해 공개키 제공
클라이언트가 client-secret을 알고 있어야 함
보안 수준 높음 (키 노출 위험 낮음)
낮음 (비밀키 노출 시 보안 위협)
설정 필요 여부 ❌ 기본값으로 처리됨 (RS256은 기본값)
✅ setJwsAlgorithmResolver() 및 client-secret 필요
사용 예시 Kakao, Google, Apple 등 대부분의 OIDC 공급자
내부 시스템 / 테스트용 공급자

 

설정) OidcIdTokenDecorderFactory

더보기
@Bean
public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
    OidcIdTokenDecoderFactory factory = new OidcIdTokenDecoderFactory();

    factory.setJwsAlgorithmResolver(clientRegistration -> {
        String registrationId = clientRegistration.getRegistrationId();

        return switch (registrationId) {
            case "kakao" -> SignatureAlgorithm.RS256;
            case "google" -> SignatureAlgorithm.RS256;
            case "??" -> MacAlgorithm.RS256;
            default -> SignatureAlgorithm.RS256; // 기본값은 RS256
        };
    });

    return factory;
}

 

 

 

출처