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;
}
출처
'Spring > Spring Security' 카테고리의 다른 글
[Spring Security] 7-1. OAuth2 Client: Core Interfaces and Classes (0) | 2025.07.18 |
---|---|
[Spring Security] 6-4. OAuth2 Login: OIDC Logout (1) | 2025.07.18 |
[Spring Security] 6-2. OAuth2 Login: Core Configuration (1) | 2025.07.15 |
[Spring Security] 6-1. OAuth2 (0) | 2025.07.13 |
[Spring Security] 4-2. Authorization: HttpServletRequests (2) | 2025.07.11 |