스프링부트x리액트 '카카오 로그인 하기' (JWT+OAuth2) [2]
스프링부트x리액트 카카오 로그인 구현하기 (JWT+OAuth2)
해당 포스팅에 대한 구조, 이론 정리는 이전 게시글 에 있습니다.
이번 포스팅은 카카오로그인 구현, 실제 로그인 테스트까지의 과정입니다.
1. build.gradle 설정
gradle에 다음을 추가한다.
spring-boot-starter-oauth2-client
implementation group: 'org.springframework.security', name: 'spring-security-oauth2-client', version: '5.6.3'
2. 카카오 개발자 사이트 애플리케이션 추가
이 챕터에서는 다음 링크를 참고합니다.
(2)
(3)
이렇게 추가하고 난 후 방금 만든 앱으로 들어가보면,
다음 챕터에서 REST API 키를 사용할 것이다. 복붙 해두거나 창을 열어두면 편리하다!
Redirect URI을 설정해준다.
예시 : http://localhost: [ port ] /login/oauth2/code/kakao
로그인 시 이 Redirect URI를 통해 우리가 얻으려는 code(인가코드)를 반환 받는다.
Redirect URI도 다음 챕터에서 사용할 것이라 복붙 해두거나 창을 열어두면 편리하다.
그리고 동의항목 체크해주기.
나의 경우 닉네임 필수, 프로필 사진 선택, 이메일 선택으로 해뒀는데..
이번 포스팅에서는 닉네임과 이메일만 사용한다.
3. application.yml 설정
프로젝트 설정 파일에 설정을 추가한다.
server:
port: 8090
jwt:
secret: [입력]
spring:
datasource:
url: [입력] ex) jdbc:mysql://localhost:3306/DB이름
driver-class-name: [입력] ex) com.mysql.cj.jdbc.Driver
username: [입력]
password: [입력]
security:
oauth2:
client:
registration:
kakao:
client-id: [REST API 키]
redirect-uri: [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
* application.properties 파일을 application.yml 로 변경하면 yaml 형식으로 사용 가능하다.
application.properties 파일이라면 아래와 같은 형식으로 입력.
spring.security.oauth2.client.registration.kakao.client-id = [클라이언트 id]
spring.security.oauth2.client.registration.kakao.client-secret = [클라이언트 pw]
spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/login/oauth2/code/kakao
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.kakao.scope=profile,account_email
spring.security.oauth2.client.registration.kakao.client-name=kakao
spring.security.oauth2.client.registration.kakao.client-authentication-method=POST
spring security에서 제공하는 oauth2-client 라이브러리에는 유명한 google, facebook, twitter 등 웹사이트에 대한 provider들은 제공을 해주지만 우리나라에서만 한정적으로 사용하는 네이버나, 카카오 같은 서비스에 대한 정보들을 제공해주지 못한다. 그래서 우리가 이렇게 직접 등록을 해야함.
4. 스프링시큐리티 설정
다음을 스프링 시큐리티 설정에 추가한다.
.oauth2Login()
ouath2 로그인 처리하는 메소드.
위의 한 줄만 추가후 실행해서 http://localhost:8080/oauth2/authorization/kakao 로 접속하면 아래 화면이 뜬다.
.oauth2Login() 다음 줄에 다음을 더 작성해준다.
.oauth2Login()
.defaultSuccessUrl("/login-success")
.successHandler(oAuth2AuthenticationSuccessHandler)
.userInfoEndpoint()
.userService(userOAuth2Service);
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService jwtUserDetailsService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Autowired
private UserOAuth2Service userOAuth2Service;
@Autowired
private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.cors().configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues());
httpSecurity.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers("/product/**", "/member/authenticate", "/auth/**", "/order/**").permitAll()
exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.oauth2Login().defaultSuccessUrl("/login-success").successHandler(oAuth2AuthenticationSuccessHandler).userInfoEndpoint().userService(userOAuth2Service);
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
configure 메서드의 맨 마지막 부분을 보면,
1) .defaultSuccessUrl("/{login-success-url}")
oauth2 인증이 성공했을 때, 이동되는 url을 설정.
2) successHandler()
인증 프로세스에 따라 사용자 정의 로직을 실행.
3) userInfoEndPoint().userService(customOAuth2UserService);
로그인이 성공하면 해당 유저의 정보를 들고 customOAuth2UserService에서 후처리를 해주겠다는 의미.
자세한 정보 참고 : https://www.baeldung.com/spring-security-5-oauth2-login
5. UserOAuth2Service 구현
DefaultOAuth2UserService를 상속한 UserOAuth2Service 클래스 구현.
OAuth2UserService 클래스는 사용자의 정보들을 기반으로 가입 및 정보수정, 세션 저장등의 기능을 지원해준다.
DefaultOAuth2UserService 클래스 안의 loadUser 메서드를 호출되게 했고, 이걸 활용해서 회원가입 작업을 한다.
UserOAuth2Service의 loadUser(OAuth2UserRequest oAuth2UserRequest) 메서드는 사용자 정보를 요청할 수 있는 access token 을 얻고나서 실행된다.
우선 이 메서드가 할 일은 이렇다.
- access token을 이용해 서드파티 서버로부터 사용자 정보를 받아온다.
- 해당 사용자가 이미 회원가입 되어있는 사용자인지 확인한다.
만약 회원가입이 되어있지 않다면, 회원가입 처리한다.
만약 회원가입이 되어있다면 가입한 적 있다는 log를 찍고 로그인한다. (추후 가입한 적 있다는 alret 추가로 구현하면 좋을듯)
- UserPrincipal 을 return 한다. 세션 방식에서는 여기서 return한 객체가 시큐리티 세션에 저장된다.
하지만 JWT 방식에서는 저장하지 않는다. (JWT 방식에서는 인증&인가 수행시 HttpSession을 사용하지 않을 것이다.)
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
@Service
public class UserOAuth2Service extends DefaultOAuth2UserService {
private final HttpSession httpSession;
@Autowired
MemberDao memberDao;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
Map<String, Object> attributes = oAuth2User.getAttributes();
Map<String, Object> kakao_account = (Map<String, Object>) attributes.get("kakao_account");
String email = (String) kakao_account.get("email");
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
String nickname = (String) properties.get("nickname");
if (memberDao.checkEmail(email) == 0) {
memberDao.createMemberKakao(email, nickname);
} else {
System.out.println("가입한적 있음.");
}
return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_MEMBER")), attributes, "id");
}
}
추가 설명:
(1) 여기서 access token을 이용해 서버로부터 사용자 정보를 받아온다.
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
DefaultOAuth2UserService 클래스의 loadUser() 메서드에 이 기능이 구현되어있기 때문에 super.loadUser() 를 호출하기만하면 된다.
(2) 앞서 OAuth2User 타입 객체로 전달받은 사용자 정보를 확인 후 처리한다.
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
Map<String, Object> attributes = oAuth2User.getAttributes();
Map<String, Object> kakao_account = (Map<String, Object>) attributes.get("kakao_account");
String email = (String) kakao_account.get("email");
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
String nickname = (String) properties.get("nickname");
if (memberDao.checkEmail(email) == 0) {
memberDao.createMemberKakao(email, nickname);
} else {
System.out.println("가입한적 있음.");
}
이렇게 가입하고나면, 이제 서버는 토큰을 만들어서 브라우저에게 내려주는 일만 남았다.
6. OAuth2AuthenticationSuccessHandler
이 작업을 AuthenticationSuccessHandler 클래스의 onAuthenticationSuccess() 메서드에서 아래 그림처럼 동작되게 구현한다. 로그인 다 완료되고 우리 서버에서 쓸 수 있는 jwt 토큰을 발급하는 부분이다.
import java.io.IOException;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
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;
@Component
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Autowired
JwtTokenUtil jwtTokenUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
// login 성공한 사용자 목록.
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
Map<String, Object> kakao_account = (Map<String, Object>) oAuth2User.getAttributes().get("kakao_account");
String email = (String) kakao_account.get("email");
Map<String, Object> properties = (Map<String, Object>) oAuth2User.getAttributes().get("properties");
String nickname = (String) properties.get("nickname");
String jwt = jwtTokenUtil.generateTokenForOAuth("kakao", email, nickname);
String url = makeRedirectUrl(jwt);
System.out.println("url: " + url);
if (response.isCommitted()) {
logger.debug("응답이 이미 커밋된 상태입니다. " + url + "로 리다이렉트하도록 바꿀 수 없습니다.");
return;
}
getRedirectStrategy().sendRedirect(request, response, url);
}
private String makeRedirectUrl(String token) {
return UriComponentsBuilder.fromUriString("http://localhost:3000/oauth2/redirect/"+token)
.build().toUriString();
}
}
7. 프론트엔드 구현
(1) 로그인 버튼을 클릭하면 다음 주소로 요청가게끔 구현한다.
(2) 받아온 access token을 local storage에 token이라는 이름으로 저장한다.
import React, { useEffect } from "react";
import { useParams } from "react-router";
function KakaoLoginRedirect() {
const params = useParams();
useEffect(() => {
localStorage.clear();
localStorage.setItem("token", params.token);
window.location.replace("/");
}, []);
return <></>;
}
export default KakaoLoginRedirect;
* Local Storage: 해당 도메인에 영구 저장하고 싶을 때 ✅
Session Storage: 해당 도메인의, 한 세션에서만 저장하고 싶을 때 (창 닫으면 data 날아감)
Cookie: 해당 도메인에 날짜를 설정하고 그 때까지만 저장하고 싶을 때
여기까지 구현은 끝났다! 테스트 해보자.
8. 회원가입 테스트
(1) 회원가입
(2)
(3)
(4)
8. 로그인 테스트
(1) 로그인 클릭.
(2) 카카오 회원가입으로 가입한 이력 있는 카카오톡 아이디로 로그인 시도.
(3) 바로 로그인 성공.