[Spring] OAuth2Service + 스프링 시큐리티 + JWT로 카카오 로그인 구현하기
📌 OAuth
"OpenID Authorization"의 약자
비밀번호를 제공하지 않으면서 웹사이트나 어플리케이션 접근 권한을 부여할 수 있는 로그인 방식
기존 아이디와 비밀번호를 통한 로그인 방식은 보안상 취약한 점이 아주 많다
그러나 OAuth 를 사용하면 특정 접근 권한만 부여할 수도 있고
강력한 보안을 제공하는 대기업에 사용자 인증과 인가를 위임하는 방식으로 안전하게 로그인할 수 있다
🏛 Dependency
implementation group: 'org.springframework.security', name: 'spring-security-oauth2-client', version: '5.6.3'
build.gradle 에 OAuth 관련 의존성을 추가해준다
🚀 Kakao Developers 설정
시작하기
어플리케이션 추가하기 누르고 앱 이름과 사업자명 입력
이후 만든 어플리케이션 클릭해서 이동
REST API 키 나중에 사용해야 하니까 이 페이지 기억해두기
좌측 메뉴 카카오 로그인 -> 활성화 설정 상태 ON 으로 변경
이후 아래 Redirect URI 설정으로 진입
일단 로컬 환경에서 테스트를 진행해야 하기 때문에 로컬호스트로 설정
로그인 시도할 때 이 URI 를 통해 인가 코드가 반환이 된다
이 URI 은 클라이언트 쪽에서 사용해야 하니까 잘 기억해두기
카카오 로그인 -> 동의항목 에서
OAuth 를 통한 회원가입 진행 시 받아올 정보를 설정
💻 application.yml 설정
📝 application.yml
...
...
security:
oauth2:
client:
registration:
kakao:
client-id: ${KAKAO_RESTAPI_KEY}
redirect-uri: ${KAKAO_REDIRECT_URI}
authorization-grant-type: authorization_code
client-authentication-method: POST
client-name: Kakao
scope:
- profile_nickname
- account_email
provider:
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: id
...
...
설정 파일에서 security 부분을 추가해준다
아까 받은 rest api key 와 redirect uri 를 넣어주면 된다
💻 OAuth2 구현
📝 CustomOAuth2Service
@Slf4j
@RequiredArgsConstructor
@Service
public class CustomOAuth2Service extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
//provider 정보 (kakao, google, naver...)
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
log.info("registrationId = {}", registrationId);
log.info("userNameAttributeName = {}", userNameAttributeName);
//provider 정보 기반 객체 생성
OAuth2Attribute oAuth2Attribute =
OAuth2Attribute.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
final String email = oAuth2Attribute.getEmail();
final String name = oAuth2Attribute.getName();
final String provider = oAuth2Attribute.getProvider();
if (userRepository.existsByEmail(email)) {
log.info("가입된 회원입니다");
} else {
User user = User.builder()
.email(email)
.name(name)
.provider(provider)
.role(UserRole.ROLE_USER)
.build();
userRepository.save(user);
log.info("회원가입");
}
var memberAttribute = oAuth2Attribute.convertToMap();
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
memberAttribute, "email");
}
}
DefaultOAuth2UserService 를 구현한 클래스이다
이 클래스는 사용자 정보 기반으로 여러 기능을 지원해준다
OAuth2UserRequest 는 서드파티 서버로부터 사용자 정보를 받아올 수 있다
이 정보를 통해 가입된 회원인지 아닌지 파악하고 결과를 반환해주면 된다
결과로는 UserPrincipal 을 반환하는데 세션 방식에서는 이 결과가 저장이 되지만
우리는 JWT 방식을 사용하기 때문에 무시된다
📝 OAuth2Attribute
@ToString
@Builder(access = AccessLevel.PRIVATE)
@Getter
public class OAuth2Attribute {
private Map<String, Object> attributes;
private String attributeKey;
private String email;
private String name;
private String provider;
static OAuth2Attribute of(String provider, String attributeKey,
Map<String, Object> attributes) {
switch (provider) {
case "google":
return ofGoogle(attributeKey, attributes);
case "kakao":
return ofKakao("email", attributes);
case "naver":
return ofNaver("id", attributes);
default:
throw new RuntimeException();
}
}
private static OAuth2Attribute ofGoogle(String attributeKey,
Map<String, Object> attributes) {
return OAuth2Attribute.builder()
.name((String) attributes.get("name"))
.email((String) attributes.get("email"))
.provider("google")
.attributes(attributes)
.attributeKey(attributeKey)
.build();
}
private static OAuth2Attribute ofKakao(String attributeKey,
Map<String, Object> attributes) {
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
Map<String, Object> kakaoProfile = (Map<String, Object>) kakaoAccount.get("profile");
return OAuth2Attribute.builder()
.name((String) kakaoProfile.get("nickname"))
.email((String) kakaoAccount.get("email"))
.provider("kakao")
.attributes(kakaoAccount)
.attributeKey(attributeKey)
.build();
}
private static OAuth2Attribute ofNaver(String attributeKey,
Map<String, Object> attributes) {
Map<String, Object> response = (Map<String, Object>) attributes.get("response");
return OAuth2Attribute.builder()
.name((String) response.get("name"))
.email((String) response.get("email"))
.provider("naver")
// .picture((String) response.get("profile_image"))
.attributes(response)
.attributeKey(attributeKey)
.build();
}
Map<String, Object> convertToMap() {
Map<String, Object> map = new HashMap<>();
map.put("id", attributeKey);
map.put("key", attributeKey);
map.put("email", email);
map.put("name", name);
map.put("provider", provider);
return map;
}
}
OAuth 를 지원하는 플랫폼 마다 이름들이 조금씩 달라져서
통합하여 관리하기 위한 클래스이다
provider에 따라서 자동으로 이름을 변환시켜서 값을 대입해준다
카카오 뿐 아니라 다른 플랫폼으로도 확장하기 위하여 도입시켰다
📝 OAuth2AuthenticationSuccessHandler
package Backend.HIFI.auth.oauth;
import Backend.HIFI.auth.dto.TokenResponseDto;
import Backend.HIFI.auth.jwt.JwtTokenProvider;
import Backend.HIFI.auth.security.UserAuthentication;
import Backend.HIFI.user.User;
import Backend.HIFI.user.UserRepository;
import Backend.HIFI.user.UserRole;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.stream.Collectors;
@Slf4j
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtTokenProvider jwtTokenProvider;
private final UserRepository userRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
//로그인 성공한 사용자
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
final String email = oAuth2User.getName();
final String provider = oAuth2User.getAttribute("provider");
final String name = oAuth2User.getAttribute("name");
final String authorities = oAuth2User.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
TokenResponseDto tokenResponseDto = jwtTokenProvider.generateOAuth2Token(oAuth2User);
log.info("토큰 발행");
String targetUrl = UriComponentsBuilder.fromUriString("/oauth2/redirect")
.queryParam("token", tokenResponseDto.getAccessToken())
.build().toUriString();
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
}
CustomOAuth2Service 이후로 실행되는 핸들러 메서드
여기서 엑세스 토큰을 만들어서 이것을 쿼리 파라미터로 리디렉션하여 프론트에 넘겨주게 된다
프론트에서는 이 값을 저장하면 된다
리프레시 토큰을 Redis 에 저장하는 로직은 아직 추가 안했는데
더 공부하면서 추가할 것이다
http://localhost:8000/oauth2/redirect?token={accessToken}
📝 SecurityConfiguration
public class SecurityConfiguration {
...
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
...
...
.oauth2Login()
.defaultSuccessUrl("/login-success")
.successHandler(oAuth2AuthenticationSuccessHandler)
.userInfoEndpoint()
.userService(customOAuth2Service);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
...
...
}
마지막으로 스프링 시큐리티 필터 체인에 등록시켜주면 끝난다
- defaultSuccessUrl - 로그인 성공 시 이동할 url
- successHanlder - 인증 절차에 따라서 사용자가 정의한 로직을 실행시킴
- userInfoEndpoint - OAuth 로그인 성공 이후 행동 정의
- userService - customOAuth2Service 에서 후처리 하겠다고 선언
http://localhost:{서버포트}/oauth2/authorization/kakao 로 접속하면 카카오 로그인 화면이 뜬다
절차를 동의하고 실행하면 리디렉션이 된다