diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index c915cdd..7f95a0c 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -51,16 +51,16 @@ jobs: docker tag ${{ secrets.DOCKERHUB_REPOSITORY }} ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY }}:${GITHUB_SHA::7} docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY }}:${GITHUB_SHA::7} - # Deploy - - name: Deploy - uses: appleboy/ssh-action@master - with: - host: 35.216.106.69 - username: ${{ secrets.SSH_USERNAME }} - key: ${{ secrets.SSH_PRIVATE_KEY }} - envs: GITHUB_SHA - script: | - sudo echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin - sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY }}:${GITHUB_SHA::7} - sudo docker tag ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY }}:${GITHUB_SHA::7} alchive_spring - sudo docker-compose up -d +# # Deploy +# - name: Deploy +# uses: appleboy/ssh-action@master +# with: +# host: 35.216.106.69 +# username: ${{ secrets.SSH_USERNAME }} +# key: ${{ secrets.SSH_PRIVATE_KEY }} +# envs: GITHUB_SHA +# script: | +# sudo echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin +# sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY }}:${GITHUB_SHA::7} +# sudo docker tag ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY }}:${GITHUB_SHA::7} alchive_spring +# sudo docker-compose up -d diff --git a/build.gradle b/build.gradle index 5372148..6e8ca12 100644 --- a/build.gradle +++ b/build.gradle @@ -52,13 +52,16 @@ dependencies { implementation 'io.micrometer:micrometer-registry-prometheus' // Slack API - implementation 'com.slack.api:bolt:1.18.0' - implementation 'com.slack.api:bolt-servlet:1.18.0' - implementation 'com.slack.api:bolt-jetty:1.18.0' + implementation("com.slack.api:bolt:1.18.0") + implementation("com.slack.api:bolt-servlet:1.18.0") + implementation("com.slack.api:bolt-jetty:1.18.0") implementation 'com.slack.api:slack-api-client:1.44.1' // Discord API implementation 'net.dv8tion:JDA:5.0.0-beta.5' + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { diff --git a/docker-compose.yml b/docker-compose.yml index 2600035..a51dd57 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,20 +22,30 @@ services: networks: - alchive - springboot: - container_name: springboot - restart: always - depends_on: - - mysql + redis: + image: redis:latest + container_name: redis ports: - - "8080:8080" - environment: - SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL} - SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME} - SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} - SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} - networks: - - alchive + - "6379:6379" + volumes: + - alchive-db:/alchive-db/redis + + container_name: springboot + build: + context: . + dockerfile: Dockerfile + restart: always + depends_on: + - mysql + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL} + SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME} + SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} + networks: + - alchive # # Monitoring # prometheus: diff --git a/src/main/java/com/Alchive/backend/config/DataInitializer.java b/src/main/java/com/Alchive/backend/config/DataInitializer.java index 7f015bb..cc2a321 100644 --- a/src/main/java/com/Alchive/backend/config/DataInitializer.java +++ b/src/main/java/com/Alchive/backend/config/DataInitializer.java @@ -1,5 +1,7 @@ package com.Alchive.backend.config; +import com.Alchive.backend.config.redis.RefreshTokenRepository; +import com.Alchive.backend.config.redis.RefreshTokenService; import com.Alchive.backend.domain.algorithm.Algorithm; import com.Alchive.backend.domain.algorithmProblem.AlgorithmProblem; import com.Alchive.backend.domain.board.Board; @@ -33,19 +35,22 @@ public class DataInitializer implements CommandLineRunner { private final BoardRepository boardRepository; private final AlgorithmRepository algorithmRepository; private final AlgorithmProblemRepository algorithmProblemRepository; + private final RefreshTokenService refreshTokenService; public DataInitializer(UserRepository userRepository, ProblemRepository problemRepository, SolutionRepository solutionRepository, BoardRepository boardRepository, AlgorithmRepository algorithmRepository, - AlgorithmProblemRepository algorithmProblemRepository) { + AlgorithmProblemRepository algorithmProblemRepository, + RefreshTokenService refreshTokenService) { this.userRepository = userRepository; this.problemRepository = problemRepository; this.solutionRepository = solutionRepository; this.boardRepository = boardRepository; this.algorithmRepository = algorithmRepository; this.algorithmProblemRepository = algorithmProblemRepository; + this.refreshTokenService = refreshTokenService; } @Override @@ -82,6 +87,19 @@ public void run(String... args) throws Exception { .build(); userRepository.save(user4); + // Redis token 생성 + String refreshToken1 = refreshTokenService.createRefreshToken("chohana@alchive.com"); + refreshTokenService.saveRefreshToken("chohana@alchive.com", refreshToken1); + + String refreshToken2 = refreshTokenService.createRefreshToken("parknahyun@alchive.com"); + refreshTokenService.saveRefreshToken("parknahyun@alchive.com", refreshToken2); + + String refreshToken3 = refreshTokenService.createRefreshToken("songyurim@alchive.com"); + refreshTokenService.saveRefreshToken("songyurim@alchive.com", refreshToken3); + + String refreshToken4 = refreshTokenService.createRefreshToken("kimmiyoung@alchive.com"); + refreshTokenService.saveRefreshToken("kimmiyoung@alchive.com", refreshToken4); + // Algorithm 목업 데이터 생성 String[] algorithmNames = {"이분탐색", "DP", "BFS", "DFS", "브루트포스", "그리디", "정렬", "구현", "그래프"}; for (String name : algorithmNames) { diff --git a/src/main/java/com/Alchive/backend/config/SwaggerConfig.java b/src/main/java/com/Alchive/backend/config/SwaggerConfig.java index a0445a8..3c63082 100644 --- a/src/main/java/com/Alchive/backend/config/SwaggerConfig.java +++ b/src/main/java/com/Alchive/backend/config/SwaggerConfig.java @@ -26,13 +26,8 @@ public OpenAPI openAPI() { .bearerFormat("JWT") .in(SecurityScheme.In.HEADER) .name(HttpHeaders.AUTHORIZATION); - SecurityScheme refreshTokenSecurityScheme = new SecurityScheme() - .type(SecurityScheme.Type.APIKEY) - .in(SecurityScheme.In.HEADER) - .name("REFRESH-TOKEN"); Components components = new Components() - .addSecuritySchemes(key, accessTokenSecurityScheme) - .addSecuritySchemes(refreshKey, refreshTokenSecurityScheme); + .addSecuritySchemes(key, accessTokenSecurityScheme); return new OpenAPI() .info(apiInfo()) diff --git a/src/main/java/com/Alchive/backend/config/auth/SecurityConfig.java b/src/main/java/com/Alchive/backend/config/auth/SecurityConfig.java index c1c7223..56c851f 100644 --- a/src/main/java/com/Alchive/backend/config/auth/SecurityConfig.java +++ b/src/main/java/com/Alchive/backend/config/auth/SecurityConfig.java @@ -5,6 +5,7 @@ import com.Alchive.backend.config.auth.service.CustomOAuth2UserService; import com.Alchive.backend.config.jwt.JwtAuthenticationFilter; import com.Alchive.backend.config.jwt.JwtTokenProvider; +import com.Alchive.backend.config.redis.RefreshTokenService; import com.Alchive.backend.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -27,6 +28,7 @@ public class SecurityConfig { private final OAuth2FailureHandler oAuth2FailureHandler; private final JwtTokenProvider jwtTokenProvider; private final UserService userService; + private final RefreshTokenService refreshTokenService; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -39,7 +41,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { requests.anyRequest().permitAll() // 모든 요청을 모든 사용자에게 허용 ) .addFilterBefore( - new JwtAuthenticationFilter(jwtTokenProvider, userService), + new JwtAuthenticationFilter(jwtTokenProvider, userService, refreshTokenService), UsernamePasswordAuthenticationFilter.class ) .sessionManagement(sessionManagement -> diff --git a/src/main/java/com/Alchive/backend/config/auth/handler/OAuth2SuccessHandler.java b/src/main/java/com/Alchive/backend/config/auth/handler/OAuth2SuccessHandler.java index f7d0dcf..21bc04e 100644 --- a/src/main/java/com/Alchive/backend/config/auth/handler/OAuth2SuccessHandler.java +++ b/src/main/java/com/Alchive/backend/config/auth/handler/OAuth2SuccessHandler.java @@ -1,6 +1,7 @@ package com.Alchive.backend.config.auth.handler; import com.Alchive.backend.config.jwt.JwtTokenProvider; +import com.Alchive.backend.config.redis.RefreshTokenService; import com.Alchive.backend.domain.user.User; import com.Alchive.backend.repository.UserRepository; import jakarta.servlet.http.HttpServletRequest; @@ -22,6 +23,7 @@ public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final UserRepository userRepository; private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenService refreshTokenService; // 검증 완료된 유저의 정보를 가져와서 토큰 생성, 로그인/회원가입 요청에 맞게 리다이렉트 @Override @@ -37,11 +39,11 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo if (user.isPresent()) { // 로그인인 경우 userEmail = user.get().getEmail(); String accessToken = jwtTokenProvider.createAccessToken(userEmail); - String refreshToken = jwtTokenProvider.createRefreshToken(userEmail); + String refreshToken = refreshTokenService.createRefreshToken(userEmail); targetUrl = UriComponentsBuilder.fromUriString("/") .queryParam("access", accessToken) - .queryParam("refresh", refreshToken) .build().toUriString(); + refreshTokenService.saveRefreshToken(userEmail, refreshToken); } else { // 회원가입인 경우 targetUrl = UriComponentsBuilder.fromUriString("http://localhost:5173/sign") .queryParam("email", email) diff --git a/src/main/java/com/Alchive/backend/config/jwt/JwtAuthenticationFilter.java b/src/main/java/com/Alchive/backend/config/jwt/JwtAuthenticationFilter.java index cfef4b8..0076c42 100644 --- a/src/main/java/com/Alchive/backend/config/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/Alchive/backend/config/jwt/JwtAuthenticationFilter.java @@ -3,7 +3,9 @@ import com.Alchive.backend.config.error.ErrorCode; import com.Alchive.backend.config.error.ErrorResponse; import com.Alchive.backend.config.error.exception.BusinessException; +import com.Alchive.backend.config.error.exception.token.TokenExpiredException; import com.Alchive.backend.config.error.exception.token.TokenNotExistsException; +import com.Alchive.backend.config.redis.RefreshTokenService; import com.Alchive.backend.domain.user.User; import com.Alchive.backend.service.UserService; import com.fasterxml.jackson.databind.ObjectMapper; @@ -12,6 +14,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; @@ -24,9 +27,11 @@ @Component @RequiredArgsConstructor +@Slf4j public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; private final UserService userService; + private final RefreshTokenService refreshTokenService; private final AntPathMatcher pathMatcher = new AntPathMatcher(); @@ -42,7 +47,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { Map.entry("/api/v2/users/username/{name}", List.of("GET")), Map.entry("/api/v2/sns/{snsId}", List.of("GET")), Map.entry("/api/v2/slack/reminder", List.of("GET")), - Map.entry("/api/v2/slack/added", List.of("GET")) + Map.entry("/api/v2/slack/added", List.of("GET")), + Map.entry("/api/v2/jwt/**", List.of("GET")), + Map.entry("/api/v2/refreshTokens/**", List.of("GET", "POST")) ); // EXCLUDE_URL과 메서드에 일치할 경우 현재 필터를 진행하지 않고 다음 필터 진행 @@ -64,25 +71,26 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse try { // 액세스 토큰 추출 및 검증 String accessToken = jwtTokenProvider.resolveAccessToken(request); - if (accessToken != null && jwtTokenProvider.validateToken(accessToken)) { + String email = jwtTokenProvider.getEmailFromToken(accessToken); + if ( jwtTokenProvider.validateToken(accessToken) ) { authenticateWithToken(accessToken); } - // 액세스 토큰이 없거나 만료된 경우 리프레시 토큰 확인 else { + // 액세스 토큰이 만료된 경우 리프레시 토큰 확인 + log.info("액세스 토큰 만료"); // 리프레시 토큰 추출 및 검증 - String refreshToken = jwtTokenProvider.resolveRefreshToken(request); - if (refreshToken != null && jwtTokenProvider.validateToken(refreshToken)) { - String email = jwtTokenProvider.getEmailFromToken(refreshToken); - // 새로운 액세스, 리프레시 토큰 발급 + String refreshToken = refreshTokenService.getRefreshToken(email); + if ( jwtTokenProvider.validateToken(refreshToken) ) { + // 이메일로 새로운 액세스 토큰 발급 String newAccessToken = jwtTokenProvider.createAccessToken(email); - String newRefreshToken = jwtTokenProvider.createRefreshToken(email); + log.info("새 액세스 토큰: " + newAccessToken); response.setHeader("Authorization", "Bearer " + newAccessToken); - response.setHeader("Refresh-Token", newRefreshToken); // 새로 발급된 액세스 토큰으로 인증 처리 authenticateWithToken(newAccessToken); - } else { - // 토큰이 없는 경우 - throw new TokenNotExistsException(); + } + else { + log.info("리프레시 토큰 만료"); + throw new TokenExpiredException(); } } diff --git a/src/main/java/com/Alchive/backend/config/jwt/JwtController.java b/src/main/java/com/Alchive/backend/config/jwt/JwtController.java index 8eef923..9cc4c83 100644 --- a/src/main/java/com/Alchive/backend/config/jwt/JwtController.java +++ b/src/main/java/com/Alchive/backend/config/jwt/JwtController.java @@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import static com.Alchive.backend.config.result.ResultCode.TOKEN_ACCESS_SUCCESS; +import static com.Alchive.backend.config.result.ResultCode.*; @Tag(name = "JWT", description = "[Test] JWT 관련 api입니다.") @RequiredArgsConstructor @@ -19,10 +19,10 @@ public class JwtController { private final JwtTokenProvider jwtTokenProvider; - @Operation(summary = "토큰 재발급 메서드", description = "액세스 토큰을 재발급하는 메서드입니다.") - @GetMapping("") - public ResponseEntity createToken(String email) { + @Operation(summary = "액세스 토큰 재발급 메서드", description = "액세스 토큰을 재발급하는 메서드입니다.") + @GetMapping("/access") + public ResponseEntity createAccessToken(String email) { String accessToken = jwtTokenProvider.createAccessToken(email); - return ResponseEntity.ok(ResultResponse.of(TOKEN_ACCESS_SUCCESS, accessToken)); + return ResponseEntity.ok(ResultResponse.of(TOKEN_ACCESS_CREATE_SUCCESS, accessToken)); } } diff --git a/src/main/java/com/Alchive/backend/config/jwt/JwtTokenProvider.java b/src/main/java/com/Alchive/backend/config/jwt/JwtTokenProvider.java index 6f9b074..a527244 100644 --- a/src/main/java/com/Alchive/backend/config/jwt/JwtTokenProvider.java +++ b/src/main/java/com/Alchive/backend/config/jwt/JwtTokenProvider.java @@ -1,13 +1,14 @@ package com.Alchive.backend.config.jwt; import com.Alchive.backend.config.error.exception.token.TokenExpiredException; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import com.Alchive.backend.config.error.exception.token.TokenNotExistsException; +import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; import jakarta.annotation.PostConstruct; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -34,15 +35,16 @@ protected void init() { // 액세스 및 리프레시 토큰 생성 public String createAccessToken(String email) { - return createToken(email, ACCESS_EXPIRE_LENGTH); + return createToken(email, ACCESS_EXPIRE_LENGTH, "ACCESS"); } public String createRefreshToken(String email) { - return createToken(email, REFRESH_EXPIRE_LENGTH); + return createToken(email, REFRESH_EXPIRE_LENGTH, "REFRESH"); } - private String createToken(String email, Long expireLength) { + private String createToken(String email, Long expireLength, String type) { Claims claims = Jwts.claims().setSubject(email); + claims.put("type", type); return Jwts.builder().setClaims(claims) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + expireLength)) @@ -66,30 +68,41 @@ private String resolveToken(HttpServletRequest request, String headerName, Strin String header = request.getHeader(headerName); return prefix.isEmpty() ? header : header.substring(prefix.length()); } catch (NullPointerException | IllegalArgumentException e) { - return null; + throw new TokenNotExistsException(); } } // 토큰 검증 public boolean validateToken(String token) { try { - Jwts.parserBuilder() + Claims claims = Jwts.parserBuilder() .setSigningKey(secretKey) .build() - .parseClaimsJws(token); + .parseClaimsJws(token) + .getBody(); + String type = (String) claims.get("type"); + if (!type.equals("ACCESS")) { + throw new TokenNotExistsException(); + } return true; - } catch (Exception e) { - throw new TokenExpiredException(); + } catch (ExpiredJwtException e) { + return false; + } catch (JwtException e) { + throw e; } } // 이메일 추출 public String getEmailFromToken(String token) { - Claims claims = Jwts.parserBuilder() - .setSigningKey(secretKey) - .build() - .parseClaimsJws(token) - .getBody(); - return claims.getSubject(); + try { + Claims claims = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + return claims.getSubject(); + } catch (ExpiredJwtException exception) { + return exception.getClaims().getSubject(); + } } } diff --git a/src/main/java/com/Alchive/backend/config/redis/CreateRefreshTokenRequest.java b/src/main/java/com/Alchive/backend/config/redis/CreateRefreshTokenRequest.java new file mode 100644 index 0000000..4fbfa0a --- /dev/null +++ b/src/main/java/com/Alchive/backend/config/redis/CreateRefreshTokenRequest.java @@ -0,0 +1,9 @@ +package com.Alchive.backend.config.redis; + +import lombok.Getter; + +@Getter +public class CreateRefreshTokenRequest { + private String email; + private String refreshToken; +} diff --git a/src/main/java/com/Alchive/backend/config/redis/RefreshToken.java b/src/main/java/com/Alchive/backend/config/redis/RefreshToken.java new file mode 100644 index 0000000..889ac22 --- /dev/null +++ b/src/main/java/com/Alchive/backend/config/redis/RefreshToken.java @@ -0,0 +1,17 @@ +package com.Alchive.backend.config.redis; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +@AllArgsConstructor +@Getter +@RedisHash(value = "refreshToken", timeToLive = 6000000) +public class RefreshToken { + @Id + @Indexed + private String email; + private String refreshToken; +} diff --git a/src/main/java/com/Alchive/backend/config/redis/RefreshTokenController.java b/src/main/java/com/Alchive/backend/config/redis/RefreshTokenController.java new file mode 100644 index 0000000..cd1d920 --- /dev/null +++ b/src/main/java/com/Alchive/backend/config/redis/RefreshTokenController.java @@ -0,0 +1,40 @@ +package com.Alchive.backend.config.redis; + +import com.Alchive.backend.config.result.ResultResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static com.Alchive.backend.config.result.ResultCode.*; + +@RestController +@Tag(name = "리프레시 토큰", description = "[Test] 리프레시 토큰 관련 api입니다. ") +@RequestMapping("/api/v2/refreshTokens") +@RequiredArgsConstructor +public class RefreshTokenController { + private final RefreshTokenService refreshTokenService; + + @Operation(summary = "리프레시 토큰 저장 메서드", description = "리프레시 토큰을 Redis에 저장하는 메서드입니다. ") + @PostMapping("") + public ResponseEntity saveRefreshToken(@RequestBody CreateRefreshTokenRequest request) { + refreshTokenService.saveRefreshToken(request.getEmail(), request.getRefreshToken()); + return ResponseEntity.ok(ResultResponse.of(TOKEN_REFRESH_SAVE_SUCCESS, request.getRefreshToken())); + } + + @Operation(summary = "리프레시 토큰 검색 메서드", description = "이메일로 리프레시 토큰을 가져오는 메서드입니다. ") + @GetMapping("") + public ResponseEntity getResfreshToken(@RequestParam String email) { + String refreshToken = refreshTokenService.getRefreshToken(email); + return ResponseEntity.ok(ResultResponse.of(TOKEN_REFRESH_GET_SUCCESS, refreshToken)); + } + + @Operation(summary = "리프레시 토큰 재발급 메서드", description = "이메일로 리프레시 토큰을 재발급하는 메서드입니다. ") + @GetMapping("/newToken") + public ResponseEntity createRefreshToken(@RequestParam String email) { + String refreshToken = refreshTokenService.createRefreshToken(email); + refreshTokenService.saveRefreshToken(email, refreshToken); + return ResponseEntity.ok(ResultResponse.of(TOKEN_REFRESH_CREATE_SUCCESS, refreshToken)); + } +} diff --git a/src/main/java/com/Alchive/backend/config/redis/RefreshTokenRepository.java b/src/main/java/com/Alchive/backend/config/redis/RefreshTokenRepository.java new file mode 100644 index 0000000..3b7ee23 --- /dev/null +++ b/src/main/java/com/Alchive/backend/config/redis/RefreshTokenRepository.java @@ -0,0 +1,9 @@ +package com.Alchive.backend.config.redis; + +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface RefreshTokenRepository extends CrudRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/Alchive/backend/config/redis/RefreshTokenService.java b/src/main/java/com/Alchive/backend/config/redis/RefreshTokenService.java new file mode 100644 index 0000000..b1a67af --- /dev/null +++ b/src/main/java/com/Alchive/backend/config/redis/RefreshTokenService.java @@ -0,0 +1,35 @@ +package com.Alchive.backend.config.redis; + +import com.Alchive.backend.config.error.exception.user.NoSuchUserIdException; +import com.Alchive.backend.config.jwt.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class RefreshTokenService { + private final RefreshTokenRepository refreshTokenRepository; + private final JwtTokenProvider jwtTokenProvider; + + @Transactional + public void saveRefreshToken(String email, String refreshToken) { + refreshTokenRepository.save(new RefreshToken(email, refreshToken)); + } + + public String getRefreshToken(String email) { + RefreshToken refreshToken = refreshTokenRepository.findByEmail(email) + .orElseThrow(NoSuchUserIdException::new); + return refreshToken.getRefreshToken(); + } + + @Transactional + public String createRefreshToken(String email) { + return jwtTokenProvider.createRefreshToken(email); + } + + @Transactional + public void deleteRefreshToken(String email) { + refreshTokenRepository.deleteById(email); + } +} diff --git a/src/main/java/com/Alchive/backend/config/result/ResultCode.java b/src/main/java/com/Alchive/backend/config/result/ResultCode.java index 792d6ef..ab14c1f 100644 --- a/src/main/java/com/Alchive/backend/config/result/ResultCode.java +++ b/src/main/java/com/Alchive/backend/config/result/ResultCode.java @@ -15,8 +15,10 @@ public enum ResultCode { USER_UPDATE_SUCCESS("U008", "회원 상세정보 수정 성공"), // AUTH - TOKEN_ACCESS_SUCCESS("A001", "액세스 토큰 생성 성공"), - TOKEN_REFRESH_SUCCESS("A002", "리프레쉬 토큰 생성 성공"), + TOKEN_ACCESS_CREATE_SUCCESS("A001", "액세스 토큰 생성 성공"), + TOKEN_REFRESH_CREATE_SUCCESS("A002", "리프레쉬 토큰 생성 성공"), + TOKEN_REFRESH_SAVE_SUCCESS("A003", "리프레쉬 토큰 저장 성공"), + TOKEN_REFRESH_GET_SUCCESS("A003", "리프레쉬 토큰 조회 성공"), // SOLUTION SOLUTION_CREATE_SUCCESS("S001", "풀이 생성 성공"), diff --git a/src/main/java/com/Alchive/backend/dto/response/UserResponseDTO.java b/src/main/java/com/Alchive/backend/dto/response/UserResponseDTO.java index e366cbc..66c78c1 100644 --- a/src/main/java/com/Alchive/backend/dto/response/UserResponseDTO.java +++ b/src/main/java/com/Alchive/backend/dto/response/UserResponseDTO.java @@ -11,13 +11,11 @@ public class UserResponseDTO { private String userEmail; private String userName; private String accessToken; - private String refreshToken; - public UserResponseDTO(User user, String accessToken, String refreshToken) { + public UserResponseDTO(User user, String accessToken) { this.userId=user.getId(); this.userEmail=user.getEmail(); this.userName=user.getName(); this.accessToken=accessToken; - this.refreshToken=refreshToken; } } diff --git a/src/main/java/com/Alchive/backend/service/UserService.java b/src/main/java/com/Alchive/backend/service/UserService.java index 38a3ff9..e52a92c 100644 --- a/src/main/java/com/Alchive/backend/service/UserService.java +++ b/src/main/java/com/Alchive/backend/service/UserService.java @@ -5,6 +5,7 @@ import com.Alchive.backend.config.error.exception.user.UserEmailExistException; import com.Alchive.backend.config.error.exception.user.UserNameExistException; import com.Alchive.backend.config.jwt.JwtTokenProvider; +import com.Alchive.backend.config.redis.RefreshTokenService; import com.Alchive.backend.domain.user.User; import com.Alchive.backend.dto.request.UserCreateRequest; import com.Alchive.backend.dto.request.UserUpdateRequest; @@ -21,6 +22,7 @@ public class UserService { private final UserRepository userRepository; private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenService refreshTokenService; @Transactional public UserResponseDTO createUser(UserCreateRequest request) { @@ -39,8 +41,9 @@ public UserResponseDTO createUser(UserCreateRequest request) { user = userRepository.save(user); // 토큰 생성 후 전달 String accessToken = jwtTokenProvider.createAccessToken(email); - String refreshToken = jwtTokenProvider.createRefreshToken(email); - return new UserResponseDTO(user, accessToken, refreshToken); + String refreshToken = refreshTokenService.createRefreshToken(email); + refreshTokenService.saveRefreshToken(email, refreshToken); + return new UserResponseDTO(user, accessToken); } public boolean isDuplicateUsername(String name) { @@ -54,12 +57,15 @@ public User getUserDetail(Long userId) { @Transactional public User updateUserDetail(User user, UserUpdateRequest updateRequest) { - return user.update(updateRequest.getDescription(), updateRequest.getAutoSave()); + User updatedUser = userRepository.findById(user.getId()) + .orElseThrow(NoSuchUserIdException::new); + return updatedUser.update(updateRequest.getDescription(), updateRequest.getAutoSave()); } @Transactional public void deleteUserDetail(User user) { userRepository.delete(user); + refreshTokenService.deleteRefreshToken(user.getEmail()); } public void validateUser(Long userId, Long requestedId) {