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(#45): redis 모듈 추가 #49

Merged
merged 20 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
15ac500
feat(#45): refresh 임시 구현 - Entity 아직 사용 X 수정 예정
jusung-c Sep 17, 2023
bbc3b20
(#45)-RefreshToken 엔티티 & RefreshTokenRepository 구현
jusung-c Sep 19, 2023
855b88b
Merge branch 'refactor(#46)-refreshToken-state-redis' into feat(#45)-…
jusung-c Sep 19, 2023
1343c08
refactor(#45): Refresh Token 구현 수정
jusung-c Sep 20, 2023
3123204
refactor(#46): generateAccessToken 메소드명 변경에 따른 수정
jusung-c Sep 20, 2023
99e38a2
refactor(#45): generateAccessToken 메소드명 변경에 따른 수정
jusung-c Sep 20, 2023
4dcd0bb
test(#45): RefreshTokenRepository 테스트
jusung-c Sep 20, 2023
ec944db
feat(#45): Logout 구현 - Logout 시 Redis에서 RefreshToken 삭제
jusung-c Sep 20, 2023
c9a0b67
test(#45): RefreshTokenService 테스트
jusung-c Sep 20, 2023
0ac592c
Merge remote-tracking branch 'upstream/feat(#45)-heachi-domain-redis'…
jusung-c Sep 20, 2023
e76e51f
Merge branch 'dev' into feat(#45)-heachi-domain-redis
ghdcksgml1 Sep 21, 2023
c4c5801
refactor(#45): refersh 예외 JwtException으로 처리
jusung-c Sep 21, 2023
cc51c39
Merge remote-tracking branch 'upstream/feat(#45)-heachi-domain-redis'…
jusung-c Sep 21, 2023
b3a4c77
refactor(#45):
jusung-c Sep 21, 2023
56b0c57
refactor(#45): 빌드 에러 수정
jusung-c Sep 21, 2023
6504974
refactor(#45): 헤더에서 토큰 추출시 생길 수 있는 OutOfBound 에러 방지
jusung-c Sep 23, 2023
da2175b
feat(#45): AccessToken 재발급 일부 구현
jusung-c Sep 23, 2023
713cda3
refactor(#45): AccessToken 재발급 수정 & 테스트
jusung-c Sep 23, 2023
6a0ceee
refactor(#45): AccessToken 재발급 수정 & 테스트
jusung-c Sep 25, 2023
27c64a8
refactor(#45): 피드백 부분 수정
jusung-c Sep 26, 2023
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
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.heachi.auth.api.controller.auth;

import com.heachi.admin.common.exception.ExceptionMessage;
import com.heachi.admin.common.exception.jwt.JwtException;
import com.heachi.admin.common.exception.oauth.OAuthException;
import com.heachi.admin.common.response.JsonResult;
import com.heachi.auth.api.controller.auth.request.AuthRegisterRequest;
import com.heachi.auth.api.controller.auth.response.UserSimpleInfoResponse;
import com.heachi.auth.api.controller.token.response.ReissueAccessTokenResponse;
import com.heachi.auth.api.service.auth.AuthService;
import com.heachi.auth.api.service.auth.request.AuthServiceRegisterRequest;
import com.heachi.auth.api.service.auth.response.AuthServiceLoginResponse;
Expand Down Expand Up @@ -74,10 +76,12 @@ public JsonResult<?> logout(@RequestHeader(name = "Authorization") String token)

if (tokens.size() == 3) {
authService.logout(tokens.get(2));

return JsonResult.successOf("Logout successfully.");
} else {
return JsonResult.successOf(ExceptionMessage.JWT_INVALID_HEADER.getText());
return JsonResult.failOf(ExceptionMessage.JWT_INVALID_HEADER.getText());
}

}

@PostMapping("/delete")
Expand All @@ -87,12 +91,16 @@ public JsonResult<?> userDelete(@AuthenticationPrincipal User user) {
return JsonResult.successOf();
}

// @PostMapping("/reissue")
// public JsonResult<?> reissueAccessToken(
// @RequestParam("refreshToken") String refreshToken) {
//
// AuthServiceLoginResponse reissueResponse = authService.reissueAccessToken(refreshToken);
//
// return JsonResult.successOf(reissueResponse);
// }
@PostMapping("/reissue")
public JsonResult<?> reissueAccessToken(@RequestHeader(name = "Authorization") String token) {
List<String> tokens = Arrays.asList(token.split(" "));

if (tokens.size() == 3) {
ReissueAccessTokenResponse reissueResponse = authService.reissueAccessToken(tokens.get(2));

return JsonResult.successOf(reissueResponse);
} else {
return JsonResult.failOf(ExceptionMessage.JWT_INVALID_HEADER.getText());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.heachi.auth.api.controller.token.response;

import lombok.Builder;
import lombok.Getter;

@Getter
public class ReissueAccessTokenResponse {
private String accessToken;
private String refreshToken;

@Builder
public ReissueAccessTokenResponse(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.heachi.admin.common.exception.ExceptionMessage;
import com.heachi.admin.common.exception.auth.AuthException;
import com.heachi.admin.common.exception.jwt.JwtException;
import com.heachi.auth.api.controller.token.response.ReissueAccessTokenResponse;
import com.heachi.auth.api.service.auth.request.AuthServiceRegisterRequest;
import com.heachi.auth.api.service.auth.response.AuthServiceLoginResponse;
import com.heachi.auth.api.service.jwt.JwtService;
Expand Down Expand Up @@ -73,6 +75,7 @@ public AuthServiceLoginResponse login(UserPlatformType platformType, String code
.role(findUser.getRole())
.build();
}

@Transactional
public void logout(String refreshToken) {
refreshTokenService.logout(refreshToken);
Expand Down Expand Up @@ -146,16 +149,22 @@ public void userDelete(String email) {
}
}

// public AuthServiceLoginResponse reissueAccessToken(String refreshToken) {
// // 리프레시 토큰을 이용해 새로운 엑세스 토큰 발급
// Claims claims = jwtService.extractAllClaims(refreshToken);
// UserRole role = claims.get("role", UserRole.class);
// String accessToken = refreshTokenService.reissue(claims, refreshToken);
//
// return AuthServiceLoginResponse.builder()
// .accessToken(accessToken)
// .refreshToken(refreshToken)
// .role(role)
// .build();
// }
public ReissueAccessTokenResponse reissueAccessToken(String refreshToken) {
Claims claims = jwtService.extractAllClaims(refreshToken);

// 토큰 검증
if (jwtService.isTokenValid(refreshToken, claims.getSubject())) {
// 리프레시 토큰을 이용해 새로운 엑세스 토큰 발급
String accessToken = refreshTokenService.reissue(claims, refreshToken);

return ReissueAccessTokenResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();

} else {
throw new JwtException(ExceptionMessage.JWT_INVALID_RTK);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ public void saveRefreshToken(RefreshToken refreshToken) {
}

public String reissue(Claims claims, String refreshToken) {
// 토큰 유효성 한번 더 검증
if (!jwtService.isTokenValid(refreshToken, claims.getSubject())) {
throw new JwtException(ExceptionMessage.JWT_INVALID_RTK);
// 레디스에 존재하는지 확인
if (refreshTokenRepository.findById(refreshToken).isEmpty()) {
Copy link
Owner

Choose a reason for hiding this comment

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

기존에 모든 Optional이 Null일때 orElseThrow를 썼는데 통일하는게 좋아보입니다. ㅎ.ㅎ

throw new JwtException(ExceptionMessage.JWT_NOT_EXIST_RTK);
}

String role = claims.get("role", String.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
} catch (ExpiredJwtException e) {
logger.error("Could not set user authentication in security context {}", e);

// 엑세스 토큰 재발급 API인 /auth/reissue로 전달하는 코드
// 헤더에서 토큰 추출 - 잘못된 헤더면 이미 try문에서 걸러졌을 것
List<String> tokens = Arrays.asList(authHeader.split(" "));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti

return http.build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ void logoutWhenInvalidHeader() throws Exception {
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "aa bb cc dd"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.resCode").value(200))
.andExpect(jsonPath("$.resObj").value(ExceptionMessage.JWT_INVALID_HEADER.getText()));
.andExpect(jsonPath("$.resCode").value(400))
.andExpect(jsonPath("$.resMsg").value(ExceptionMessage.JWT_INVALID_HEADER.getText()));
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
package com.heachi.auth.api.service.token;

import com.heachi.admin.common.exception.ExceptionMessage;
import com.heachi.auth.api.controller.auth.AuthController;
import com.heachi.auth.api.controller.token.response.ReissueAccessTokenResponse;
import com.heachi.auth.api.service.auth.AuthService;
import com.heachi.auth.api.service.jwt.JwtService;
import com.heachi.auth.api.service.oauth.OAuthService;
import com.heachi.mysql.define.user.User;
import com.heachi.mysql.define.user.constant.UserRole;
import com.heachi.mysql.define.user.repository.UserRepository;
import com.heachi.redis.define.refreshToken.RefreshToken;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import java.util.HashMap;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

Expand All @@ -31,18 +39,23 @@ public class AccessTokenReissueTest {
@Autowired
private MockMvc mockMvc;

@Autowired
private RefreshTokenService refreshTokenService;

@Autowired
private UserRepository userRepository;

@Autowired
private JwtService jwtService;

@Autowired
private RefreshTokenService refreshTokenService;
@AfterEach
void tearDown() {
userRepository.deleteAllInBatch();
}

@Test
@DisplayName("AccessToken 만료시 재발급 테스트")
void redisRefreshTokenGenerate() throws Exception {
@DisplayName("AccessToken 재발급 성공 테스트")
void reissueSuccess() throws Exception {
// given
User user = User.builder()
.name("김민수")
Expand All @@ -56,22 +69,62 @@ void redisRefreshTokenGenerate() throws Exception {
map.put("role", savedUser.getRole().name());
map.put("name", savedUser.getName());
map.put("profileImageUrl", savedUser.getProfileImageUrl());

String expiredAccessToken = jwtService.generateExpiredAccessToken(map, savedUser);
String accessToken = jwtService.generateAccessToken(map, savedUser);
String refreshToken = jwtService.generateRefreshToken(map, savedUser);

refreshTokenService.saveRefreshToken(RefreshToken.builder().refreshToken(refreshToken).email(user.getEmail()).build());


// when
mockMvc.perform(get("/auth/info")
mockMvc.perform(post("/auth/reissue")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + expiredAccessToken + " " + refreshToken))
.header("Authorization", "Bearer " + accessToken + " " + refreshToken))

// then
.andExpect(status().isOk())
.andExpect(jsonPath("$.resCode").value(200))
.andExpect(jsonPath("$.resObj.role").value("USER"))
.andExpect(jsonPath("$.resObj.name").value("김민수"))
.andExpect(jsonPath("$.resObj.email").value("[email protected]"))
.andExpect(jsonPath("$.resObj.profileImageUrl").value("https://google.com"));
.andDo(print());
}

@Test
@DisplayName("잘못된 헤더로 재발급 요청시 JWT_INVALID_HEADER 예외가 터져야 한다.")
void reissueFailWithInvalidHeader() throws Exception {
mockMvc.perform(post("/auth/reissue")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "aa bb cc dd"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.resCode").value(400))
.andExpect(jsonPath("$.resMsg").value(ExceptionMessage.JWT_INVALID_HEADER.getText()));
}

@Test
@DisplayName("존재하지 않는 리프레시 토큰으로 재발급 요청시 JWT_NOT_EXIST_RTK 예외가 터져야 한다.")
void reissueFailWithNotExistRtk() throws Exception {

// given
User user = User.builder()
.name("김민수")
.role(UserRole.USER)
.email("[email protected]")
.profileImageUrl("https://google.com")
.build();
User savedUser = userRepository.save(user);

HashMap<String, String> map = new HashMap<>();
map.put("role", savedUser.getRole().name());
map.put("name", savedUser.getName());
map.put("profileImageUrl", savedUser.getProfileImageUrl());
String accessToken = jwtService.generateAccessToken(map, savedUser);
String refreshToken = jwtService.generateRefreshToken(map, savedUser);

// 레디스 저장 부분 주석처리
// refreshTokenService.saveRefreshToken(RefreshToken.builder().refreshToken(refreshToken).email(user.getEmail()).build());

mockMvc.perform(post("/auth/reissue")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "aa " + accessToken + " " + refreshToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.resCode").value(400))
.andExpect(jsonPath("$.resMsg").value(ExceptionMessage.JWT_NOT_EXIST_RTK.getText()));
}
}