Skip to content

Commit

Permalink
refactor: 리프레시 토큰 발급 기능 제거
Browse files Browse the repository at this point in the history
  • Loading branch information
Albatross3 committed Sep 22, 2023
1 parent 2077de3 commit 3fc20b4
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 133 deletions.
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ dependencies {

implementation 'org.springframework.boot:spring-boot-starter-security'

implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
// runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package shop.zip.travel.domain.member.dto.response;

public record MemberLoginResponse(String accessToken) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,21 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import shop.zip.travel.domain.member.dto.request.AccessTokenReissueReq;
import shop.zip.travel.domain.member.dto.request.MemberLoginReq;
import shop.zip.travel.domain.member.dto.request.MemberRegisterReq;
import shop.zip.travel.domain.member.dto.response.MemberLoginRes;
import shop.zip.travel.domain.member.dto.response.MemberLoginResponse;
import shop.zip.travel.domain.member.entity.Member;
import shop.zip.travel.domain.member.exception.InvalidRefreshTokenException;
import shop.zip.travel.domain.member.exception.MemberNotFoundException;
import shop.zip.travel.domain.member.exception.PasswordNotMatchException;
import shop.zip.travel.domain.member.repository.MemberRepository;
import shop.zip.travel.global.error.ErrorCode;
import shop.zip.travel.global.security.JwtTokenProvider;
import shop.zip.travel.global.util.RedisUtil;

@Service
@RequiredArgsConstructor
public class MemberService {

private final MemberRepository memberRepository;
private final RedisUtil redisUtil;
private final JwtTokenProvider jwtTokenProvider;
private final PasswordEncoder passwordEncoder;

Expand All @@ -41,44 +37,16 @@ public Long createMember(MemberRegisterReq memberRegisterReq) {
}

@Transactional
public MemberLoginRes login(MemberLoginReq memberLoginReq) {
public MemberLoginResponse login(MemberLoginReq memberLoginReq) {
Member member = findMemberByEmail(memberLoginReq.email());

if (!passwordEncoder.matches(memberLoginReq.password(), member.getPassword())) {
throw new PasswordNotMatchException(ErrorCode.PASSWORD_NOT_MATCH);
}

String accessToken = jwtTokenProvider.createAccessToken(member.getId());
String refreshToken = jwtTokenProvider.createRefreshToken();

redisUtil.setDataWithExpire(String.valueOf(member.getId()), refreshToken, 120L);

return new MemberLoginRes(accessToken, refreshToken);
}

// TODO accessToken 에서 memberId 를 가져오는 방법을 refactoring 해보기
@Transactional
public MemberLoginRes recreateAccessAndRefreshToken(AccessTokenReissueReq accessTokenReissueReq) {

String accessToken = accessTokenReissueReq.accessToken();
String refreshToken = accessTokenReissueReq.refreshToken();
String memberId = jwtTokenProvider.getMemberIdUsingDecode(accessToken);

if (jwtTokenProvider.validateRefreshToken(accessTokenReissueReq.refreshToken()) &&
redisUtil.getData(memberId).equals(refreshToken)) {
redisUtil.deleteData(memberId);
String newAccessToken = jwtTokenProvider.createAccessToken(Long.parseLong(memberId));
String newRefreshToken = jwtTokenProvider.createRefreshToken();
redisUtil.setDataWithExpire(memberId, newRefreshToken, 120L);

return new MemberLoginRes(newAccessToken, newRefreshToken);
} else {
throw new InvalidRefreshTokenException(ErrorCode.INVALID_REFRESH_TOKEN);
}
}

public void deleteRefreshToken(Long memberId) {
redisUtil.deleteData(String.valueOf(memberId));
return new MemberLoginResponse(accessToken);
}

public Member getMember(Long id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,10 @@ public SecurityConfig(JwtTokenProvider jwtTokenProvider, ObjectMapper objectMapp

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.httpBasic().disable()
.csrf().disable()
.headers().disable()
http.csrf().disable()
.logout().disable()
.formLogin().disable()
.httpBasic().disable()
.authorizeHttpRequests(requests -> requests
.requestMatchers(HttpMethod.OPTIONS).permitAll()
.requestMatchers("/api/members/**").permitAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
Expand All @@ -30,7 +28,7 @@ public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, ObjectMapper o
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("AccessToken");
String token = request.getHeader("Authorization");

try {
if (token != null && jwtTokenProvider.validateAccessToken(token)) {
Expand Down
77 changes: 20 additions & 57 deletions src/main/java/shop/zip/travel/global/security/JwtTokenProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,44 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.time.Duration;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import shop.zip.travel.domain.member.exception.InvalidRefreshTokenException;
import shop.zip.travel.global.error.BusinessException;
import shop.zip.travel.global.error.ErrorCode;
import shop.zip.travel.global.error.exception.JsonNotParsingException;
import shop.zip.travel.global.util.Constants;

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

private static final Logger log = LoggerFactory.getLogger(JwtTokenProvider.class);

private final String accessTokenSecretKey;
private final String refreshTokenSecretKey;
private final CustomUserDetailsService customUserDetailsService;

private final byte[] SECRET_KEY_BYTE_ARRAY = Constants.getSecretKey().getBytes();
private final long ACCESS_TOKEN_EXPIRED_TIME = Duration.ofMinutes(1).toMillis();
private final long REFRESH_TOKEN_EXPIRED_TIME = Duration.ofMinutes(5).toMillis();

private final CustomUserDetailsService customUserDetailsService;

public JwtTokenProvider(
@Value("${spring.jwt.secret-key[0].accessToken}") String accessTokenSecretKey,
@Value("${spring.jwt.secret-key[1].refreshToken}") String refreshTokenSecretKey,
CustomUserDetailsService customUserDetailsService) {
this.accessTokenSecretKey = accessTokenSecretKey;
this.refreshTokenSecretKey = refreshTokenSecretKey;
this.customUserDetailsService = customUserDetailsService;
}


public String removeBearer(String bearerToken) {
return bearerToken.substring("Bearer ".length());
Expand All @@ -54,7 +48,7 @@ public String removeBearer(String bearerToken) {
public boolean validateAccessToken(String token) {
try {
String accessToken = removeBearer(token);
Jwts.parser().setSigningKey(accessTokenSecretKey).parseClaimsJws(accessToken);
Jwts.parserBuilder().setSigningKey(SECRET_KEY_BYTE_ARRAY);
return true;
} catch (SignatureException ex) {
log.error("유효하지 않은 JWT 서명");
Expand All @@ -75,7 +69,7 @@ public boolean validateAccessToken(String token) {
}

public String getMemberId(String accessToken) {
return Jwts.parser().setSigningKey(accessTokenSecretKey).parseClaimsJws(accessToken)
return Jwts.parser().setSigningKey(SECRET_KEY_BYTE_ARRAY).parseClaimsJws(accessToken)
.getBody().get("memberId").toString();
}

Expand All @@ -86,54 +80,23 @@ public Authentication getAuthentication(String token) {
}

public String createAccessToken(Long memberId) {
Claims claims = Jwts.claims();
claims.setSubject("Travel.zip");
claims.put("memberId", memberId);
Date nowDate = new Date();
Date expiryDate = new Date(nowDate.getTime() + ACCESS_TOKEN_EXPIRED_TIME);
Key key = Keys.hmacShaKeyFor(SECRET_KEY_BYTE_ARRAY);

Date now = new Date();
Date expiryDate = new Date(now.getTime() + ACCESS_TOKEN_EXPIRED_TIME);
Map<String, Object> customClaim = new HashMap<>();
customClaim.put("memberId", memberId);

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setIssuer("Travelogues")
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, accessTokenSecretKey)
.setIssuedAt(nowDate)
.addClaims(customClaim)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}

public boolean validateRefreshToken(String refreshToken) {
try {
Jwts.parser().setSigningKey(refreshTokenSecretKey).parseClaimsJws(refreshToken);
return true;
} catch (SignatureException ex) {
log.info("유효하지 않은 JWT 서명");
throw new BusinessException(ErrorCode.TOKEN_EXCEPTION);
} catch (MalformedJwtException ex) {
log.info("유효하지 않은 JWT 토큰");
throw new BusinessException(ErrorCode.TOKEN_EXCEPTION);
} catch (ExpiredJwtException ex) {
log.info("만료된 JWT 토큰");
throw new InvalidRefreshTokenException(ErrorCode.INVALID_REFRESH_TOKEN);
} catch (UnsupportedJwtException ex) {
log.info("지원하지 않는 JWT 토큰");
throw new BusinessException(ErrorCode.TOKEN_EXCEPTION);
} catch (IllegalArgumentException ex) {
log.info("비어있는 토큰");
throw new BusinessException(ErrorCode.TOKEN_EXCEPTION);
}
}

public String createRefreshToken() {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + REFRESH_TOKEN_EXPIRED_TIME);

return Jwts.builder()
.setSubject("Travel.zip")
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, refreshTokenSecretKey)
.compact();
}

public String getMemberIdUsingDecode(String accessToken) {
String[] chunks = accessToken.split("\\.");
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/shop/zip/travel/global/util/Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package shop.zip.travel.global.util;

import org.springframework.beans.factory.annotation.Value;

public class Constants {

static class Constant {
@Value("${spring.jwt.secret-key}")
private static String secretKey;
}

public static String getSecretKey() {
return Constant.secretKey;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,16 @@
import java.net.URI;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import shop.zip.travel.domain.member.dto.request.AccessTokenReissueReq;
import shop.zip.travel.domain.member.dto.request.MemberLoginReq;
import shop.zip.travel.domain.member.dto.request.MemberRegisterReq;
import shop.zip.travel.domain.member.dto.response.DuplicatedNicknameCheckRes;
import shop.zip.travel.domain.member.dto.response.MemberLoginRes;
import shop.zip.travel.domain.member.dto.response.MemberLoginResponse;
import shop.zip.travel.domain.member.service.MemberService;
import shop.zip.travel.global.security.UserPrincipal;

@RestController
@RequiredArgsConstructor
Expand All @@ -43,28 +39,12 @@ public ResponseEntity<Void> register(
}

@PostMapping("/login")
public ResponseEntity<MemberLoginRes> login(
public ResponseEntity<MemberLoginResponse> login(
@RequestBody @Valid MemberLoginReq memberLoginReq
) {
MemberLoginRes memberLoginRes = memberService.login(memberLoginReq);
return ResponseEntity.ok(memberLoginRes);
MemberLoginResponse memberLoginResponse = memberService.login(memberLoginReq);
return ResponseEntity.ok(memberLoginResponse);
}

@PostMapping("/refresh")
public ResponseEntity<MemberLoginRes> reissueAccessToken(
@RequestBody AccessTokenReissueReq accessTokenReissueReq
) {
MemberLoginRes memberLoginRes = memberService.recreateAccessAndRefreshToken(
accessTokenReissueReq);
return ResponseEntity.ok(memberLoginRes);
}

@DeleteMapping("/logout")
public ResponseEntity<Void> logout(
@AuthenticationPrincipal UserPrincipal userPrincipal
) {
memberService.deleteRefreshToken(userPrincipal.getUserId());
return ResponseEntity.ok().build();
}

}

0 comments on commit 3fc20b4

Please sign in to comment.