반응형
Spring Boot와 Nuxt3를 이용한 카카오 sso 로그인 개발 및 JWT토큰 발급 방법
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
카카오 개발자 설정은 넘어가도록 하겠습니다.
버전 정보
Spring Boot 2.7.3
Java 11
Nuxt3
# nuxt
<template>
<div class="container">
<div class="login-div">
<div class="login-div-header">로그인</div>
<a :href="kakaoAuthUrl">
<img class="login-div-button" src="/images/common/kakao_login_img.png">
</a>
</div>
</div>
</template>
<script setup>
const { baseURL } = useRuntimeConfig().public
const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?client_id=efb7a9d2600fa1ee3a40f47c92fee75d&redirect_uri=${baseURL}/api/kakao/callback&response_type=code`
</script>
위와 같이 프론트 화면을 만든 후
# build.gradle
// Webflux (webClient)
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
# KakaoLoginController
package com.reroll.rerollback.api.auth.kakao;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.view.RedirectView;
import org.springframework.beans.factory.annotation.Value;
import java.util.Map;
@RestController
public class KakaoLoginController {
@Value("${front.url}")
private String frontUrl;
private final KakaoLoginService kakaoLoginService;
public KakaoLoginController(KakaoLoginService kakaoLoginService) {
this.kakaoLoginService = kakaoLoginService;
}
@GetMapping("/api/kakao/callback")
public RedirectView loadChampionList(@RequestParam Map params) throws Exception {
Map result = kakaoLoginService.kakaoLogin(params);
String code = (String) result.get("code");
String accessToken = (String) result.get("accessToken");
String refreshToken = (String) result.get("refreshToken");
String adminToken = (String) result.get("adminToken");
String redirectUrl = "";
if ("200".equals(code)) {
redirectUrl = frontUrl + "/login/complete?accessToken=" + accessToken + "&refreshToken=" + refreshToken;
if (!"".equals(adminToken)) {
redirectUrl += "&adminToken=" + adminToken;
}
} else {
redirectUrl = frontUrl + "/login";
}
return new RedirectView(redirectUrl);
}
}
위와같이 작성 후 Service 부분에서 WebClient를 활용하여 토큰을 발급받습니다.
# KakaoLoginService
package com.reroll.rerollback.api.auth.kakao;
import com.reroll.rerollback.api.user.user.UserMapper;
import com.reroll.rerollback.util.CommonUtil;
import com.reroll.rerollback.util.JwtUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.HashMap;
import java.util.Map;
@Service
public class KakaoLoginService {
@Value("${kakao.client_id}")
private String clientId;
@Value("${kakao.auth.url.host}")
private String authUrl;
@Value("${kakao.user.url.host}")
private String userUrl;
private final UserMapper userMapper;
private final JwtUtil jwtUtil;
private final CommonUtil commonUtil;
public KakaoLoginService(UserMapper userMapper, JwtUtil jwtUtil, CommonUtil commonUtil) {
this.userMapper = userMapper;
this.jwtUtil = jwtUtil;
this.commonUtil = commonUtil;
}
public Map kakaoLogin(Map<String, String> params) throws Exception {
Map result = new HashMap();
String code = params.get("code");
Map<String, Object> authResponse = WebClient.create(authUrl).post()
.uri(uriBuilder -> uriBuilder
.path("/oauth/token")
.queryParam("grant_type", "authorization_code")
.queryParam("client_id", clientId)
.queryParam("code", code)
.build())
.header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
.retrieve()
.bodyToMono(Map.class)
.block();
String accessToken = (String) authResponse.get("access_token");
Map<String, Object> userResponse = WebClient.create(userUrl)
.get()
.uri(uriBuilder -> uriBuilder
.path("/v2/user/me")
.build())
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
.retrieve()
.bodyToMono(Map.class)
.block();
Map<String, String> kakaoUserInfo = (Map<String, String>) userResponse.get("kakao_account");
String email = kakaoUserInfo.get("email");
String userType = "user";
String nickname = "";
Map<String, Object> userInfo = userMapper.loadUserInfo(email);
if (userInfo != null) {
userType = (String) userInfo.get("userType");
nickname = (String) userInfo.get("nickname");
userMapper.modifyLastLoginDate(email);
} else {
Map<String, Object> userMap = new HashMap<>();
nickname = commonUtil.makeNickname();
userMap.put("email", email);
userMap.put("loginType", "kakao");
userMap.put("nickname", nickname);
userMapper.addUserInfo(userMap);
}
String newAccessToken = jwtUtil.generateToken(email, userType, nickname, 10800000); // 3시간
String newRefreshToken = jwtUtil.generateToken(email, userType, nickname, 36000000); // 10시간
result.put("code", "200");
result.put("accessToken", newAccessToken);
result.put("refreshToken", newRefreshToken);
return result;
}
}
이후 nuxt에서 전달받은 토큰을 쿠키로 저장한다.
<template>
<div class="container">
<div class="login-div">
<div class="login-div-header">로그인 완료</div>
</div>
</div>
</template>
<script setup>
const { $auth } = useNuxtApp();
const config = useRuntimeConfig();
const route = useRoute()
const accessToken = ref('')
const refreshToken = ref('')
// 페이지 로드 시 쿼리 파라미터 읽기
onMounted(() => {
accessToken.value = route.query.accessToken || ''
refreshToken.value = route.query.refreshToken || ''
// 쿠키 설정
document.cookie = `reroll_AT=${accessToken.value}; SameSite=None; Secure; Max-Age=10800; Path=/`;
document.cookie = `reroll_RT=${refreshToken.value}; SameSite=None; Secure; Max-Age=36000; Path=/`;
})
</script>
+ 심화작업
전달받은 토큰을 통해 API를 제한하거나 페이지 이동을 제한할 수 있다.
1. API 통신 제어
package com.reroll.rerollback.config.interceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
private final TokenInterceptor tokenInterceptor;
public InterceptorConfig(TokenInterceptor tokenInterceptor) {
this.tokenInterceptor = tokenInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/api/check/**");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/check/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
이와 같이 작성하면 /api/check url로 요청 받은 값은 TokenInterceptor 로직을 통해 토큰을 검사한다.
package com.reroll.rerollback.config.interceptor;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.reroll.rerollback.util.JwtUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Map;
@Component
public class TokenInterceptor implements HandlerInterceptor {
private JwtUtil jwtUtil;
public TokenInterceptor(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("accessToken".equals(cookie.getName())) { // adminToken 쿠키 값 확인
String token = cookie.getValue();
// 원하는 로직 개발
}
}
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Admin access only");
return false;
} else {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Admin access only");
return false;
}
}
}
2. nuxt 페이지 이동 시 체크
nuxt는 라우터 이동 시 미들웨어를 통해 함수를 실행할 수 있다.
// middleware/auth.global.js
export default defineNuxtRouteMiddleware(async (to, from) => {
const config = useRuntimeConfig();
const rerollAT = useCookie('reroll_AT');
const rerollRT = useCookie('reroll_RT');
if (process.client) {
if (!rerollAT?.value) {
if (rerollRT?.value) {
const response = await $fetch('/api/token/refresh', {
baseURL: config.public.baseURL,
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.code === '200') {
document.cookie = `reroll_AT=${response.accessToken}; SameSite=None; Secure; Max-Age=10800; Path=/`;
document.cookie = `reroll_RT=${response.refreshToken}; SameSite=None; Secure; Max-Age=36000; Path=/`;
} else {
// 데이터 초기화 로직
}
} else {
// 데이터 초기화 로직
}
}
// 특정 페이지 제한
if (to.path.startsWith('/chcek')) {
try {
if (!rerollAT?.value) {
return navigateTo('/login');
}
} catch (error) {
return navigateTo('/login');
}
}
}
});
access 토큰이 만료된 경우 refresh 토큰이 존재한다면 새로운 토큰을 받아오는 로직을 만든다.
또한 토큰값 존재 여부를 통해 특정 페이지 접속을 제한할 수 있다.
반응형
'프로그래밍 일기 > Java & Spring' 카테고리의 다른 글
JWT란? (0) | 2024.07.09 |
---|---|
자바 스프링 메일 안에 이미지 추가하는 방법 (0) | 2023.07.18 |
[에러해결] javax.mail.MessagingException: Could not connect to SMTP host: smtp.worksmobile.com, port: 465, response: -1 (isSSL true 설정) (0) | 2023.07.18 |
[Spring Boot] 파일 다운로드 로직 구현 feat.Nuxt3 (0) | 2023.07.12 |
[Spring Boot] 단일, 다중 파일 업로드 로직 구현 feat.Nuxt3 (0) | 2023.07.12 |