-
Notifications
You must be signed in to change notification settings - Fork 1
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: accessToken 재발급 #155
Changes from 3 commits
038b9ed
c2709da
6402566
2d778a7
52499bc
f03034f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package in.koreatech.koin.domain.auth; | ||
|
||
import static java.lang.annotation.ElementType.FIELD; | ||
import static java.lang.annotation.ElementType.PARAMETER; | ||
import static java.lang.annotation.RetentionPolicy.RUNTIME; | ||
|
||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.Target; | ||
|
||
@Target({PARAMETER, FIELD}) | ||
@Retention(RUNTIME) | ||
public @interface UserAuth { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package in.koreatech.koin.domain.auth.resolver; | ||
|
||
import in.koreatech.koin.domain.auth.JwtProvider; | ||
import in.koreatech.koin.domain.auth.UserAuth; | ||
import in.koreatech.koin.domain.auth.exception.AuthException; | ||
import in.koreatech.koin.domain.user.exception.UserNotFoundException; | ||
import in.koreatech.koin.domain.user.repository.UserRepository; | ||
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; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class UserArgumentResolver implements HandlerMethodArgumentResolver { | ||
|
||
private static final String AUTHORIZATION = "Authorization"; | ||
|
||
private final JwtProvider jwtProvider; | ||
private final UserRepository userRepository; | ||
|
||
@Override | ||
public boolean supportsParameter(MethodParameter parameter) { | ||
return parameter.hasParameterAnnotation(UserAuth.class); | ||
} | ||
|
||
@Override | ||
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, | ||
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { | ||
|
||
HttpServletRequest nativeRequest = webRequest.getNativeRequest(HttpServletRequest.class); | ||
if (nativeRequest == null) { | ||
throw new AuthException("요청 값이 비어있습니다."); | ||
} | ||
|
||
String authorizationHeader = nativeRequest.getHeader(AUTHORIZATION); | ||
if (authorizationHeader == null) { | ||
throw new AuthException("인증 헤더값이 비어있습니다. authorizationHeader: " + nativeRequest); | ||
} | ||
Long userId = jwtProvider.getUserId(authorizationHeader); | ||
return userRepository.findById(userId) | ||
.orElseThrow(() -> UserNotFoundException.withDetail("authorizationHeader: " + authorizationHeader)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,19 @@ | ||
package in.koreatech.koin.domain.user.controller; | ||
|
||
import java.net.URI; | ||
|
||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import in.koreatech.koin.domain.auth.UserAuth; | ||
import in.koreatech.koin.domain.user.dto.UserLoginRequest; | ||
import in.koreatech.koin.domain.user.dto.UserLoginResponse; | ||
import in.koreatech.koin.domain.user.dto.UserTokenRefreshRequest; | ||
import in.koreatech.koin.domain.user.dto.UserTokenRefreshResponse; | ||
import in.koreatech.koin.domain.user.model.User; | ||
import in.koreatech.koin.domain.user.service.UserService; | ||
import jakarta.validation.Valid; | ||
import java.net.URI; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
|
@@ -25,4 +27,19 @@ public ResponseEntity<UserLoginResponse> login(@RequestBody @Valid UserLoginRequ | |
return ResponseEntity.created(URI.create("/")) | ||
.body(response); | ||
} | ||
|
||
@PostMapping("/user/logout") | ||
public ResponseEntity<Void> logout(@UserAuth User user) { | ||
userService.logout(user); | ||
return ResponseEntity.ok().build(); | ||
} | ||
|
||
@PostMapping("/user/refresh") | ||
public ResponseEntity<UserTokenRefreshResponse> refresh( | ||
@UserAuth User user, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. R
인증 헤더 필요 유무에 대한 의견 부탁드립니다~! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 상황에 대해 저는 2번 방법이 좋을 것 같습니다!
refresh token만 검증하면 관리나 검증 로직이 간편하기 때문입니다. |
||
@RequestBody @Valid UserTokenRefreshRequest request | ||
) { | ||
UserTokenRefreshResponse tokenGroupResponse = userService.refresh(user, request); | ||
return ResponseEntity.ok().body(tokenGroupResponse); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package in.koreatech.koin.domain.user.dto; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; | ||
import com.fasterxml.jackson.databind.annotation.JsonNaming; | ||
import jakarta.validation.constraints.NotNull; | ||
|
||
@JsonNaming(value = SnakeCaseStrategy.class) | ||
public record UserTokenRefreshRequest( | ||
@JsonProperty("refresh_token") @NotNull(message = "refresh_token을 입력해주세요.") String refreshToken | ||
) { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package in.koreatech.koin.domain.user.dto; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
||
public record UserTokenRefreshResponse( | ||
@JsonProperty("token") String accessToken, | ||
@JsonProperty("refresh_token") String refreshToken | ||
) { | ||
|
||
public static UserTokenRefreshResponse of(String accessToken, String refreshToken) { | ||
return new UserTokenRefreshResponse(accessToken, refreshToken); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,14 @@ | ||
package in.koreatech.koin.domain.user.repository; | ||
|
||
import in.koreatech.koin.domain.user.model.UserToken; | ||
import java.util.Optional; | ||
|
||
import org.springframework.data.repository.Repository; | ||
|
||
import in.koreatech.koin.domain.user.model.UserToken; | ||
|
||
public interface UserTokenRepository extends Repository<UserToken, Long> { | ||
|
||
UserToken save(UserToken userToken); | ||
|
||
Optional<UserToken> findById(Long userId); | ||
|
||
void deleteById(Long id); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,9 @@ | |
import io.restassured.http.ContentType; | ||
import io.restassured.response.ExtractableResponse; | ||
import io.restassured.response.Response; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import org.assertj.core.api.Assertions; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
|
@@ -73,4 +76,117 @@ void userLoginSuccess() { | |
} | ||
); | ||
} | ||
|
||
@Test | ||
@DisplayName("사용자가 로그인 이후 로그아웃을 수행한다") | ||
void userLogoutSuccess() { | ||
User user = User.builder() | ||
.password("1234") | ||
.nickname("주노") | ||
.name("최준호") | ||
.phoneNumber("010-1234-5678") | ||
.userType(UserType.USER) | ||
.email("[email protected]") | ||
.isAuthed(true) | ||
.isDeleted(false) | ||
.build(); | ||
|
||
userRepository.save(user); | ||
|
||
ExtractableResponse<Response> response = RestAssured | ||
.given() | ||
.log().all() | ||
.body(""" | ||
{ | ||
"email": "[email protected]", | ||
"password": "1234" | ||
} | ||
""") | ||
.contentType(ContentType.JSON) | ||
.when() | ||
.log().all() | ||
.post("/user/login") | ||
.then() | ||
.log().all() | ||
.statusCode(HttpStatus.CREATED.value()) | ||
.extract(); | ||
|
||
RestAssured | ||
.given() | ||
.log().all() | ||
.header("Authorization", "BEARER " + response.jsonPath().getString("token")) | ||
.when() | ||
.log().all() | ||
.post("/user/logout") | ||
.then() | ||
.log().all() | ||
.statusCode(HttpStatus.OK.value()) | ||
.extract(); | ||
|
||
Optional<UserToken> token = tokenRepository.findById(user.getId()); | ||
|
||
Assertions.assertThat(token).isEmpty(); | ||
} | ||
|
||
@Test | ||
@DisplayName("사용자가 로그인 이후 refreshToken을 재발급한다") | ||
void userRefreshToken() { | ||
User user = User.builder() | ||
.password("1234") | ||
.nickname("주노") | ||
.name("최준호") | ||
.phoneNumber("010-1234-5678") | ||
.userType(UserType.USER) | ||
.email("[email protected]") | ||
.isAuthed(true) | ||
.isDeleted(false) | ||
.build(); | ||
|
||
userRepository.save(user); | ||
|
||
ExtractableResponse<Response> response = RestAssured | ||
.given() | ||
.log().all() | ||
.body(""" | ||
{ | ||
"email": "[email protected]", | ||
"password": "1234" | ||
} | ||
""") | ||
.contentType(ContentType.JSON) | ||
.when() | ||
.log().all() | ||
.post("/user/login") | ||
.then() | ||
.log().all() | ||
.statusCode(HttpStatus.CREATED.value()) | ||
.extract(); | ||
|
||
RestAssured | ||
.given() | ||
.log().all() | ||
.header("Authorization", "BEARER " + response.jsonPath().getString("token")) | ||
.body( | ||
Map.of("refresh_token", response.jsonPath().getString("refresh_token")) | ||
) | ||
.contentType(ContentType.JSON) | ||
.when() | ||
.log().all() | ||
.post("/user/refresh") | ||
.then() | ||
.log().all() | ||
.statusCode(HttpStatus.OK.value()) | ||
.extract(); | ||
|
||
UserToken token = tokenRepository.findById(user.getId()).get(); | ||
|
||
assertSoftly( | ||
softly -> { | ||
softly.assertThat(response.jsonPath().getString("token")).isNotNull(); | ||
softly.assertThat(response.jsonPath().getString("refresh_token")).isNotNull(); | ||
softly.assertThat(response.jsonPath().getString("refresh_token")) | ||
.isEqualTo(token.getRefreshToken()); | ||
} | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
User 관련 요청을 받으면 먼저 Argument Resolver를 통해 헤더에 유효한 토큰이 담겨있는지 확인하는데, refresh token을 재발급하는 요청에 대해서만 해당 과정을 거치지 않도록 처리하면 비즈니스 로직에 영향을 끼치지 않고 해결이 가능할 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/user/refresh
에서@UserAuth User user
를 제거하여 말씀하신 부분 적용했습니다.