Skip to content

Commit

Permalink
Merge pull request #30 from DDD-Community/develop
Browse files Browse the repository at this point in the history
6차 배포
  • Loading branch information
kikingki authored Sep 19, 2024
2 parents ee71088 + 6cae141 commit 35e2419
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 14 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ dependencies {
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

def QDomains = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public enum ErrorCode {
INVALID_APPLE_TOKEN(HttpStatus.BAD_REQUEST, "유효하지 않는 Apple Token입니다."),
INVALID_JSON_FORMAT(HttpStatus.BAD_REQUEST, "잘못된 JSON 형식입니다."),

// 403
UNAUTHORIZED_REFRESH_TOKEN(HttpStatus.FORBIDDEN, "권한없는 Refresh Token입니다."),

// 404
NON_EXISTENT_USER_ID(HttpStatus.NOT_FOUND, "해당 id의 사용자가 존재하지 않습니다."),
NON_EXISTENT_EMAIL(HttpStatus.NOT_FOUND, "해당 email의 사용자가 존재하지 않습니다."),
Expand Down
22 changes: 9 additions & 13 deletions src/main/java/com/dissonance/itit/common/jwt/util/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import com.dissonance.itit.domain.entity.UserDetailsImpl;
import com.dissonance.itit.dto.response.GeneratedToken;
import com.dissonance.itit.service.RedisService;
import com.dissonance.itit.service.UserDetailsServiceImpl;

import io.jsonwebtoken.Claims;
Expand All @@ -36,34 +37,29 @@ public class JwtUtil {
private String secretKey;

private final UserDetailsServiceImpl userDetailsService;

// TODO: redis를 이용한 refresh token 재발급 구현
// private final RedisService redisService;
private final RedisService redisService;

@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(jwtSecretKey.getBytes(StandardCharsets.UTF_8));
}

public GeneratedToken generateToken(String email, String role) {
String refreshToken = generateRefreshToken(email, role);
String refreshToken = generateRefreshToken();
String accessToken = generateAccessToken(email, role);

redisService.setValuesWithTimeout(email, refreshToken, REFRESH_TOKEN_EXPIRATION_TIME.getValue());

return GeneratedToken.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}

public String generateRefreshToken(String email, String role) {
// Claim에 이메일, 권한 세팅
Claims claims = Jwts.claims().setSubject(email);
claims.put("role", role);

public String generateRefreshToken() {
Date now = new Date();

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now) // 발행일자
.setExpiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION_TIME.getValue())) // 만료 일시
.signWith(SignatureAlgorithm.HS256, secretKey) // HS256 알고리즘과 secretKey로 서명
Expand All @@ -86,9 +82,9 @@ public String generateAccessToken(String email, String role) {

public boolean verifyToken(String token) {
try {
// if (redisService.getValues(token) != null && redisService.getValues(token).equals("logout")) {
// throw new JwtException("Invalid JWT Token - logout");
// }
if (redisService.getValues(token) != null && redisService.getValues(token).equals("logout")) {
throw new JwtException("Invalid JWT Token - logout");
}
Jws<Claims> claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/dissonance/itit/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.dissonance.itit.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private Integer port;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
}
32 changes: 32 additions & 0 deletions src/main/java/com/dissonance/itit/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package com.dissonance.itit.controller;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.dissonance.itit.common.annotation.CurrentUser;
import com.dissonance.itit.common.util.ApiResponse;
import com.dissonance.itit.domain.entity.User;
import com.dissonance.itit.dto.request.RefreshTokenReq;
import com.dissonance.itit.dto.response.GeneratedToken;
import com.dissonance.itit.dto.response.LoginUserInfoRes;
import com.dissonance.itit.service.UserService;

Expand All @@ -26,4 +32,30 @@ public ApiResponse<LoginUserInfoRes> getUserInfo(@CurrentUser User loginUser) {

return ApiResponse.success(userInfoRes);
}

@PostMapping("reissue")
@Operation(summary = "토큰 재발급", description = "access token과 refresh token을 재발급합니다.")
public ApiResponse<GeneratedToken> reissue(@RequestHeader("Authorization") String requestAccessToken,
@RequestBody RefreshTokenReq tokenRequest) {
GeneratedToken reissuedToken = userService.accessTokenByRefreshToken(requestAccessToken,
tokenRequest.refreshToken());

return ApiResponse.success(reissuedToken);
}

@GetMapping("/logout")
@Operation(summary = "로그아웃", description = "access token을 만료시킵니다.")
public ApiResponse<String> logout(@RequestHeader("Authorization") String requestAccessToken) {
userService.logout(requestAccessToken);

return ApiResponse.success("로그아웃되었습니다.");
}

@DeleteMapping
@Operation(summary = "회원 탈퇴", description = "로그인 유저의 계정을 탈퇴시킵니다.")
public ApiResponse<String> withdraw(@CurrentUser User loginUser) {
userService.withdraw(loginUser.getId());

return ApiResponse.success("회원 탈퇴가 완료되었습니다.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@Getter
@AllArgsConstructor
public enum JwtTokenExpiration {
ACCESS_TOKEN_EXPIRED_TIME("1시간", 1000L * 60 * 60 * 100000), // TODO: RT 토큰 도입 후 만료 시간 적용
ACCESS_TOKEN_EXPIRED_TIME("1시간", 1000L * 60 * 60),
REFRESH_TOKEN_EXPIRATION_TIME("2주", 1000L * 60 * 60 * 24 * 14);

private final String description;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.dissonance.itit.dto.request;

public record RefreshTokenReq(
String refreshToken
) {
}
26 changes: 26 additions & 0 deletions src/main/java/com/dissonance/itit/service/RedisService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.dissonance.itit.service;

import java.util.concurrent.TimeUnit;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class RedisService {
private final RedisTemplate<String, String> redisTemplate;

public String getValues(String key) {
return redisTemplate.opsForValue().get(key);
}

public void setValuesWithTimeout(String key, String value, long duration) {
redisTemplate.opsForValue().set(key, value, duration, TimeUnit.MILLISECONDS);
}

public void deleteValues(String key) {
redisTemplate.delete(key);
}
}
34 changes: 34 additions & 0 deletions src/main/java/com/dissonance/itit/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
@Service
public class UserService {
private final JwtUtil jwtUtil;
private final RedisService redisService;
private final UserRepository userRepository;

private final OAuthServiceFactory oAuthServiceFactory;
Expand Down Expand Up @@ -79,4 +80,37 @@ public LoginUserInfoRes getUserInfo(User loginUser) {
private boolean isAdmin(User loginUser) {
return loginUser.getRole().equals(Role.ADMIN);
}

public GeneratedToken accessTokenByRefreshToken(String accessTokenHeader, String refreshToken) {
String accessToken = jwtUtil.resolveToken(accessTokenHeader);

String uid = jwtUtil.getUid(accessToken);
String data = redisService.getValues(uid);

if (data == null || !data.equals(refreshToken)) {
log.info("Invalid Token");
throw new CustomException(ErrorCode.UNAUTHORIZED_REFRESH_TOKEN);
}
String role = jwtUtil.getRole(accessToken);

return jwtUtil.generateToken(uid, role);
}

@Transactional
public void logout(String accessTokenHeader) {
String accessToken = jwtUtil.resolveToken(accessTokenHeader);

jwtUtil.verifyToken(accessToken);

String uid = jwtUtil.getUid(accessToken);
long time = jwtUtil.getExpiration(accessToken);

redisService.setValuesWithTimeout(accessToken, "logout", time);
redisService.deleteValues(uid);
}

@Transactional
public void withdraw(Long loginUserId) {
userRepository.deleteById(loginUserId);
}
}
4 changes: 4 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ spring:
multipart:
max-request-size: 10MB
max-file-size: 10MB
data:
redis:
port: ${REDIS_PORT}
host: ${REDIS_HOST}

jwt:
token:
Expand Down

0 comments on commit 35e2419

Please sign in to comment.