우리는 보통 로그인할 때 네이버로 로그인, 구글 계정으로 로그인 같은 것을 본 적이 있다. 이를 이용하게 되면 해당 서비스에 따로 가입 정보를 입력하지 않아도 곧바로 이용이 가능하다.
Open Authorization의 약자로 제 3자 인증 방식으로, 신뢰할 수 있는 웹사이트에 등록되어 있는 회원 정보를 활용하여 서비스에 로그인을 하는 기능을 말한다. 이는 정확히 말하자면, 대신 로그인을 하는 게 아니고 사용자 정보를 위임하는 기술이다.
HTTPS는 HTTP 프로토콜을 암호화하여, 패킷을 가로챌 수 있는 위험성을 줄였다. 그런 HTTPS의 암호화 방식 중에 제 3자 인증 방식이 있는데, 그것이 바로 OAuth이다. 개인정보의 보안성을 높이기 위해서 나온 것으로 생각하면 된다.
네이버를 예로 들어본다.
우리는 이 과정에서 프론트 엔드로 로그인 토큰 반환 과정까지 진행해 본다.
우선 네이버 로그인 API를 이용한다.
원하는 정보를 선택하고, 환경 설정을 해준다. 나는 PC 웹으로 진행했다.
여기서 Callback URL은 인가코드를 요청한 뒤 리다이렉트되는 주소이다.
( 서버는 클라이언트가 지정한 콜백 URL로 사용자를 리다이렉트 하고, 콜백 URL로 이동한 사용자는 인가 코드를 수신한다. )
1. SpringBoot의 gradle에 의존성을 주입한다.
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
2. yaml 파일을 수정해 준다.
spring:
security:
oauth2:
client:
provider:
naver:
authorization-uri: https://nid.naver.com/oauth2.0/authorize
token-uri: https://nid.naver.com/oauth2.0/token
user-info-uri: https://openapi.naver.com/v1/nid/me
user-name-attribute: response
registration:
naver:
client-id: RP8tuCd8ec5_zuNb7Yv2
client-secret: VnYHzwP2PN
redirect-uri: http://localhost:8080/login/oauth2/code/naver
authorization-grant-type: authorization_code
client-authentication-method: client_secret_post
client-name: Naver
scope:
- nickname
- email
- profile_image
여기서
authorization-uri
: 위에서 작성했던 콜백 URL을 쓴다.token-uri
: 사용자 정보 요청을 위한 Access Token을 받기 위한 URL을 작성한다.user-info-uri
: 사용자 정보를 조회하기 위한 URL을 작성한다.user-name-attribute
: 클라이언트로부터 받은 사용자 정보 중 어떤 부분을 활용하는 지를 작성한다. 서비스에서 필요로 하는 사용자 정보를 자동으로 선택하기 위한 용도로 활용되며, 실제로 사용자에게 제공되거나 외부로 노출되지는 않는다.client-id
, client-secret
: 클라이언트 측에 저희가 어떤 서비스인지를 인증하기 위한 값이다. 이는 애플리케이션 정보창에서 확인 가능하다.redirect-uri
: 위에서 작성했던 콜백 URL을 쓴다.authorization-grant-type
: 어떤 방식으로 Access Token을 받을지를 정의한니다. 이 부분은 일반적으로 authorization_code
로 유지합니다.client-authentication-method
: 클라이언트가 자신을 인증하는 방식을 나타내며, Client Id와 Client Secret를 요청의 어디에 포함할지를 정의한다. client_secret_post는 그중 하나로, 클라이언트 ID와 클라이언트 시크릿을 POST 요청의 본문(body)에 포함하여 보내는 방식이다.3. 인증 이후에 사용자 데이터를 처리하기 위한 OAuth2UserServiceImpl를 작성한다.
@Slf4j
@Service
public class OAuth2UserServiceImpl extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest)
throws OAuth2AuthenticationException {
// 부모 클래스의 loadUser 메서드를 호출하여 OAuth2User 정보를 가져옴
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
SpringBoot에서는 DefaultOAuth2UserService와 같은 미리 구성된 서비스를 제공한다. 이를 상속받아서 설정이 정의된 서비스 제공자에 대하여 큰 설정 없이 사용할 수 있게 해 준다.
super.loadUser(oAuth2UserRequest)
를 호출하여 부모 클래스의 loadUser 메서드를 실행한다. 이는 Spring Security가 제공하는 기본 OAuth2UserService로, OAuth 2.0 제공자로부터 사용자 정보를 가져오는 역할을 한다.
즉, loadUser 메서드가 반환하는 OAuth2User 객체를 통해 네이버로부터 받은 사용자 정보에 접근할 수 있다.
이제 사용자 정보를 담을 Map을 생성할 차례이다.
// 새로운 속성을 저장할 Map을 생성
Map<String, Object> attributes = new HashMap<>();
// 사용자 정보의 제공자(provider)를 "naver"로 설정함
attributes.put("provider", "naver");
// 네이버로부터 받은 데이터를 아래와 같이 활용함
Map<String, Object> responseMap
= oAuth2User.getAttribute("response");
attributes.put("id", responseMap.get("id"));
attributes.put("email", responseMap.get("email"));
attributes.put("nickname", responseMap.get("nickname"));
// 사용자의 식별에 사용할 속성을 "email"로 설정함
String nameAttribute = "email";
// 사용자 정보를 담은 DefaultOAuth2User 객체를 생성하여 반환함
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority("USER")),
// "USER"라는 권한을 부여함
attributes, // 사용자의 추가 정보를 담은 Map
nameAttribute // 사용자를 식별하는 데 사용할 속성
);
}
}
위와 같이 작성하여, 네이버로부터 받은 데이터 중에서 사용하고자 하는 정보를 선택하여 새로운 Map에 담고 DefaultOAuth2User 객체를 생성하여 반환한다.
4. 이번에는 인증 이후 어떻게 동작할지를 정의하는 OAuth2SuccessHandler를 작성한다.
@Slf4j
@Component
public class OAuth2SuccessHandler
extends SimpleUrlAuthenticationSuccessHandler {
//JwtTokenUtils 를 활용해 JWT를 생성하도록 함
private final JwtTokenUtils tokenUtils;
public OAuth2SuccessHandler(JwtTokenUtils tokenUtils) {
this.tokenUtils = tokenUtils;
}
//인증 성공 시 동작 정의
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) throws IOException, ServletException {
// OAuth2 인증을 통해 얻은 사용자 정보를 가져옴
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
// JWT 토큰 생성
String jwt = tokenUtils.generateToken(
User
.withUsername(oAuth2User.getName())
.password(oAuth2User.getAttribute("id").toString())
.build());
// 생성한 JWT를 이용하여 리다이렉트할 URL 생성
String targetUrl = String.format(
"http://localhost:8080/token/val?token=%s", jwt
);
// 생성한 URL로 리다이렉트
// 아래는 SimpleUrlAuthenticationSuccessHandler의 메서드
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
}
여기서는 impleUrlAuthenticationSuccessHandler 클래스에서 상속받은 onAuthenticationSuccess를 정의한다. 이는 인증 성공 시 동작을 정의한다.
(1).authentication.getPrincipal()
으로 인증을 통해 얻은 사용자 정보를 가져온다.
(2) 그 후 tokenUtils.generateToken을 사용하여 Token을 발급받는다.
(3) 이 JWT를 포함한 리다이렉트 URL을 생성하고,
(4) getRedirectStrategy().sendRedirect
으로 자동 리다이렉트 되도록 한다.
5. 여기까지 작성했다면 WebSecurityConfig 를 통해 OAuth2UserServiceImpl 과 OAuth2SuccessHandler를 구성한다.
여기서는 이 두 가지 Bean객체를 SecurityFilterChain 구성 시 추가해 준다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 기본 HTTP 기본 인증 및 CSRF 보안 설정을 비활성화
.httpBasic(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
// 특정 URL 패턴에 대한 권한 설정
.authorizeHttpRequests(authHttp -> authHttp
.requestMatchers("/token/**", "view url")
// "/token/**", "뷰 url" 경로에 대해서는 권한 검사를 하지 않음.이는 ViewController 작성 시 경로
.permitAll() // 모든 사용자에게 허용
.anyRequest().authenticated() // 그 외의 요청은 인증된 사용자에게만 허용
)
// OAuth 2.0 로그인 설정
.oauth2Login(oauth2Login -> oauth2Login
.loginPage("/views/login") // 사용자를 로그인 페이지로 리다이렉트
.successHandler(oAuth2SuccessHandler) // 로그인 성공 시의 핸들러로 oAuth2SuccessHandler를 사용
.userInfoEndpoint(userInfo -> userInfo
.userService(oAuth2UserService) // 사용자 정보 엔드포인트를 처리하는 사용자 서비스 설정
)
)
// 세션 관리 설정
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// JWT 토큰 필터 추가
.addFilterBefore(jwtTokenFilter, AuthorizationFilter.class);
return http.build();
}
우리가 살펴볼 곳은 로그인 설정 구간이다.
.loginPage()
: 로그인이 성공하지 않았을 때 사용자를 리다이렉트할 로그인 페이지를 정의한다.successHandler()
: 인증이 성공했을 때 사용할 핸들러 객체를 설정userInfoEndpoint()
: 사용자 정보를 조회하는 Endpoint 설정을 담당한다.마지막으로 Controller를 작성하고, login 링크를 만들면 소셜 로그인을 진행할 수 있다.
네이버 로그인이 생긴 것을 확인할 수 있고, 권한을 부여하면 아래와 같은 JWT 페이로드 정보를 받을 수 있다.
클라이언트는 이 정보를 안전하게 보관하고, 필요할 때마다 HTTP 헤더에 JWT를 포함시켜 서버에 보내서 인증 과정을 진행한다.
여기까지 네이버 로그인을 진행해 봤다. 네이버 로그인 구현을 통해 OAuth의 기본 원리를 이해하고 코드를 작성하는 경험이 되었다. 이로써 OAuth를 사용한 로그인 시스템을 구축하는 데 필요한 기본 기능들을 숙지했다.
OAuth는 코드의 재사용성이 높기 때문에, 이제는 다른 플랫폼에도 쉽게 확장할 수 있을 것으로 예상된다. OAuth의 핵심 메커니즘은 공통적으로 적용되기 때문에, 네이버 로그인에서 구현한 코드를 기반으로 구글과 카카오 로그인을 적용하는 것은 어렵지 않을 것으로 추측..
다음 단계에서는 구글과 카카오 로그인을 진행해 보면서, 각 플랫폼에서의 고유한 특징과 동작 방식에 대한 이해를 높이고, 이러한 다양한 로그인 시나리오를 다루어 보고자 한다.