후라이
그래서 OAuth 2.0로 로그인 어떻게 하는 건데 본문
스프링 시큐리티 공부하다가 뇌가 터져버릴 거 같아요
암튼
1. OAuth 2.0이란?
OAuth는 제 3의 서비스에 계정 관리를 맡기는 방식입니다.
쉽게 말하면 "네이버로 로그인 하기", "구글로 로그인 하기"를 말하는 겁니다.
여기서 OAuth로 로그인한다는 것은 유저들이 별도의 비밀번호를 제공하지 않고서 다른 웹 사이트 상의 자신들의 정보에 대해 웹 사이트나 애플리케이션의 접근 권한을 부여받는다는 것입니다.
2. OAuth 용어 정리
흐름을 파악하기 위해서는 용어에 대한 이해가 필요합니다.
- 리소스 오너(resource owner) : 자신의 정보를 사용하도록 인증 서버에 허가하는 주체입니다. 서비스를 이용하는 사용자가 리소스 오너에 해당합니다.
- 리소스 서버(resource server) : 리소스 오너의 정보를 가지며, 리소스 오너의 정보를 보호하는 주체를 의미합니다. 네이버, 구글, 카카오가 리소스 서버에 해당합니다.
- 인증 서버(authorization server) : 클라이언트에게 리소스 오너의 정보에 접근할 수 있는 토큰을 발급하는 역할을 하는 어플리케이션을 의미합니다.
- 클라이언트 어플리케이션(client application) : 인증 서버에게 인증을 받고 리소스 오너의 리소스를 사용하는 주체를 의미합니다. 즉, 지금 만들고 있는 서비스가 이에 해당하는 것입니다.
OAuth를 사용하면 인증 서버에서 발급받은 토큰을 사용해서 리소스 서버에 리소스 오너의 정보를 요청하고 응답받아 사용할 수 있습니다. 말로 설명하니까 뭔 소리냐!! 싶으신 분들은 그림을 차근히 보고 이해하시면 됩니다.
- User(Resource Owner) : 웹 서비스를 이용하려는 유저, 개인정보를 소유하는 자(사용자) 입니다.
- Client(Application) : 자사 또는 개인이 만든 어플리케이션 서버, 진짜 걍 말하면 localhost:8080입니다.
- Authorization Server : 위에서 말한 인증 서버로 권한을 부여해주는 서버입니다.
User는 이 서버로 ID, PW를 넘겨 Authorization Code를 발급 받을 수 있구요
Client는 이 서버로 Authorization Code를 넘겨 Token을 발급 받을 수 있습니다. - Resource Server : 사용자의 개인정보를 가지고 있는 어플리케이션 (Google, Kakao 등) 회사 서버를 의미합니다.
Client는 Token을 이 서버로 넘겨서 개인정보 응답을 받게 되는 것이지요.
Access Token : 자원에 대한 접근 권한을 Resource Owner가 인가하였음을 나타내는 자격 증명입니다.
Refresh Token : Client는 인증 서버로부터 access token(짧은 만료기간)과 refresh token(긴 만료기간)을 함께 부여 받습니다. access token은 보안상 만료기간이 짧기 때문에 얼마 지나지 않아 만료되면 사용자는 로그인을 다시 시도해야합니다.
그러나 refresh token이 있다면 access token이 만료될 때 refresh token을 통해 access token을 재발급 받아 재 로그인 할 필요없게끔 합니다.
소셜 로그인을 구현하고 싶다?
= 유저의 로그인 정보인 개인정보를 리소스 서버(네이버)에 요청해서 클라이언트가 대신 로그인 하는 것이다.
3. 거쳐야 하는 단계
- Resource Owner로부터 동의 -> 사용자가 "구글 계정으로 로그인" 버튼을 클릭하고 동의
- Authorization Code 발급 -> 클라이언트가 인증 서버에 사용자 동의와 함께 요청을 보내 Authorization Code 받기
- Access Token 발급 -> 클라이언트가 Authorization Code를 인증 서버에 다시 전달해서 Access Token 발급 받음
- 데이터 요청 및 응답 -> 클라리언트는 Access Token을 사용해서 Resource Server에 데이터를 요청함, Resource Server는 이를 검증한 후에 데이터 냅다 던짐
4. 네이버로 로그인하기
https://developers.naver.com/main/
NAVER Developers
네이버 오픈 API들을 활용해 개발자들이 다양한 애플리케이션을 개발할 수 있도록 API 가이드와 SDK를 제공합니다. 제공중인 오픈 API에는 네이버 로그인, 검색, 단축URL, 캡차를 비롯 기계번역, 음
developers.naver.com
위 사이트가 네이버 API 서비스를 이용할 수 있는 공간이구요
https://developers.naver.com/apps/#/register?api=nvlogin
애플리케이션 - NAVER Developers
developers.naver.com
요기 링크를 들어가시면 아래와 같은 페이지가 나옵니다.
이 부분을 채워주심 됩니다.
간단하게 회원이름, 연락처 이메일 주소, 별명만 체크하고 넘어가보겠습니다?
환경 추가에 PC 웹을 선택한 후에 서비스 URL과 Callback URL을 입력합니다.
localhost 환경에서 개발하시는 분이라면 저렇게 똑같이 쓰시면 됩니다.
이러면 이제 Client ID와 Client Secret이라고 하는 게 발급됩니다.
Client ID : 어플리케이션을 식별하는 식별자 ID입니다.
리소스 서버(네이버) 입장에서 어떤 서비스에게 제공할 것인지 구분하는 ID이지요.
Client Secret : Client ID에 대한 password입니다. 이거 깃허브에 올리면 조집니다.
애플리케이션 정보는 application-oauth.properties에 기록하도록 합니다.
(application.properties에 쓰지 않고 application-oauth에 작성하는 이유는 공개되면 안되는 정보를 담고 있기 때문에 .gitignore에 해당 파일을 담기 위함입니다.)
# Naver OAuth2
spring.security.oauth2.client.registration.naver.client-id=[Client ID]
spring.security.oauth2.client.registration.naver.client-secret=[Client Secret]
spring.security.oauth2.client.registration.naver.scope=name,email,nickname
spring.security.oauth2.client.registration.naver.client-name=Naver
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8080/login/oauth2/code/naver
# Naver Provider
spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response
그냥 이렇게 똑같이 써도 좋습니다. 단, 대괄호 부분만 개인별로 발급 받은 아이디와 시크릿을 넣어주면 됩니다.
scope 부분도 name만 했으면 name만 쓰시고, 더 많은 스코프를 쓰셨다면 그에 맞는 정보들을 후두다닥 써주심 됩니다.
중간 부분에 등장하는 해당 코드는
spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8080/login/oauth2/code/naver
앞서 네이버 애플리케이션 등록 부분에서 작성한 Callback URL과 동일해야 합니다.
redirect URI는 Resource Server만 갖는 정보로, client에 권한을 부여하는 과정에서 나중에 Authorized Code를 전달하는 통로입니다. 클라이언트와 리소스 서버 사이의 유효성 검사에서 이 redirect URI도 체크되며 해당 주소가 아닐 경우 리소스 서버는 해당 클라이언트가 아니라고 판단하게 됩니다.
(=네이버 서버가 개인정보를 콜백할 주소)
또한, 네이버는 스프링 시큐리티를 공식 지원하지 않기 때문에 Provider를 직접 입력해주어야 합니다.
해당 내용은 네이버 API를 참고하시면 됩니다.
5. OAuth2 로그인 구현 시 필요한 설정
https://docs.spring.io/spring-security/reference/servlet/oauth2/login/core.html
Core Configuration :: Spring Security
If you are not able to use Spring Boot and would like to configure one of the pre-defined providers in CommonOAuth2Provider (for example, Google), apply the following configuration: OAuth2 Login Configuration @Configuration @EnableWebSecurity public class
docs.spring.io
이제 OAuth 로그인을 위해서는 인증 흐름 처리와 사용자 정보 처리를 코드로 구현해야 합니다.
5-1. SecurityConfig 설정
SecurityConfig는 OAuth2 로그인을 포함한 Spring Security 전체 설정을 담당하는 클래스입니다.
일반적인 로그인 구현에 있어서도 해당 Config 클래스를 작성했었습니다.
여기서 OAuth2 인증 관련 설정도 추가해주면 됩니다.
@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final CustomAuthFailureHandler customAuthFailureHandler;
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
public WebSecurityCustomizer webSecurityCustomizer() { // security를 적용하지 않을 리소스
return web -> web.ignoring()
// error endpoint를 열어줘야 함
.requestMatchers("/error");
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/**", "/auth/**", "/js/**", "/css/**", "/image/**").permitAll() // 특정 경로는 인증 없이 접근 가능
.requestMatchers("/admin/**").hasAnyRole("ADMIN")
.anyRequest().authenticated() // 그 외 경로는 인증 필요
)
.formLogin((auth) -> auth.loginPage("/auth/login")
.loginProcessingUrl("/auth/login")
.failureHandler(customAuthFailureHandler)
.defaultSuccessUrl("/welcome")
//위에 이제 post page로 데리고 가야 함.
.permitAll())
.oauth2Login((oauth2) -> oauth2
.defaultSuccessUrl("/welcome")
.userInfoEndpoint(c -> c.userService(customOAuth2UserService))
)
.logout((logout)->logout
// .logoutUrl("/auth/logout")
.logoutSuccessUrl("/auth/login")
.addLogoutHandler(new LogoutHandler() {
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
HttpSession httpSession = request.getSession();
httpSession.invalidate();
}
})
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("로그아웃 성공");
response.sendRedirect("/");
}
})
.deleteCookies("remember-me"));
http
.csrf((auth) -> auth.disable());
return http.build(); // SecurityFilterChain 반환
}
// 이 부분 버전에 따라 바뀜
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
코드가 길긴 하지만 주의깊게 살펴볼 부분은
.oauth2Login((oauth2) -> oauth2
.defaultSuccessUrl("/welcome")
.userInfoEndpoint(c -> c.userService(customOAuth2UserService))
)
이 부분입니다. 사실 큰 내용이 있는 것은 아니고
로그인 성공 후 리다이렉션 경로와 사용자 정보 엔드포인트를 설정하여사용자 정보 처리 로직을 등록한 것 뿐입니다.
SecurityConfig를 통해 Spring Security의 기본 인증 흐름을 확장하거나 커스터마이징해야 OAuth2 로그인을 처리할 수 있습니다. 어플리케이션 요구사항에 맞는 사용자 정보 처리를 직접 구현해야겠죠??
5-2. OAuth2UserService
OAuth2UserService는 OAuth2 로그인 과정에서 사용자 정보를 처리하는 핵심 구성 요소입니다.
Spring Security는 외부 서비스로부터 사용자 정보를 가져오지만, 이 정보를 어떻게 어플리케이션 내부에서 사용할지는 개발자가 정의해야 합니다.
- 사용자 정보 매핑 : OAuth2 제공자의 데이터(JSON)를 어플리케이션의 도메인 객체로 변환
- 회원가입/로그인 처리 : 새로운 사용자라면 DB에 사용자 정보를 저장(회원가입) / 기존 사용자라면 정보 업데이트 및 로그인
@Service
@Slf4j
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
/* OAuth2 서비스 id 구분코드 ( 구글, 네이버 ) */
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
/* OAuth2 서비스 id 구분 코드 (구글, 네이버) */
String registrationId = userRequest.getClientRegistration().getRegistrationId();
log.info("registrationId : "+registrationId);
/* OAuth2 로그인 진행 시 키가 되는 필드 값 PK (구글의 기본 코드는 "sub") */
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
log.info("============================================");
log.info("userNameAttributeName : " + userNameAttributeName);
/* OAuth2UserService */
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
log.info("============================================");
log.info("getAttributes(): " + attributes.getAttributes());
User user = saveOrUpdate(attributes);
/* 세션 정보를 저장하는 직렬화된 DTO 클래스 */
httpSession.setAttribute("user", new UserDto.Response(user));
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(user.getRoleValue())),
attributes.getAttributes(),
attributes.getNameAttributeKey()
);
}
private User saveOrUpdate(OAuthAttributes attributes) {
User user = userRepository.findByEmail(attributes.getEmail())
.map(User::updateModifiedDate)
.orElse(attributes.toEntity());
return userRepository.save(user);
}
}
OAuth2UserService는 OAuth2 로그인 과정에서 사용자 정보를 처리하기위해 Spring Security에서 제공하는 추상 클래스입니다.
좀 텀을 나눠서 차분히 이해해봅시다.
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
- DefaultOAuth2UserService
Spring Security가 제공하는 기본 구현체로, 외부 인증 제공자(구글, 네이버)의 UserInfoEndPoint로 요청을 보내 사용자 정보를 가져옵니다. - 파라미터로 가져온 OAuth2UserRequest는 인증 요청과 관련된 정보를 포함한 객체로, 인증 제공자의 설정 정보 및 액세스 토큰 정보를 포함합니다.
- oAuth2User는 가져온 사용자 정보(JSON)를 포함하게 됩니다.
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
- registraionId : 인증 제공자의 고유 ID를 의미합니다 구글이면 "google", 네이버라면 "naver"이런식으로 어떤 인증 제공자에로부터 요청이 온 건지 구분하게 됩니다.
- userNameAttributeName : 인증 제공자에서 반환하는 사용자 정보의 키, 값으로 제공자마다 다릅니다.
구글 : "sub"
네이버 : "id"
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
- OAuthAttributes : 인증 제공자에서 반환한 JSON 데이터를 어플리케이션에서 사용할 수 있도록 매핑하는 클래스입니다.
제공자별로 다른 JSON 구조를 통합적으로 처리하게 됩니다. - of() : 인증 제공자에 따라 다른 매핑 로직을 처리하는 정적 팩토리 메서드입니다.
ex) 구글 사용자는 name, email 필드를 매핑
네이버 사용자는 nickname, email 필드를 매핑
User user = saveOrUpdate(attributes);
//...
private User saveOrUpdate(OAuthAttributes attributes) {
User user = userRepository.findByEmail(attributes.getEmail())
.map(User::updateModifiedDate)
.orElse(attributes.toEntity());
return userRepository.save(user);
}
- 사용자 정보를 저장하게 되는데, 데이터베이스에 이메일을 기준으로 사용자를 조회합니다.
없다면 attributes.toEntity()로 새로운 사용자 객체를 생성하고, 이미 존재하는 사용자라면 정보 수정을 수행합니다.
여기서 우리는 OAuthAttributes가 뭔지 궁금합니다.
5-3. Attributes 처리
OAuth2 제공자(구글, 네이버)는 사용자 정보를 JSON 형식으로 제공합니다.
이 정보를 어플리케이션 도메인과 매핑하거나 로직에 활용하기 위해 속성(attributes)을 다루는 로직이 필요합니다!
리소스 서버로부터 받은 사용자 데이터를 매핑하고 처리하기 위한 DTO 클래스를 설계한다고 생각하면 편할 것이고요
주로 인증 제공자별로 다른 데이터 구조를 처리하기 위한 과정이므로 내가 필요한 데이터를 추출 및 변환해서 프로젝트에 사용할 수 있는 형태로 제공하게 됩니다.
private Map<String, Object> attributes;
private String nameAttributeKey;
private String username;
private String nickname;
private String email;
private Role role;
attributes :우선 인증 제공자(구글, 네이버)로부터 받은 원본 데이터를 저장해야 합니다.
nameAttributeKey : 인증 제공자에서 반환한 데이터 중, 사용자를 식별하기 위한 키 값입니다. (google : sub, naver : id)
public static OAuthAttributes of(String registrationId, String usernameAttributeName, Map<String, Object> attributes) {
if("naver".equals(registrationId)) {
return ofNaver("id", attributes);
}
return ofGoogle(usernameAttributeName, attributes);
}
처리 과정은 단순합니다.
registrationId를 확인하여, 인증 제공자를 구분한 후 네이버일 경우 ofNaver 그외에는 ofGoogle 호출하게 됩니다.
private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
Map<String, Object> response = (Map<String, Object>) attributes.get("response");
log.info("naver response : " + response);
return OAuthAttributes.builder()
.username((String) response.get("email"))
.email((String) response.get("email"))
.nickname((String) response.get("nickname"))
.attributes(response)
.nameAttributeKey(userNameAttributeName)
.build();
}
네이버 인증 제공자 데이터 처리 로직만 살펴보면
네이버는 사용자 정보를 response 라는 키 아래에 JSON 형태로 제공합니다.
{
"response": {
"id": "12345",
"email": "example@naver.com",
"nickname": "nickname"
}
}
- attributes.get("response")로 실제 사용자 데이터를 추출.
- 추출한 데이터를 username, email, nickname 필드로 매핑.
- OAuthAttributes 객체로 반환.
public User toEntity() {
return User.builder()
.username(username)
.email(email)
.nickname(nickname)
.role(Role.SOCIAL)
.build();
}
마지막으로 OAuthAttributes 객체를 User 엔티티 객체로 변환해서 DB에 저장하거나 업데이트할 때 사용하게 됩니다.
Resource Owner의 동의와 Resource Server의 권한 부여 과정을 깊이 있게 이해하고 싶다면 해당 티스토리를 추천합니다!너무 잘 정리해주셔서 이해가 쏙쏙 된 것 같습니다:)
🌐 OAuth 2.0 개념 - 그림으로 이해하기 쉽게 설명
OAuth란? 웹 서핑을 하다 보면 Google과 Facebook 등의 외부 소셜 계정을 기반으로 간편히 회원가입 및 로그인할 수 있는 웹 어플리케이션을 쉽게 찾아볼 수 있다. 클릭 한 번으로 간편하게 로그인할
inpa.tistory.com
redirect_uri 경로를 통해 리소스 서버가 클라이언트에게 임시 비밀번호의 역할이 되는 Authorization Code를 제공한다는 점이
'Spring' 카테고리의 다른 글
@RequestBody 와 <form> 태그 (0) | 2025.02.05 |
---|---|
Spring Pageable 안 쓰면 바보 (0) | 2025.01.31 |
@Valid 어노테이션으로 유효성 검사 (0) | 2025.01.19 |
@LoginUser 커스텀 어노테이션으로 로그인 사용자 정보 주입하기 (0) | 2025.01.14 |
Spring security Config 설정 | 생각없이 따라하다가 deprecated 놓치잖아 (0) | 2025.01.08 |