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(Auth): reissue, logout 구현 #41

Merged
merged 10 commits into from
Jul 29, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.jabiseo.auth.exception.AuthenticationBusinessException;
import com.jabiseo.auth.exception.AuthenticationErrorCode;
import com.jabiseo.member.domain.Member;
import io.jsonwebtoken.ClaimJwtException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
Expand All @@ -21,7 +22,7 @@ public class JwtHandler {
private final Key refreshKey;
private final Integer accessExpiredMin;
private final Integer refreshExpiredDay;
private final String APP_ISSUER = "jabiseo";
private static final String APP_ISSUER = "jabiseo";

public JwtHandler(JwtProperty jwtProperty) {
byte[] accessEncodeByte = Base64.getEncoder().encode((jwtProperty.getAccessKey().getBytes()));
Expand All @@ -36,7 +37,6 @@ public JwtHandler(JwtProperty jwtProperty) {
public String createAccessToken(Member member) {
Instant accessExpiredTime = Instant.now()
.plus(this.accessExpiredMin, ChronoUnit.MINUTES);

Map<String, Object> payload = new HashMap<>();

return Jwts.builder()
Expand All @@ -57,21 +57,35 @@ public String createRefreshToken() {
.compact();
}
InHyeok-J marked this conversation as resolved.
Show resolved Hide resolved


public boolean validateAccessToken(String token) {
public void validateAccessToken(String accessToken){
try {
Jwts.parserBuilder()
.setSigningKey(accessKey)
.build()
.parseClaimsJws(token);
return true;
.parseClaimsJws(accessToken)
.getBody();
} catch (ExpiredJwtException e) {
throw new AuthenticationBusinessException(AuthenticationErrorCode.EXPIRED_APP_JWT);
} catch (Exception e) {
throw new AuthenticationBusinessException(AuthenticationErrorCode.INVALID_APP_JWT);
}
}

public void validateAccessTokenNotCheckExpired(String accessToken){
try {
Jwts.parserBuilder()
.setSigningKey(accessKey)
.build()
.parseClaimsJws(accessToken)
.getBody();
} catch (ExpiredJwtException e) {
return;
} catch (Exception e) {
throw new AuthenticationBusinessException(AuthenticationErrorCode.INVALID_APP_JWT);
}
}


public void validateRefreshToken(String refreshToken) {
try {
Jwts.parserBuilder()
Expand All @@ -86,26 +100,19 @@ public void validateRefreshToken(String refreshToken) {
}


public Claims getClaimFromExpiredAccessToken(String accessToken) {
public Claims getClaimsFromAccessToken(String token) {
try {
return Jwts.parserBuilder()
return Jwts
.parserBuilder()
.setSigningKey(accessKey)
.build()
.parseClaimsJws(accessToken)
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
} catch (ClaimJwtException e) {
// 기존 검증에서 처리후 가져오는 동작
return e.getClaims();
} catch (Exception e) {
throw new AuthenticationBusinessException(AuthenticationErrorCode.INVALID_APP_JWT);
}
}

public Claims getClaimsFromAccessToken(String token) {
return Jwts
.parserBuilder()
.setSigningKey(accessKey)
.build()
.parseClaimsJws(token)
.getBody();
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package com.jabiseo.auth.application.usecase;

import com.jabiseo.cache.RedisCacheRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogoutUseCase {
public void execute() {

private final RedisCacheRepository redisCacheRepository;

public void execute(String memberId) {
redisCacheRepository.deleteToken(memberId);
}
morenow98 marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,42 @@
package com.jabiseo.auth.application.usecase;

import com.jabiseo.auth.application.JwtHandler;
import com.jabiseo.auth.dto.LoginResponse;
import com.jabiseo.auth.dto.ReissueRequest;
import com.jabiseo.auth.dto.ReissueResponse;
import com.jabiseo.auth.exception.AuthenticationBusinessException;
import com.jabiseo.auth.exception.AuthenticationErrorCode;
import com.jabiseo.cache.RedisCacheRepository;
import com.jabiseo.member.domain.Member;
import com.jabiseo.member.domain.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ReissueUseCase {

public LoginResponse reissue(String refreshToken) {
return new LoginResponse("access_token", "refresh_token");
private final MemberRepository memberRepository;
private final RedisCacheRepository redisCacheRepository;
private final JwtHandler jwtHandler;

public ReissueResponse execute(ReissueRequest request, String memberId) {
Member member = memberRepository.getReferenceById(memberId);

jwtHandler.validateRefreshToken(request.refreshToken());

String savedToken = redisCacheRepository.findToken(memberId)
.orElseThrow(() -> new AuthenticationBusinessException(AuthenticationErrorCode.REQUIRE_LOGIN));

if (!savedToken.equals(request.refreshToken())) {
throw new AuthenticationBusinessException(AuthenticationErrorCode.NOT_MATCH_REFRESH);
}

String accessToken = jwtHandler.createAccessToken(member);
return new ReissueResponse(accessToken);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import com.jabiseo.auth.application.usecase.LogoutUseCase;
import com.jabiseo.auth.application.usecase.ReissueUseCase;
import com.jabiseo.auth.application.usecase.WithdrawUseCase;
import com.jabiseo.auth.dto.ReissueRequest;
import com.jabiseo.auth.dto.ReissueResponse;
import com.jabiseo.config.auth.AuthMember;
import com.jabiseo.config.auth.AuthenticatedMember;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -34,14 +38,14 @@ public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest logi
}

@PostMapping("/reissue")
public ResponseEntity<LoginResponse> reissue(String refreshToken) {
LoginResponse result = reissueUseCase.reissue(refreshToken);
public ResponseEntity<ReissueResponse> reissue(@Valid @RequestBody ReissueRequest request, @AuthenticatedMember AuthMember member) {
ReissueResponse result = reissueUseCase.execute(request, member.getMemberId());
return ResponseEntity.ok(result);
}

@PostMapping("/logout")
public ResponseEntity<Void> logout() {
logoutUseCase.execute();
public ResponseEntity<Void> logout(@AuthenticatedMember AuthMember member) {
logoutUseCase.execute(member.getMemberId());
return ResponseEntity.noContent().build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.jabiseo.auth.dto;

import jakarta.validation.constraints.NotBlank;

public record ReissueRequest(@NotBlank String refreshToken) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.jabiseo.auth.dto;

public record ReissueResponse(String accessToken) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,31 @@
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtHandler jwtHandler;
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String HEADER_PREFIX = "Bearer ";

private static final String REISSUE_REQUEST = "/api/auth/reissue";
morenow98 marked this conversation as resolved.
Show resolved Hide resolved

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = extractTokenFromRequest(request);

if (StringUtils.hasText(token) && jwtHandler.validateAccessToken(token)) {
Claims claims = jwtHandler.getClaimsFromAccessToken(token);
if (StringUtils.hasText(token)) {
Claims claims = getClaimsFromToken(token, request);
setAuthenticationToContext(claims);
}

filterChain.doFilter(request, response);
}

private Claims getClaimsFromToken(String token, HttpServletRequest request) {
if (request.getRequestURI().equals(REISSUE_REQUEST)) {
jwtHandler.validateAccessTokenNotCheckExpired(token);
} else {
jwtHandler.validateAccessToken(token);
}
return jwtHandler.getClaimsFromAccessToken(token);
}

private void setAuthenticationToContext(Claims jwtClaim) {
String memberId = jwtClaim.getSubject();
Set<GrantedAuthority> authorities = new HashSet<>();
Expand Down
2 changes: 1 addition & 1 deletion jabiseo-api/src/main/resources/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ management:
include: "*"

jwt:
access-expired-min: 6000
access-expired-min: 1

---
spring:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.jabiseo.auth.application.usecase;

import com.jabiseo.auth.application.JwtHandler;
import com.jabiseo.auth.dto.ReissueRequest;
import com.jabiseo.auth.dto.ReissueResponse;
import com.jabiseo.auth.exception.AuthenticationBusinessException;
import com.jabiseo.auth.exception.AuthenticationErrorCode;
import com.jabiseo.cache.RedisCacheRepository;
import com.jabiseo.member.domain.Member;
import com.jabiseo.member.domain.MemberRepository;
import fixture.MemberFixture;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.BDDMockito.given;

@ExtendWith(MockitoExtension.class)
class ReissueUseCaseTest {

@InjectMocks
ReissueUseCase reissueUseCase;

@Mock
MemberRepository memberRepository;

@Mock
RedisCacheRepository redisCacheRepository;

@Mock
JwtHandler jwtHandler;

ReissueRequest request;

@BeforeEach
void setUp() {
request = new ReissueRequest("refresh");
}

@Test
@DisplayName("저장된 토큰이 없는 경우 예외를 반환한다")
void savedTokenIsNullThrownException() {
//given
String memberId = "id";
given(memberRepository.getReferenceById(memberId)).willReturn(MemberFixture.createMember(memberId));
given(redisCacheRepository.findToken(memberId)).willReturn(Optional.empty());

//when then
assertThatThrownBy(() -> reissueUseCase.execute(request, memberId))
.isInstanceOf(AuthenticationBusinessException.class);
}

@Test
@DisplayName("다른 refreshToken으로 요청하면 예외를 반환한다")
void otherTokenRequestThrownException() {
//given
String memberId = "id";
String otherToken = "tokens";
given(memberRepository.getReferenceById(memberId)).willReturn(MemberFixture.createMember(memberId));
given(redisCacheRepository.findToken(memberId)).willReturn(Optional.of(otherToken));

//when then
assertThatThrownBy(() -> reissueUseCase.execute(request, memberId))
.isInstanceOf(AuthenticationBusinessException.class)
.hasMessage(AuthenticationErrorCode.NOT_MATCH_REFRESH.getMessage());
}

@Test
@DisplayName("정상 요청의 경우 새로운 access Token을 발급한다.")
void requestSuccessReturnNewAccessToken(){
//given
String memberId = "id";
Member member = MemberFixture.createMember(memberId);
String newAccessToken = "accessToken";
given(memberRepository.getReferenceById(memberId)).willReturn(member);
given(redisCacheRepository.findToken(memberId)).willReturn(Optional.of(request.refreshToken()));
given(jwtHandler.createAccessToken(member)).willReturn(newAccessToken);

//when
ReissueResponse execute = reissueUseCase.execute(request, memberId);

//then
assertThat(execute.accessToken()).isEqualTo(newAccessToken);
}
}
Loading
Loading