Skip to content

Commit

Permalink
feat: token 재발급, 로그아웃 기능 구현
Browse files Browse the repository at this point in the history
로그인 시, refresh token 저장 로직도 추가
related to: #11
  • Loading branch information
heejjinkim committed Sep 7, 2024
1 parent 6693930 commit 5825446
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 18 deletions.
12 changes: 12 additions & 0 deletions src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ public static class SignInRequest {
private String idToken;
}

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class RefreshRequest {
@NotNull
private String accessToken;

@NotNull
private String refreshToken;
}

@Getter
@Builder
@NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@

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

public class AuthResponse {

@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class SignInResponse {

private boolean newMember;
Expand Down
42 changes: 34 additions & 8 deletions src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static com._119.wepro.global.exception.errorcode.CommonErrorCode.INVALID_TOKEN;

import com._119.wepro.auth.dto.response.TokenInfo;
import com._119.wepro.global.util.RedisUtil;
import com._119.wepro.global.enums.Role;
import com._119.wepro.global.exception.RestApiException;
import io.jsonwebtoken.Claims;
Expand All @@ -30,21 +31,25 @@
@Component
public class JwtTokenProvider {

private static final long ACCESS_TOKEN_DURATION = 1000 * 60 * 60L * 24; // 1일
private static final long ACCESS_TOKEN_DURATION = 1000 * 60 * 60L * 24 * 7; // 1일
private static final long REFRESH_TOKEN_DURATION = 1000 * 60 * 60L * 24 * 7; // 7일

private static final String AUTHORITIES_KEY = "auth";
private final RedisUtil redisUtil;
private SecretKey secretKey;

public JwtTokenProvider(@Value("${jwt.secret}") String key) {
public JwtTokenProvider(@Value("${jwt.secret}") String key, RedisUtil redisUtil) {
this.redisUtil = redisUtil;
byte[] keyBytes = key.getBytes();
this.secretKey = Keys.hmacShaKeyFor(keyBytes);
}

public TokenInfo generateToken(Long memberId, Role memberRole) {
String accessToken = generateAccessToken(memberId, memberRole);
// TODO: refresh redis에 저장
String refreshToken = generateRefreshToken();

deleteInvalidRefreshToken(memberId.toString());
redisUtil.setData(memberId.toString(), refreshToken);

return new TokenInfo("Bearer", accessToken, refreshToken);
}

Expand All @@ -61,7 +66,7 @@ public boolean validateToken(String token) {
public Authentication getAuthentication(String accessToken) {
Claims claims = parseClaims(accessToken);

if (claims.get("auth") == null) {
if (claims.get(AUTHORITIES_KEY) == null) {
throw new RestApiException(INVALID_TOKEN);
}
List<SimpleGrantedAuthority> authority = getAuthorities(claims);
Expand Down Expand Up @@ -89,7 +94,6 @@ private boolean isValidAuthority(SimpleGrantedAuthority authority) {

private Claims parseClaims(String accessToken) {
try {
// TODO: 블랙 리스트 여부 추가
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
Expand All @@ -114,7 +118,7 @@ private String generateAccessToken(Long memberId, Role memberRole) {

return Jwts.builder()
.setSubject(memberId.toString())
.claim("auth", memberRole.name())
.claim(AUTHORITIES_KEY, memberRole.name())
.setIssuedAt(now)
.setExpiration(expiredDate)
.signWith(secretKey, SignatureAlgorithm.HS256)
Expand All @@ -133,6 +137,28 @@ private String generateRefreshToken() {

private List<SimpleGrantedAuthority> getAuthorities(Claims claims) {
return Collections.singletonList(new SimpleGrantedAuthority(
claims.get("auth").toString()));
claims.get(AUTHORITIES_KEY).toString()));
}

public String getRefreshToken(String memberId){
return redisUtil.getData(memberId);
}

public void deleteInvalidRefreshToken(String memberId) {
redisUtil.deleteData(memberId);
}

public Claims parseExpiredToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
} catch (JwtException e) {
throw new RestApiException(INVALID_TOKEN);
}
}
}
23 changes: 20 additions & 3 deletions src/main/java/com/_119/wepro/auth/presentation/AuthController.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com._119.wepro.auth.presentation;

import com._119.wepro.auth.dto.response.AuthResponse.SignInResponse;
import com._119.wepro.auth.dto.response.TokenInfo;
import com._119.wepro.auth.service.KakaoService;
import com._119.wepro.auth.service.SignInService;
import com._119.wepro.auth.service.RefreshService;
import com._119.wepro.auth.service.AuthService;
import com._119.wepro.auth.dto.request.AuthRequest.*;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
Expand All @@ -23,13 +25,28 @@
public class AuthController {

private final KakaoService kakaoService;
private final SignInService signInService;
private final AuthService authService;
private final RefreshService refreshService;

@PostMapping("/login")
@Operation(summary = "Kakao idToken 받아 소셜 로그인")
public ResponseEntity<SignInResponse> signIn(
@RequestBody @Valid SignInRequest request) {
return ResponseEntity.ok(signInService.signIn(request));
return ResponseEntity.ok(authService.signIn(request));
}

@PostMapping("/refresh")
@Operation(summary = "access token 재발급")
public ResponseEntity<TokenInfo> refresh(
@RequestBody @Valid RefreshRequest request) {
return ResponseEntity.ok(refreshService.refresh(request));
}

@PostMapping("/logout")
@Operation(summary = "로그아웃")
public ResponseEntity<Void> logout(Authentication authentication){
authService.logOut(authentication.getName());
return ResponseEntity.ok().build();
}

@PostMapping("/signup")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,32 @@
import static com._119.wepro.global.enums.Provider.APPLE;
import static com._119.wepro.global.enums.Provider.KAKAO;

import com._119.wepro.auth.dto.request.AuthRequest.SignInRequest;
import com._119.wepro.auth.dto.response.AuthResponse.SignInResponse;
import com._119.wepro.auth.dto.response.TokenInfo;
import com._119.wepro.auth.jwt.JwtTokenProvider;
import com._119.wepro.global.enums.Provider;
import com._119.wepro.global.enums.Role;
import com._119.wepro.auth.jwt.JwtTokenProvider;
import com._119.wepro.member.domain.Member;
import com._119.wepro.member.domain.repository.MemberRepository;
import com._119.wepro.auth.dto.request.AuthRequest.SignInRequest;
import jakarta.transaction.Transactional;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Service;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.stereotype.Service;


@Slf4j
@Service
@RequiredArgsConstructor
public class SignInService {
public class AuthService {

private final MemberRepository memberRepository;
private final JwtTokenProvider jwtTokenProvider;
Expand All @@ -52,6 +52,11 @@ public SignInResponse signIn(SignInRequest request) {
return new SignInResponse(isNewMember, tokenInfo);
}

@Transactional
public void logOut(String memberId) {
jwtTokenProvider.deleteInvalidRefreshToken(memberId);
}


private Member getOrSaveUser(SignInRequest request, OidcUser oidcDecodePayload) {
Optional<Member> member = memberRepository.findByProviderAndProviderId(
Expand Down
60 changes: 60 additions & 0 deletions src/main/java/com/_119/wepro/auth/service/RefreshService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com._119.wepro.auth.service;

import static com._119.wepro.global.exception.errorcode.CommonErrorCode.EXPIRED_TOKEN;
import static com._119.wepro.global.exception.errorcode.CommonErrorCode.INVALID_TOKEN;
import static com._119.wepro.global.exception.errorcode.CommonErrorCode.REFRESH_DENIED;

import com._119.wepro.auth.dto.request.AuthRequest.RefreshRequest;
import com._119.wepro.auth.dto.response.TokenInfo;
import com._119.wepro.auth.jwt.JwtTokenProvider;
import com._119.wepro.global.exception.RestApiException;
import com._119.wepro.global.exception.errorcode.UserErrorCode;
import com._119.wepro.member.domain.Member;
import com._119.wepro.member.domain.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class RefreshService {

private final JwtTokenProvider jwtTokenProvider;
private final MemberRepository memberRepository;

public TokenInfo refresh(RefreshRequest request) {
String accessToken = request.getAccessToken();
String refreshToken = request.getRefreshToken();

if (!isTokenExpired(accessToken)) {
throw new RestApiException(REFRESH_DENIED);
}
String memberId = jwtTokenProvider.parseExpiredToken(accessToken)
.getSubject();
Member member = memberRepository.findById(Long.parseLong(memberId))
.orElseThrow(() -> new RestApiException(UserErrorCode.USER_NOT_FOUND));

validateRefreshToken(refreshToken, memberId);
return jwtTokenProvider.generateToken(Long.parseLong(memberId), member.getRole());
}

private boolean isTokenExpired(String accessToken) {
try {
jwtTokenProvider.validateToken(accessToken);
throw new RestApiException(REFRESH_DENIED);
} catch (RestApiException e) {
if (e.getErrorCode() == EXPIRED_TOKEN) {
return true;
}
throw e;
}
}

private void validateRefreshToken(String refreshToken, String memberId) {
String savedRefreshToken = jwtTokenProvider.getRefreshToken(memberId);
if (!refreshToken.equals(savedRefreshToken)) {
throw new RestApiException(INVALID_TOKEN);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public enum CommonErrorCode implements ErrorCode {
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "Invalid token"),
EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "Expired token"),
NOT_EXIST_BEARER_SUFFIX(HttpStatus.UNAUTHORIZED, "Bearer prefix is missing."),
REFRESH_DENIED(HttpStatus.FORBIDDEN, "Refresh denied"),
;

private final HttpStatus httpStatus;
Expand Down

0 comments on commit 5825446

Please sign in to comment.