OAuth는 제3의 서비스에 계정 관리를 맡기는 방식이다. 다른 웹사이트에서 흔히 볼 수 있는 네이버, 구글, 카카오 계정을 이용하여 로그인하는 방식이다.
스프링 부트 서버가 특정 사용자 데이터에 접근하기 위해 권한 서버, 즉 구글이나 카카오 서버에 요청을 보내는 것이다. 요청 URI는 권한 서버마다 다르지만 보통 클라이언트 ID, 라다이렉트 URI, 응답 타입 등을 파라미터로 보낸다.
Google Cloud 콘솔에 접속 한 후 오른쪽 상단 콘솔 버튼을 클릭해준다.
이 후 새 프로젝트를 생성한다.

API 및 서비스 -> 사용자 인증 정보 -> 동의 화면 구성에 들어간다.

앱 이름, 이메일 등을 입력하여 준다.

리디렉션 URI: http://localhost:8080/login/oauth2/code/google
생성 후 나오는 클라이언트 ID와 클라이언트 보안 비밀번호는 애플리케이션에서 사용하는 값이다. 이 값은 리소스 오너의 정보에 접근할 때 사용한다.
리디렉션 URI: http://localhost:8080/login/oauth2/code/naver
application-oauth2.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${oauth2.google.client-id}
client-secret: ${oauth2.google.client-secret}
scope:
- email
- profile
naver:
client-id: ${oauth2.naver.client-id}
client-secret: ${oauth2.naver.client-secret}
scope:
- email
- name
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/naver
client-name: Naver
provider: # 네이버의 provider는 등록되어 있지 않아 사용자가 등록해야 함
naver:
authorization-uri: https://nid.naver.com/oauth2.0/authorize # 네이버 로그인 창
token-uri: https://nid.naver.com/oauth2.0/token # 토큰을 받는 URI
user-info-uri: https://openapi.naver.com/v1/nid/me # 프로필 주소를 받는 URI
user-name-attribute: response # 회원 벙보를 json 형태로 받는데 response라는 키값으로 네이버가 리턴해줌
// OAuth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
@Builder
public User(String email, String password, String nickname, int age, Role role, String provider, String providerId) {
this.email = email;
this.password = password;
this.nickname = nickname;
this.age = age;
this.role = role;
this.provider = provider;
this.providerId = providerId;
}
// 사용자 이름 변경
public User update(String nickname) {
this.nickname = nickname;
return this;
}
// 권한 정보 반환
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority(role.name()));
}
WebSecurityConfig
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
.successHandler(oAuth2SuccessHandler))
EncoderConfig
순환 참조 발생이 우려 되어 BCryptPasswordEncoder는 따로 클래스를 빼준다.
@Configuration
public class EncoderConfig {
@Bean
public BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}
@RequiredArgsConstructor
public class CustomOAuth2User implements OAuth2User {
private final User user;
private final Map<String, Object> attributes;
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getAuthorities();
}
@Override
public String getName() {
return user.getEmail();
}
public String getEmail() {
return user.getEmail();
}
public String getRole() {
return user.getRole().name();
}
}
OAuth2 인증을 처리할 때 OAuth2User 인터페이스를 구현한 사용자 정의 클래스이다.
OAuth2 로그인 성공 후, Spring Security는 기본적으로 OAuth2User 객체를 사용하여 로그인한 사용자의 정보를 저장한다. 하지만, 기본 OAuth2User는 우리가 원하는 추가 정보를 담을 수 없기 때문에, 사용자 정보를 커스텀하기 위해 CustomOAuth2User 클래스를 만들어서 사용한다.
Map<String, Object> attributesGoogle에서는 sub, email, name 등의 정보가 들어 있고, Naver는 response라는 키 아래에 email, name 등의 정보가 들어 있다.@Service
@RequiredArgsConstructor
@Slf4j
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId();
Map<String, Object> attributes = oAuth2User.getAttributes();
// 네이버인 경우 응답 데이터가 "response" 키로 감싸져 있음
if (provider.equals("naver")) {
attributes = (Map<String, Object>) attributes.get("response");
}
String providerId;
if (provider.equals("google")) {
providerId = (String) attributes.get("sub"); // Google의 사용자 ID는 "sub"
} else if (provider.equals("naver")) {
providerId = (String) attributes.get("id"); // Naver의 사용자 ID는 "id"
} else {
throw new IllegalArgumentException("지원하지 않는 OAuth2 제공자입니다: " + provider);
}
String email = (String) attributes.get("email");
String name = (String) attributes.get("name");
log.info("email -> {}", email);
User user = userRepository.findByEmail(email)
.orElseGet(() -> {
User newUser = User.builder()
.email(email)
.password(passwordEncoder.encode("oauth2user")) // 기본 비밀번호 설정 (사용되지 않음)
.nickname(name)
.age(0) // 기본값
.provider(provider)
.providerId(providerId)
.role(Role.USER)
.build();
return userRepository.save(newUser);
});
return new CustomOAuth2User(user, oAuth2User.getAttributes());
}
}
Spring Security의 OAuth2 로그인 과정에서 사용자 정보를 가져오고, 필요하면 회원가입까지 처리하는 서비스 클래스이다.
Spring Security의 DefaultOAuth2UserService를 확장하여 OAuth2 제공자(Google, Naver 등)에서 사용자 정보를 가져오고, 회원 정보를 데이터베이스에 저장하는 역할을 한다.
OAuth2 제공자로부터 사용자 정보를 가져옵니다.
super.loadUser(userRequest)를 호출하면, OAuth2 제공자로부터 받은 사용자의 기본 정보(OAuth2User)를 반환한다.
OAuth2 제공자로부터 받은 사용자 정보를 Map 형태로 가져온다.
네이버의 경우, 응답 데이터가 response라는 키 안에 감싸져 있으므로, 내부 데이터를 꺼내서 사용해야 한다.
@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final TokenProvider tokenProvider;
private final RefreshTokenService refreshTokenService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();
String email = oAuth2User.getEmail();
String role = oAuth2User.getRole();
String accessToken = tokenProvider.createAccessToken(email, role);
String refreshToken = tokenProvider.createRefreshToken(email);
// RefreshToken 저장
refreshTokenService.saveRefreshToken(email, refreshToken);
// Token을 HttpOnly 쿠키에 저장
addAccessTokenCookie(response, accessToken);
addRefreshTokenCookie(response, refreshToken);
// 응답에 토큰 추가
response.setHeader("Authorization", "Bearer " + accessToken);
response.setHeader("Refresh-Token", refreshToken);
super.onAuthenticationSuccess(request, response, authentication);
/*
// 추가 정보 입력 페이지로 리다이렉트
// 입력 폼을 제공해야 됨
// controller도 작성 -> email과 nickname 추출 후 나머지 정보 기입
response.sendRedirect("/additional-info");
*/
}
private void addAccessTokenCookie(HttpServletResponse response, String accessToken) {
Cookie cookie = new Cookie("accessToken", accessToken);
cookie.setHttpOnly(true); // JavaScript에서 접근 불가능
cookie.setSecure(false); // true -> HTTPS 환경에서만 전송
cookie.setPath("/"); // 모든 경로에서 접근 가능
cookie.setMaxAge((int) tokenProvider.getAccessTokenExpiration() / 1000);
response.addCookie(cookie);
}
private void addRefreshTokenCookie(HttpServletResponse response, String refreshToken) {
Cookie cookie = new Cookie("refreshToken", refreshToken);
cookie.setHttpOnly(true); // JavaScript에서 접근 불가능
cookie.setSecure(false); // true -> HTTPS 환경에서만 전송
cookie.setPath("/"); // 모든 경로에서 접근 가능
cookie.setMaxAge((int) tokenProvider.getRefreshTokenExpiration() / 1000);
response.addCookie(cookie);
}
}
이 클래스는 OAuth2 로그인 성공 시 실행되는 핸들러이다.
RefreshToken을 저장AccessToken과 RefreshToken을 포함시킨다.HttpOnly 쿠키에 저장 (XSS 방지)이후에 RefreshToken은 Redis에 저장하는 것이 효율적이고,
Oauth2로 바로 로그인하는 것이 아니라 추가 정보를 입력할 수 있게 해주는 것이 좋다.