Skip to content

Commit

Permalink
feat: 리프래쉬 토큰을 쿠키로 저장하도록 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
hjk0761 committed Jan 17, 2025
1 parent 928f532 commit 99b9a58
Show file tree
Hide file tree
Showing 11 changed files with 109 additions and 11 deletions.
6 changes: 5 additions & 1 deletion backend/src/main/java/corea/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import corea.auth.interceptor.AuthorizationInterceptor;
import corea.auth.resolver.AccessedMemberArgumentResolver;
import corea.auth.resolver.LoginMemberArgumentResolver;
import corea.auth.resolver.RefreshTokenArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
Expand All @@ -13,6 +14,7 @@
import java.util.List;

import static corea.global.config.Constants.AUTHORIZATION_HEADER;
import static org.springframework.http.HttpHeaders.SET_COOKIE;

@Configuration
@RequiredArgsConstructor
Expand All @@ -21,6 +23,7 @@ public class WebConfig implements WebMvcConfigurer {
private final LoginMemberArgumentResolver loginMemberArgumentResolver;
private final AccessedMemberArgumentResolver accessedMemberArgumentResolver;
private final AuthorizationInterceptor authorizationInterceptor;
private final RefreshTokenArgumentResolver refreshTokenArgumentResolver;

@Override
public void addCorsMappings(CorsRegistry registry) {
Expand All @@ -30,14 +33,15 @@ public void addCorsMappings(CorsRegistry registry) {
"https://dev.code-review-area.com/")
.allowedMethods("GET", "POST", "DELETE", "PUT")
.allowCredentials(true)
.exposedHeaders(AUTHORIZATION_HEADER)
.exposedHeaders(AUTHORIZATION_HEADER, SET_COOKIE)
.maxAge(3000);
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginMemberArgumentResolver);
resolvers.add(accessedMemberArgumentResolver);
resolvers.add(refreshTokenArgumentResolver);
}

@Override
Expand Down
14 changes: 14 additions & 0 deletions backend/src/main/java/corea/auth/annotation/RefreshToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package corea.auth.annotation;

import io.swagger.v3.oas.annotations.Hidden;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Hidden()
public @interface RefreshToken {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package corea.auth.controller;

import corea.auth.annotation.LoginMember;
import corea.auth.annotation.RefreshToken;
import corea.auth.domain.AuthInfo;
import corea.auth.domain.TokenInfo;
import corea.auth.dto.GithubUserInfo;
Expand All @@ -20,6 +21,7 @@
import org.springframework.web.bind.annotation.RestController;

import static corea.global.config.Constants.AUTHORIZATION_HEADER;
import static org.springframework.http.HttpHeaders.SET_COOKIE;

@RestController
@RequestMapping
Expand All @@ -39,11 +41,12 @@ public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest loginReques

return ResponseEntity.ok()
.header(AUTHORIZATION_HEADER, tokenInfo.accessToken())
.body(new LoginResponse(tokenInfo.refreshToken(), userInfo, memberRoleResponse.role()));
.header(SET_COOKIE, tokenInfo.refreshToken().toString())
.body(new LoginResponse(userInfo, memberRoleResponse.role()));
}

@PostMapping("/refresh")
public ResponseEntity<Void> extendAuthorization(@RequestBody TokenRefreshRequest tokenRefreshRequest) {
public ResponseEntity<Void> extendAuthorization(@RefreshToken TokenRefreshRequest tokenRefreshRequest) {
String accessToken = loginService.refresh(tokenRefreshRequest.refreshToken());
return ResponseEntity.ok()
.header(AUTHORIZATION_HEADER, accessToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public interface LoginControllerSpecification {
@Operation(summary = "로그인을 유지합니다.",
description = "로그인 되어 있는 상태에서 액세스 토큰의 유효기간이 만료되었을 경우 리프레시 토큰을 이용해 새로운 액세스 토큰을 부여받습니다. <br>" +
"리프레시 토큰은 서버와 클라이언트 양쪽에서 저장하며 API 요청 시 두 값을 비교한 후 동일한 경우에만 액세스 토큰을 부여합니다.")
@ApiErrorResponses(value = {ExceptionType.TOKEN_EXPIRED, ExceptionType.INVALID_TOKEN},
@ApiErrorResponses(value = {ExceptionType.TOKEN_EXPIRED, ExceptionType.INVALID_TOKEN, ExceptionType.COOKIE_NOT_EXIST},
groups = ExceptionTypeGroup.INTERNAL_SERVER_ERROR)
ResponseEntity<Void> extendAuthorization(TokenRefreshRequest tokenRefreshRequest);

Expand Down
4 changes: 3 additions & 1 deletion backend/src/main/java/corea/auth/domain/TokenInfo.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package corea.auth.domain;

public record TokenInfo(String accessToken, String refreshToken) {
import org.springframework.http.ResponseCookie;

public record TokenInfo(String accessToken, ResponseCookie refreshToken) {
}
1 change: 0 additions & 1 deletion backend/src/main/java/corea/auth/dto/LoginResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

@Schema(description = "로그인/로그인 유지를 위한 정보 전달")
public record LoginResponse(@Schema(description = "리프레시 JWT 토큰", example = "O1234567COREAREFRESH")
String refreshToken,
GithubUserInfo userInfo,
String memberRole) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package corea.auth.resolver;

import corea.auth.annotation.RefreshToken;
import corea.auth.dto.TokenRefreshRequest;
import corea.auth.service.CookieService;
import corea.exception.CoreaException;
import corea.exception.ExceptionType;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import static corea.global.config.Constants.REFRESH_COOKIE;

@Component
@RequiredArgsConstructor
public class RefreshTokenArgumentResolver implements HandlerMethodArgumentResolver {

private final CookieService cookieService;

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RefreshToken.class);
}

@Override
public TokenRefreshRequest resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
Cookie[] cookies = request.getCookies();
if (cookies == null) {
throw new CoreaException(ExceptionType.COOKIE_NOT_EXIST);
}
return new TokenRefreshRequest(cookieService.getCookieValue(cookies, REFRESH_COOKIE)
.orElseThrow(() -> new CoreaException(ExceptionType.COOKIE_NOT_EXIST)));
}
}
29 changes: 29 additions & 0 deletions backend/src/main/java/corea/auth/service/CookieService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package corea.auth.service;

import jakarta.servlet.http.Cookie;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.Optional;

@Service
public class CookieService {

public ResponseCookie createCookie(String name, String value, long maxAge) {
return ResponseCookie.from(name, value)
.httpOnly(true)
.secure(true)
.sameSite("Lax")
.path("/")
.maxAge(maxAge)
.build();
}

public Optional<String> getCookieValue(Cookie[] cookies, String name) {
return Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals(name))
.map(Cookie::getValue)
.findFirst();
}
}
10 changes: 7 additions & 3 deletions backend/src/main/java/corea/auth/service/LoginService.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
import corea.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import static corea.exception.ExceptionType.INVALID_TOKEN;
import static corea.exception.ExceptionType.TOKEN_EXPIRED;
import static corea.global.config.Constants.COOKIE_EXPIRATION;
import static corea.global.config.Constants.REFRESH_COOKIE;

@Slf4j
@Service
Expand All @@ -25,14 +28,15 @@ public class LoginService {
private final MemberRepository memberRepository;
private final TokenService tokenService;
private final LogoutService logoutService;
private final CookieService cookieService;

@Transactional
public TokenInfo login(GithubUserInfo userInfo) {
Member member = memberRepository.findByGithubUserId(userInfo.id())
.orElseGet(() -> register(userInfo));

String accessToken = tokenService.createAccessToken(member);
String refreshToken = extendAuthorization(member);
ResponseCookie refreshToken = extendAuthorization(member);
return new TokenInfo(accessToken, refreshToken);
}

Expand All @@ -46,14 +50,14 @@ private void logCreateMembers(Member member) {
log.info("멤버를 생성했습니다. 멤버 id={}, 멤버 이름={},깃허브 id={}, 닉네임={}", member.getId(), member.getName(), member.getGithubUserId(), member.getUsername());
}

private String extendAuthorization(Member member) {
private ResponseCookie extendAuthorization(Member member) {
String refreshToken = tokenService.createRefreshToken(member);
loginInfoRepository.findByMemberId(member.getId())
.ifPresentOrElse(
loginInfo -> loginInfoRepository.save(loginInfo.changeRefreshToken(refreshToken)),
() -> loginInfoRepository.save(new LoginInfo(member, refreshToken))
);
return refreshToken;
return cookieService.createCookie(REFRESH_COOKIE, refreshToken, COOKIE_EXPIRATION);
}

@Transactional
Expand Down
2 changes: 2 additions & 0 deletions backend/src/main/java/corea/global/config/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public class Constants {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String ANONYMOUS = "ANONYMOUS";
public static final String TOKEN_TYPE = "Bearer ";
public static final String REFRESH_COOKIE = "refreshToken";
public static final long COOKIE_EXPIRATION = 1209600;
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ void extendAuthorization() {
"https://gongu.copyright.or.kr/",
"98307410"
));

assertThat(tokenInfo.refreshToken()).isNotEqualTo(refreshToken);
assertThat(tokenInfo.refreshToken().getValue()).isNotEqualTo(refreshToken);
}

@Test
Expand Down

0 comments on commit 99b9a58

Please sign in to comment.