Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/#146/리프레쉬 토큰 도입 #156

Merged
merged 27 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
aeccc18
refactor : OauthTestController 코드 -> TestController 로 이동
seongjunnoh Sep 8, 2024
9f7c8e7
feat : tokenDTO, enum 클래스 추가
seongjunnoh Sep 8, 2024
7e6e479
refactor : login 메서드에서 jwt 타입별로 토큰 발급 요청하도록 수정
seongjunnoh Sep 8, 2024
c6dec8c
refactor : generateToken 메서드 로직 수정
seongjunnoh Sep 8, 2024
3802066
feat : 로컬 로그인 시 refresh, access token 발급 & response 헤더에 추가
seongjunnoh Sep 8, 2024
d7bdf99
feat : 카카오 로그인 시 refresh token 발급 추가
seongjunnoh Sep 8, 2024
2f56cfd
feat : 카카오 로그인 시 refresh token 정보 db에 저장
seongjunnoh Sep 8, 2024
b38cbd7
feat : access token 만료시 refresh token을 포함한 access token 갱신 요청에 대한 int…
seongjunnoh Sep 8, 2024
068ec55
Revert "feat : 카카오 로그인 시 refresh token 정보 db에 저장"
seongjunnoh Sep 10, 2024
b2b1aa2
feat : 카카오 로그인 시 refresh token db에 저장 & 기존 refresh token 인터셉터 코드 삭제
seongjunnoh Sep 10, 2024
e456315
feat : access token 갱신 요청 api 컨트롤러 코드 추가
seongjunnoh Sep 10, 2024
565d4cd
feat : access token 갱신 요청 api
seongjunnoh Sep 10, 2024
c705e48
refactor : access token, refresh token 생성시 만료시간 value 분리
seongjunnoh Sep 10, 2024
a704a7e
fix : access token, refresh token 시크릿키 분리 & 그에따른 코드 수정
seongjunnoh Sep 10, 2024
06b53d4
fix : 로그인 시 access token, refresh token 서로 반대로 헤더에 주입되는 에러 해결
seongjunnoh Sep 10, 2024
f5df44e
fix : 엑세스 토큰 갱신 요청 헤더의 refresh token 파싱 에러 수정
seongjunnoh Sep 10, 2024
2c42a77
refactor : yml 파일 구조 변경
seongjunnoh Sep 11, 2024
373b392
refactor : refresh token 생성시 payload에 userId 값 제거
seongjunnoh Sep 11, 2024
84de52a
refactor : TokenStorage 엔티티 생성 & refresh token 저장을 해당 엔티티에게 위임
seongjunnoh Sep 11, 2024
5af7702
refactor : access token 갱신 api 수정
seongjunnoh Sep 22, 2024
2c0bf76
fix : BeanCreationException 해결을 위해 ByteBuddy 의존성 추가
seongjunnoh Sep 22, 2024
8f4ae5c
fix : 만료된 access token 으로부터 userId get 할 때 exception 발생하는 에러 해결
seongjunnoh Sep 22, 2024
4044089
fix : 충돌 해결
seongjunnoh Sep 22, 2024
432d54f
refactor : build.gradle 에 추가한 buddy 의존성 제거
seongjunnoh Sep 22, 2024
89a444c
refactor : TokenDTO -> TokenPairDTO DTO 이름 변경
seongjunnoh Sep 23, 2024
d5329d8
refactor : JwtLoginProvider.getUserIdFromToken 메서드 수정 -> 파라미터의 TokenT…
seongjunnoh Sep 25, 2024
8b96674
refactor : user, auth 도메인 생성 & 도메인 구조로 변경
seongjunnoh Sep 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/main/java/space/space_spring/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
public class WebConfig implements WebMvcConfigurer {

private final JwtLoginAuthInterceptor jwtLoginAuthInterceptor;
private final UserSpaceValidationInterceptor userSpaceValidationInterceptor;

private final JwtLoginAuthHandlerArgumentResolver jwtLoginAuthHandlerArgumentResolver;
private final UserSpaceIdHandlerArgumentResolver userSpaceIdHandlerArgumentResolver;
private final UserSpaceAuthHandlerArgumentResolver userSpaceAuthHandlerArgumentResolver;
private final UserSpaceValidationInterceptor userSpaceValidationInterceptor;


@Override
Expand All @@ -45,8 +45,6 @@ public void addInterceptors(InterceptorRegistry registry) {
for(UserSpaceValidationInterceptorURL url:UserSpaceValidationInterceptorURL.values()) {
userSpaceRegistration.addPathPatterns(url.getUrlPattern());
}


}

@Override
Expand Down
57 changes: 45 additions & 12 deletions src/main/java/space/space_spring/controller/OAuthController.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package space.space_spring.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import space.space_spring.dto.jwt.TokenPairDTO;
import space.space_spring.dto.oAuth.KakaoInfo;
import space.space_spring.dto.oAuth.OAuthLoginResponse;
import space.space_spring.entity.User;
import space.space_spring.response.BaseResponse;
import space.space_spring.service.OAuthService;
Expand Down Expand Up @@ -69,18 +67,53 @@ public void kakaoCallback(@RequestParam(name = "code") String code, HttpServletR
User userByOAuthInfo = oAuthService.findUserByOAuthInfo(kakaoInfo);

// TODO 5. 카카오 로그인 유저에게 jwt 발급
String jwtOAuthLogin = oAuthService.provideJwtToOAuthUser(userByOAuthInfo);
response.setHeader("Authorization", "Bearer " + jwtOAuthLogin);
log.info("jwtOAuthLogin = {}", jwtOAuthLogin);
TokenPairDTO tokenPairDTO = oAuthService.provideJwtToOAuthUser(userByOAuthInfo);

// Construct the redirect URL with the JWT and userId as query parameters
// TODO 6. 카카오 로그인 유저에게 발급한 refresh token을 db에 저장
oAuthService.updateRefreshToken(userByOAuthInfo, tokenPairDTO.getRefreshToken());

System.out.println("tokenPairDTO.getAccessToken() = " + tokenPairDTO.getAccessToken());
System.out.println("tokenPairDTO.getRefreshToken() = " + tokenPairDTO.getRefreshToken());

// 클라이언트로 response 전달
// -> 메서드 분리 ??
// 공백문자가 %20 으로 전달되는 듯 함 -> 프론트 분들과 협의 필요할 듯
String redirectUrl = String.format(
"https://kuit-space.github.io/KUIT-Space-front/login?jwt=Bearer %s&userId=%s",
jwtOAuthLogin,
"https://kuit-space.github.io/KUIT-Space-front/login?access-token=%s&refresh-token=%s&userId=%s",
"Bearer " + tokenPairDTO.getAccessToken(),
"Bearer " + tokenPairDTO.getRefreshToken(),
userByOAuthInfo.getUserId()
);

// Redirect to the specified URL
response.sendRedirect(redirectUrl);
}

/**
* 엑세스 토큰 갱신 요청 처리
* -> 엑세스 토큰, 리프레시 토큰 갱신 (RTR 패턴)
*/
@PostMapping("/new-token")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 :

  1. GET 요청은 어떤가요? 내용을 수정한다기 보단 재"발급"을 받는 상황이라 더 적절하지 않을까 싶습니다.
  2. url을 "/token"도 괜찮은 것 같습니다. "자원"의 이름이기도 하고, "재"발급과 헷갈릴 요소도 있을것 같아서

정답은 없는 것 같고 보통 어떻게 사용하는지 저는 모르지만 제안 드려봅니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음 사실 requestBody 가 없어서 Post 메서드를 사용하는게 맞을지 고민을 했지만, Get 은 뭔가 조회의 역할이 강하다고 생각했고, token의 재발급(== 새로운 토큰의 발급 == 새로운 토큰의 생성)이 목적인 api 요청이라 Post 메서드를 사용했긴 합니다.
마찬가지로 재발급이라는 의미를 담아서 url을 new-token 이라 명명해보았습니다.

이건 어디까지나 저의 의견이었고, restful 한 url 을 위해 뭐가 더 적절할 지 조금 더 생각해보겠습니다!
의견 감사합니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 "발급"도 일종의 "조회"(데이터를 가져오는 것)이라고 생각했는데,

  1. GET : 같은 요청을 여러번 해도 같은 결과를 내야함(멱등성)
  2. GET : 데이터를 수정하는 것은 비권장
  3. POST : 데이터를 생성하는가
    의 기준들로 보았을때, POST도 적절하다고 생각합니다.
    /new-token도 의미가 확실한 것 같네요

아무래도 refresh token이 보안 도메인이기도 하고 다른 HTTP 통신들과 다른 흐름을 가지고 있어서 REST API에 딱 맞게 설계하기가 어렵네요

public BaseResponse<String> updateAccessToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
// access token, refresh token 파싱
TokenPairDTO tokenPairDTO = oAuthService.resolveTokenPair(request);

// access token 로부터 user find
User userByAccessToken = oAuthService.getUserByAccessToken(tokenPairDTO.getAccessToken());

// refresh token 유효성 검사
oAuthService.validateRefreshToken(userByAccessToken, tokenPairDTO.getRefreshToken());

// access token, refresh token 새로 발급
TokenPairDTO newTokenPairDTO = oAuthService.updateTokenPair(userByAccessToken);

// response header에 새로 발급한 token pair set
response.setHeader("Authorization-refresh", "Bearer " + newTokenPairDTO.getRefreshToken());
response.setHeader("Authorization", "Bearer " + newTokenPairDTO.getAccessToken());

System.out.println("tokenPairDTO.getAccessToken() = " + newTokenPairDTO.getAccessToken());
System.out.println("tokenPairDTO.getRefreshToken() = " + newTokenPairDTO.getRefreshToken());

// return
return new BaseResponse<>("토큰 갱신 요청 성공");
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package space.space_spring.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
Expand Down Expand Up @@ -52,6 +53,4 @@ public BaseResponse<String> LoginPassAnnotaionTest(
+"");
}



}
12 changes: 7 additions & 5 deletions src/main/java/space/space_spring/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,19 @@
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import space.space_spring.dto.user.GetUserProfileListDto;
import space.space_spring.argumentResolver.jwtLogin.JwtLoginAuth;
import space.space_spring.dto.user.PostLoginDto;
import space.space_spring.dto.user.request.PostUserLoginRequest;
import space.space_spring.dto.user.request.PostUserSignupRequest;
import space.space_spring.dto.user.response.GetSpaceInfoForUserResponse;
import space.space_spring.exception.CustomException;
import space.space_spring.response.BaseResponse;
import space.space_spring.service.UserService;
import space.space_spring.util.userSpace.UserSpaceUtils;

import java.util.stream.Collectors;

import static space.space_spring.response.status.BaseExceptionResponseStatus.*;
import static space.space_spring.util.bindingResult.BindingResultUtils.getErrorMessage;

Expand Down Expand Up @@ -56,7 +52,12 @@ public BaseResponse<PostLoginDto.Response> login(@Validated @RequestBody PostLog
}

PostLoginDto login = userService.login(request);
response.setHeader("Authorization", "Bearer " + login.getJwt());

System.out.println("login.getTokenPairDTO().getRefreshToken() = " + login.getTokenPairDTO().getRefreshToken());
System.out.println("login.getTokenPairDTO().getAccessToken() = " + login.getTokenPairDTO().getAccessToken());

response.setHeader("Authorization-refresh", "Bearer " + login.getTokenPairDTO().getRefreshToken());
response.setHeader("Authorization", "Bearer " + login.getTokenPairDTO().getAccessToken());

return new BaseResponse<>(new PostLoginDto.Response(login.getUserId()));
}
Expand All @@ -81,4 +82,5 @@ public BaseResponse<GetUserProfileListDto.Response> showUserProfileList(@JwtLogi

return new BaseResponse<>(userService.getUserProfileList(userId));
}

}
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
package space.space_spring.controller;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/oauth")
@Slf4j
public class TestOAuthController {
public class ViewTestController {

/**
* 카카오 로그인 요청 처리
* 카카오 인증 서버의 인증 및 동의 요청 페이지로 redirect
*/
@Value("${oauth.kakao.client.id}")
private String clientId;

@Value("${oauth.kakao.redirect.uri}")
private String redirectUri;

/**
* 카카오 로그인 요청 처리
* 카카오 인증 서버의 인증 및 동의 요청 페이지로 redirect
*/
@GetMapping("/kakao")
@GetMapping("/oauth/kakao")
public String kakaoConnect() {
StringBuffer url = new StringBuffer();
url.append("https://kauth.kakao.com/oauth/authorize?");
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/space/space_spring/dao/JwtRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package space.space_spring.dao;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import space.space_spring.entity.TokenStorage;
import space.space_spring.entity.User;

import java.util.Optional;

@Repository
public interface JwtRepository extends JpaRepository<TokenStorage, Long> {

Optional<TokenStorage> findByUser(User user);
void deleteByUser(User user);
}
13 changes: 13 additions & 0 deletions src/main/java/space/space_spring/dao/UserDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import space.space_spring.entity.User;
import space.space_spring.entity.enumStatus.UserSignupType;

import java.util.Optional;

@Repository
public class UserDao {

Expand Down Expand Up @@ -53,4 +55,15 @@ public User findUserByUserId(Long userId) {
return em.find(User.class, userId);
}

public Optional<User> findUserByRefreshToken(String refreshToken) {
String jpql = "SELECT u FROM User u WHERE u.refreshToken = :refreshToken AND u.status = 'ACTIVE'";
TypedQuery<User> query = em.createQuery(jpql, User.class);
query.setParameter("refreshToken", refreshToken);

try {
return Optional.of(query.getSingleResult());
} catch (NoResultException e) {
return Optional.empty();
}
}
}
15 changes: 15 additions & 0 deletions src/main/java/space/space_spring/dao/UserRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package space.space_spring.dao;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import space.space_spring.entity.User;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

@Query("SELECT u FROM User u WHERE u.userId = :userId AND u.status = 'ACTIVE'")
Optional<User> findByUserId(Long userId);
}
30 changes: 0 additions & 30 deletions src/main/java/space/space_spring/dto/jwt/JwtPayloadDto.java

This file was deleted.

18 changes: 0 additions & 18 deletions src/main/java/space/space_spring/dto/jwt/JwtUserSpaceAuthDto.java

This file was deleted.

16 changes: 16 additions & 0 deletions src/main/java/space/space_spring/dto/jwt/TokenPairDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package space.space_spring.dto.jwt;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TokenPairDTO {

private String refreshToken;
private String accessToken;
}
6 changes: 6 additions & 0 deletions src/main/java/space/space_spring/dto/jwt/TokenType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package space.space_spring.dto.jwt;

public enum TokenType {
REFRESH,
ACCESS
}
10 changes: 4 additions & 6 deletions src/main/java/space/space_spring/dto/user/PostLoginDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.*;
import space.space_spring.dto.jwt.TokenPairDTO;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PostLoginDto {

private String jwt;

private TokenPairDTO TokenPairDTO;
private Long userId;

@Getter
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/space/space_spring/entity/TokenStorage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package space.space_spring.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "Token_Storage")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TokenStorage {

@Id @GeneratedValue
@Column(name = "token_storage_id")
private Long tokenStorageId;

@OneToOne
// @Column(name = "user_id")
private User user;
Comment on lines +15 to +23
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 : findByUser이 주로 사용될 것이 분명하기 때문에 User을 index 설정 해주는 것이 성능 향상에 큰 도움이 될 것이라고 생각합니다.
JPA에서 인덱스 설정하는 블로그 포스트 첨부합니다
https://velog.io/@ljinsk3/JPA%EB%A1%9C-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 안그래도 저희가 아직 redis 를 도입하지않아 token 같이 자주 변경되는 값을 저장할때의 성능에 대해 의문이 있었는데 첨부해주신 블로그 한번 참고해보겠습니다!
추가로 refresh token이 유효하지 않다면 그냥 프론트단으로 예외만 던지기보다 아예 해당 tuple을 지워버리는것이 db 용량측면에서 낫다고 생각해서 JpaRepo 의 delete 메서드를 호출하기는 합니다.
다만 아직 테스트 코드를 작성하기 전이라 이 부분도 추후에 테스트 코드를 통해 검증해보겠습니다


@Column(name = "token_value")
private String tokenValue;

public void updateTokenValue(String tokenValue) {
this.tokenValue = tokenValue;
}

public boolean checkTokenValue(String tokenValue) {
return this.tokenValue.equals(tokenValue);
}

}
Loading
Loading