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

[Refactor] refresh token #92

Merged
merged 3 commits into from
Jan 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 0 additions & 21 deletions src/main/java/yonseigolf/server/config/LoginInterceptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import yonseigolf.server.user.dto.response.LoggedInUser;
import yonseigolf.server.user.exception.DuplicatedLoginException;
import yonseigolf.server.user.service.JwtService;
import yonseigolf.server.user.service.PreventDuplicateLoginService;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

Expand All @@ -19,7 +16,6 @@
public class LoginInterceptor implements HandlerInterceptor {

private final JwtService jwtUtil;
private final PreventDuplicateLoginService preventDuplicateLoginService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
Expand All @@ -33,25 +29,8 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons
String token = request.getHeader("Authorization").split(" ")[1];
LoggedInUser loggedInUser = jwtUtil.extractedUserFromToken(token, LoggedInUser.class);

boolean duplicated = preventDuplicateLoginService.checkDuplicatedLogin(loggedInUser.getId(), token);

if (!duplicated) {

invalidateRefreshToken(response);
throw new DuplicatedLoginException("중복 로그인이 발생했습니다.");
}

request.setAttribute("userId", loggedInUser.getId());

return true;
}

private void invalidateRefreshToken(HttpServletResponse response) {
Cookie cookie = new Cookie("refreshToken", null); // 쿠키 이름을 Refresh Token 쿠키 이름과 동일하게 설정
cookie.setHttpOnly(true);
cookie.setSecure(true); // 프로덕션 환경에서는 true로 설정
cookie.setPath("/"); // Refresh Token 쿠키와 동일한 경로 설정
cookie.setMaxAge(0); // 쿠키의 만료 시간을 0으로 설정하여 즉시 만료
response.addCookie(cookie);
}
}
60 changes: 11 additions & 49 deletions src/main/java/yonseigolf/server/user/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
import yonseigolf.server.user.dto.token.KakaoOauthInfo;
import yonseigolf.server.user.dto.token.OauthToken;
import yonseigolf.server.user.entity.UserClass;
import yonseigolf.server.user.exception.RefreshTokenExpiredException;
import yonseigolf.server.user.exception.RefreshTokenException;
import yonseigolf.server.user.service.JwtService;
import yonseigolf.server.user.service.OauthLoginService;
import yonseigolf.server.user.service.PreventDuplicateLoginService;
import yonseigolf.server.user.service.UserService;
import yonseigolf.server.util.CustomResponse;

Expand All @@ -35,20 +34,18 @@ public class UserController {
private final OauthLoginService oauthLoginService;
private final KakaoOauthInfo kakaoOauthInfo;
private final JwtService jwtUtil;
private final PreventDuplicateLoginService preventDuplicateLoginService;

@Autowired
public UserController(UserService userService, OauthLoginService oauthLoginService, KakaoOauthInfo kakaoOauthInfo, JwtService jwtUtil, PreventDuplicateLoginService preventDuplicateLoginService) {
public UserController(UserService userService, OauthLoginService oauthLoginService, KakaoOauthInfo kakaoOauthInfo, JwtService jwtUtil) {

this.userService = userService;
this.oauthLoginService = oauthLoginService;
this.kakaoOauthInfo = kakaoOauthInfo;
this.jwtUtil = jwtUtil;
this.preventDuplicateLoginService = preventDuplicateLoginService;
}

@PostMapping("/oauth/kakao")
public ResponseEntity<CustomResponse<JwtTokenResponse>> kakaoLogin(@RequestBody KakaoCode kakaoCode) {
public ResponseEntity<CustomResponse<JwtTokenResponse>> kakaoLogin(@RequestBody KakaoCode kakaoCode, HttpServletResponse response) {
OauthToken oauthToken = oauthLoginService.getOauthToken(kakaoCode.getKakaoCode(), kakaoOauthInfo);
KakaoLoginResponse kakaoLoginResponse = oauthLoginService.processKakaoLogin(oauthToken.getAccessToken(), kakaoOauthInfo.getLoginUri());

Expand All @@ -59,6 +56,7 @@ public ResponseEntity<CustomResponse<JwtTokenResponse>> kakaoLogin(@RequestBody
new Date(new Date().getTime() + 360000)
);

createRefreshToken(response, oauthToken.getRefreshToken());

return ResponseEntity
.ok()
Expand All @@ -76,12 +74,6 @@ public ResponseEntity<CustomResponse<JwtTokenResponse>> signIn(@RequestAttribute
Date date = new Date(new Date().getTime() + 3600000);
String tokenReponse = jwtUtil.createToken(loggedInUser, date);

// signIn 할 경우 로그인 진행
makeRefreshToken(response, loggedInUser);

// redis에 중복 로그인 방지를 위한 access token 저장
preventDuplicateLoginService.registerLogin(loggedInUser.getId(), tokenReponse);

return ResponseEntity
.ok()
.body(CustomResponse.successResponse("로그인 성공",
Expand All @@ -91,14 +83,6 @@ public ResponseEntity<CustomResponse<JwtTokenResponse>> signIn(@RequestAttribute
);
}

private void makeRefreshToken(HttpServletResponse response, LoggedInUser loggedInUser) {

Date expireDate = new Date(new Date().getTime() + 1209600000);
String refreshToken = jwtUtil.createRefreshToken(loggedInUser.getId(), expireDate);
userService.saveRefreshToken(loggedInUser.getId(), refreshToken);
createRefreshToken(response, refreshToken);
}

private void createRefreshToken(HttpServletResponse response, String refreshToken) {
Cookie cookie = new Cookie("refreshToken", refreshToken);
cookie.setHttpOnly(true); // HTTP Only 설정
Expand All @@ -120,43 +104,25 @@ public ResponseEntity<CustomResponse<Void>> loggedIn() {
public ResponseEntity<CustomResponse<JwtTokenResponse>> refreshAccessToken(HttpServletRequest request) {

String refreshToken = findRefreshToken(request);
if (refreshToken == null) {
throw new RefreshTokenException("Refresh Token이 존재하지 않습니다.");
}

// refresh token 검증 (null, 만료, 조작)
validateRefreshToken(refreshToken);

JwtTokenUser jwtTokenUser = jwtUtil.extractedUserFromToken(refreshToken, JwtTokenUser.class);
userService.validateRefreshToken(jwtTokenUser.getId(), jwtUtil);
long kakaoId = oauthLoginService.refreshAccessToken(refreshToken, kakaoOauthInfo);
LoggedInUser loggedInUser = userService.signIn(kakaoId);

// access token 재발급
String accessToken = userService.generateAccessToken(jwtTokenUser.getId(), jwtUtil, new Date(new Date().getTime() + 3600000));
preventDuplicateLoginService.registerLogin(jwtTokenUser.getId(), accessToken);
String jwt = jwtUtil.createToken(loggedInUser, new Date(new Date().getTime() + 360000));

return ResponseEntity
.ok()
.body(CustomResponse.successResponse(
"토큰 재발급 성공",
JwtTokenResponse.builder()
.accessToken(accessToken)
.accessToken(jwt)
.build()
));
}

private void validateRefreshToken(String refreshToken) {

validateRefreshTokenNull(refreshToken);

if (!jwtUtil.validateTokenIsManipulated(refreshToken) || !jwtUtil.validateTokenIsExpired(refreshToken)) {
throw new RefreshTokenExpiredException("[ERROR] Refresh Token이 만료되었습니다.");
}
}

private void validateRefreshTokenNull(String refreshToken) {

if (refreshToken == null) {
throw new RefreshTokenExpiredException("[ERROR] Refresh Token이 존재하지 않습니다.");
}
}

private String findRefreshToken(HttpServletRequest request) {
String refreshToken = null;

Expand All @@ -173,13 +139,9 @@ private String findRefreshToken(HttpServletRequest request) {
return refreshToken;
}


@PostMapping("/users/logout")
public ResponseEntity<CustomResponse<Void>> logOut(@RequestAttribute Long userId, HttpServletResponse response) {

// refresh toekn 무효화
userService.invalidateRefreshToken(userId);

// Cookie 삭제
invalidateCookie(response);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,14 @@ public ResponseEntity<CustomErrorResponse> refreshTokenExpired(RefreshTokenExpir
));
}

@ExceptionHandler(DuplicatedLoginException.class)
public ResponseEntity<CustomErrorResponse> duplicatedLogin(DuplicatedLoginException ex) {
@ExceptionHandler(RefreshTokenException.class)
public ResponseEntity<CustomErrorResponse> refreshTokenException(RefreshTokenException ex) {

return ResponseEntity
.status(HttpStatus.CONFLICT)
.status(HttpStatus.UNAUTHORIZED)
.body(new CustomErrorResponse(
"fail",
409,
401,
ex.getMessage()
));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package yonseigolf.server.user.dto.token;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class AccessTokenResponse {


@JsonProperty("token_type")
private String tokenType;
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("expires_in")
private Integer expiresIn;
}
27 changes: 1 addition & 26 deletions src/main/java/yonseigolf/server/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import yonseigolf.server.user.dto.request.SignUpUserRequest;
import yonseigolf.server.user.exception.RefreshTokenExpiredException;
import yonseigolf.server.user.service.JwtService;

import javax.persistence.*;

Expand All @@ -30,11 +28,7 @@ public class User {
private UserRole role;
@Enumerated(EnumType.STRING)
private UserClass userClass;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "refresh_token_id")
private RefreshToken refreshToken;

// TODO: 회원가입 시 멤버로 설정할 것
public static User of(SignUpUserRequest request, Long kakaoId) {

return User.builder()
Expand All @@ -44,7 +38,7 @@ public static User of(SignUpUserRequest request, Long kakaoId) {
.studentId(request.getStudentId())
.major(request.getMajor())
.semester(request.getSemester())
.role(UserRole.OB_ASSISTANT_LEADER)
.role(UserRole.MEMBER)
.userClass(UserClass.NONE)
.build();
}
Expand Down Expand Up @@ -75,25 +69,6 @@ public boolean isMember() {
this.userClass == UserClass.OB;
}

public void validateRefreshToken(JwtService jwtUtil) {
// refresh token이 없을 경우 발급한다.
if (this.refreshToken == null) {
return;
}
// refresh token이 만료된 경우 재발급한다.
this.refreshToken.isBeforeExpired(jwtUtil);
}

public void saveRefreshToken(RefreshToken refreshToken) {

this.refreshToken = refreshToken;
}

public void invalidateRefreshToken() {

this.refreshToken = null;
}

public boolean checkOwner(Long userId) {
return this.id == userId;
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package yonseigolf.server.user.exception;

public class RefreshTokenException extends RuntimeException{

public RefreshTokenException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import yonseigolf.server.user.dto.response.KakaoLoginResponse;
import yonseigolf.server.user.dto.token.AccessTokenResponse;
import yonseigolf.server.user.dto.token.KakaoOauthInfo;
import yonseigolf.server.user.dto.token.OauthToken;

Expand Down Expand Up @@ -68,4 +69,32 @@ private KakaoLoginResponse processLogin(String accessToken, String loginUri) {
KakaoLoginResponse.class)
.getBody();
}

public long refreshAccessToken(String refreshToken, KakaoOauthInfo oauthInfo) {

HttpEntity<?> request = createRefreshRequestEntity(refreshToken, oauthInfo);
ResponseEntity<AccessTokenResponse> refreshResponse = restTemplate.postForEntity(oauthInfo.getRedirectUri(), request, AccessTokenResponse.class);

AccessTokenResponse accessResponse = refreshResponse.getBody();
KakaoLoginResponse kakaoLoginResponse = processKakaoLogin(accessResponse.getAccessToken(), oauthInfo.getLoginUri());

return kakaoLoginResponse.getId();
}

private HttpEntity<?> createRefreshRequestEntity(String refreshToken, KakaoOauthInfo oauthInfo) {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
Map<String, String> header = new HashMap<>();
header.put("Accept", "application/json");
header.put("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
headers.setAll(header);

MultiValueMap<String, String> requestPayloads = new LinkedMultiValueMap<>();
Map<String, String> requestPayload = new HashMap<>();
requestPayload.put("grant_type", "refresh_token");
requestPayload.put("client_id", oauthInfo.getClientId());
requestPayload.put("refresh_token", refreshToken);
requestPayloads.setAll(requestPayload);

return new HttpEntity<>(requestPayloads, headers);
}
}

This file was deleted.

Loading