diff --git a/.gitignore b/.gitignore index 8704231..99a7de0 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ out/ ### VS Code ### .vscode/ +/mysql-data/ diff --git a/docker-compose.yaml b/docker-compose.yaml index a6c841e..1a26d74 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,12 @@ +version: "3" services: mysql: - image: 'mysql:latest' + image: mysql:8.0 + container_name: festimate-mysql-1 env_file: - .env ports: - - '3306:3306' + - "3306:3306" + volumes: + - ./mysql-data:/var/lib/mysql + restart: unless-stopped diff --git a/src/main/java/org/festimate/team/api/admin/AdminController.java b/src/main/java/org/festimate/team/api/admin/AdminController.java index 48db59b..389194d 100644 --- a/src/main/java/org/festimate/team/api/admin/AdminController.java +++ b/src/main/java/org/festimate/team/api/admin/AdminController.java @@ -6,7 +6,6 @@ import org.festimate.team.api.point.dto.PointHistoryResponse; import org.festimate.team.global.response.ApiResponse; import org.festimate.team.global.response.ResponseBuilder; -import org.festimate.team.infra.jwt.JwtService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -16,7 +15,6 @@ @RequestMapping("/v1/admin/festivals") @RequiredArgsConstructor public class AdminController { - private final JwtService jwtService; private final FestivalFacade festivalFacade; private final FestivalHostFacade festivalHostFacade; private final ParticipantFacade participantFacade; @@ -25,85 +23,76 @@ public class AdminController { @PostMapping() public ResponseEntity> createFestival( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @RequestBody FestivalRequest request ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); FestivalResponse response = festivalFacade.createFestival(userId, request); return ResponseBuilder.created(response); } @GetMapping() public ResponseEntity>> getAllFestivals( - @RequestHeader("Authorization") String accessToken + @RequestAttribute("userId") Long userId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); List response = festivalFacade.getAllFestivals(userId); return ResponseBuilder.ok(response); } @GetMapping("/{festivalId}") public ResponseEntity> getFestivalDetail( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); AdminFestivalDetailResponse response = festivalFacade.getFestivalDetail(userId, festivalId); return ResponseBuilder.ok(response); } @GetMapping("/{festivalId}/participants/search") public ResponseEntity>> getParticipantByNickname( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @RequestParam("nickname") String nickname ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); - List response = participantFacade.getParticipantByNickname(userId, festivalId, nickname); return ResponseBuilder.ok(response); } @PostMapping("/{festivalId}/points") public ResponseEntity> rechargePoints( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @RequestBody RechargePointRequest request ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); pointFacade.rechargePoints(userId, festivalId, request); return ResponseBuilder.ok(null); } @PostMapping("/{festivalId}/hosts") public ResponseEntity> addHost( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @RequestBody AddHostRequest request ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); festivalHostFacade.addHost(userId, festivalId, request); return ResponseBuilder.created(null); } @GetMapping("/{festivalId}/participants/{participantId}/points") public ResponseEntity> getParticipantPointHistory( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @PathVariable("participantId") Long participantId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); PointHistoryResponse response = pointFacade.getParticipantPointHistory(userId, festivalId, participantId); return ResponseBuilder.ok(response); } @GetMapping("/{festivalId}/participants/{participantId}/matchings") public ResponseEntity> getParticipantMatchingHistory( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @PathVariable("participantId") Long participantId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); AdminMatchingResponse response = matchingFacade.getMatchingSize(userId, festivalId, participantId); return ResponseBuilder.ok(response); } diff --git a/src/main/java/org/festimate/team/api/auth/AuthController.java b/src/main/java/org/festimate/team/api/auth/AuthController.java index 2070f17..33aca5a 100644 --- a/src/main/java/org/festimate/team/api/auth/AuthController.java +++ b/src/main/java/org/festimate/team/api/auth/AuthController.java @@ -26,7 +26,7 @@ public class AuthController { public ResponseEntity> login( @RequestHeader("Authorization") String kakaoAccessToken ) { - log.info("social login - Code: {}", kakaoAccessToken); + log.info("social login - kakaoAccessToken: {}", kakaoAccessToken); String platformId = loginFacade.getPlatformId(kakaoAccessToken); diff --git a/src/main/java/org/festimate/team/api/facade/LoginFacade.java b/src/main/java/org/festimate/team/api/facade/LoginFacade.java index 2c82621..ece800d 100644 --- a/src/main/java/org/festimate/team/api/facade/LoginFacade.java +++ b/src/main/java/org/festimate/team/api/facade/LoginFacade.java @@ -6,7 +6,7 @@ import org.festimate.team.domain.auth.service.KakaoLoginService; import org.festimate.team.domain.user.entity.Platform; import org.festimate.team.domain.user.service.UserService; -import org.festimate.team.infra.jwt.JwtService; +import org.festimate.team.infra.jwt.JwtTokenProvider; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -15,7 +15,7 @@ @RequiredArgsConstructor public class LoginFacade { - private final JwtService jwtService; + private final JwtTokenProvider jwtTokenProvider; private final UserService userService; private final KakaoLoginService kakaoLoginService; @@ -28,10 +28,10 @@ public TokenResponse login(String platformId, Platform platform) { private TokenResponse loginExistingUser(Long userId) { log.info("기존 유저 로그인 성공 - userId: {}", userId); - String newRefreshToken = jwtService.createRefreshToken(userId); + String newRefreshToken = jwtTokenProvider.createRefreshToken(userId); userService.updateRefreshToken(userId, newRefreshToken); - return new TokenResponse(userId, jwtService.createAccessToken(userId), newRefreshToken); + return new TokenResponse(userId, jwtTokenProvider.createAccessToken(userId), newRefreshToken); } public String getPlatformId(String authorization) { @@ -39,7 +39,7 @@ public String getPlatformId(String authorization) { } private TokenResponse createTemporaryToken(String platformId) { - return new TokenResponse(null, jwtService.createTempAccessToken(platformId), jwtService.createTempRefreshToken(platformId)); + return new TokenResponse(null, jwtTokenProvider.createTempAccessToken(platformId), jwtTokenProvider.createTempRefreshToken(platformId)); } } diff --git a/src/main/java/org/festimate/team/api/facade/SignUpFacade.java b/src/main/java/org/festimate/team/api/facade/SignUpFacade.java index 823b40d..694660f 100644 --- a/src/main/java/org/festimate/team/api/facade/SignUpFacade.java +++ b/src/main/java/org/festimate/team/api/facade/SignUpFacade.java @@ -9,14 +9,14 @@ import org.festimate.team.domain.user.validator.NicknameValidator; import org.festimate.team.global.exception.FestimateException; import org.festimate.team.global.response.ResponseError; -import org.festimate.team.infra.jwt.JwtService; +import org.festimate.team.infra.jwt.JwtTokenProvider; import org.springframework.stereotype.Component; @Slf4j @Component @RequiredArgsConstructor public class SignUpFacade { - private final JwtService jwtService; + private final JwtTokenProvider jwtTokenProvider; private final UserService userService; private final NicknameValidator nicknameValidator; @@ -36,8 +36,8 @@ public void validateNickname(String nickname) { private TokenResponse createTokenResponse(User user) { log.info("signup success - userId : {}, nickname : {}", user.getUserId(), user.getNickname()); - String accessToken = jwtService.createAccessToken(user.getUserId()); - String refreshToken = jwtService.createRefreshToken(user.getUserId()); + String accessToken = jwtTokenProvider.createAccessToken(user.getUserId()); + String refreshToken = jwtTokenProvider.createRefreshToken(user.getUserId()); userService.updateRefreshToken(user.getUserId(), refreshToken); return TokenResponse.of(user.getUserId(), accessToken, refreshToken); } diff --git a/src/main/java/org/festimate/team/api/festival/FestivalController.java b/src/main/java/org/festimate/team/api/festival/FestivalController.java index d7557cd..c9a095b 100644 --- a/src/main/java/org/festimate/team/api/festival/FestivalController.java +++ b/src/main/java/org/festimate/team/api/festival/FestivalController.java @@ -7,7 +7,6 @@ import org.festimate.team.api.festival.dto.FestivalVerifyResponse; import org.festimate.team.global.response.ApiResponse; import org.festimate.team.global.response.ResponseBuilder; -import org.festimate.team.infra.jwt.JwtService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -15,25 +14,22 @@ @RequestMapping("/v1/festivals") @RequiredArgsConstructor public class FestivalController { - private final JwtService jwtService; private final FestivalFacade festivalFacade; @PostMapping("/verify") public ResponseEntity> verifyFestival( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @RequestBody FestivalVerifyRequest request ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); FestivalVerifyResponse response = festivalFacade.verifyFestival(userId, request); return ResponseBuilder.ok(response); } @GetMapping("/{festivalId}") public ResponseEntity> getFestivalInfo( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); FestivalInfoResponse response = festivalFacade.getFestivalInfo(userId, festivalId); return ResponseBuilder.ok(response); } diff --git a/src/main/java/org/festimate/team/api/matching/MatchingController.java b/src/main/java/org/festimate/team/api/matching/MatchingController.java index 3c554f3..86254db 100644 --- a/src/main/java/org/festimate/team/api/matching/MatchingController.java +++ b/src/main/java/org/festimate/team/api/matching/MatchingController.java @@ -7,7 +7,6 @@ import org.festimate.team.api.matching.dto.MatchingStatusResponse; import org.festimate.team.global.response.ApiResponse; import org.festimate.team.global.response.ResponseBuilder; -import org.festimate.team.infra.jwt.JwtService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -16,38 +15,31 @@ @RequiredArgsConstructor public class MatchingController { private final MatchingFacade matchingFacade; - private final JwtService jwtService; @PostMapping("/{festivalId}/matchings") public ResponseEntity> createMatching( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); - MatchingStatusResponse response = matchingFacade.createMatching(userId, festivalId); return ResponseBuilder.created(response); } @GetMapping("/{festivalId}/matchings") public ResponseEntity> getMatching( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); - MatchingListResponse response = matchingFacade.getMatchingList(userId, festivalId); return ResponseBuilder.ok(response); } @GetMapping("/{festivalId}/matchings/{matchingId}") public ResponseEntity> getMatchingDetail( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @PathVariable("matchingId") Long matchingId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); - MatchingDetailInfo response = matchingFacade.getMatchingDetail(userId, festivalId, matchingId); return ResponseBuilder.ok(response); } diff --git a/src/main/java/org/festimate/team/api/participant/ParticipantController.java b/src/main/java/org/festimate/team/api/participant/ParticipantController.java index 473f5cc..d9b9bfb 100644 --- a/src/main/java/org/festimate/team/api/participant/ParticipantController.java +++ b/src/main/java/org/festimate/team/api/participant/ParticipantController.java @@ -6,7 +6,6 @@ import org.festimate.team.domain.participant.service.ParticipantService; import org.festimate.team.global.response.ApiResponse; import org.festimate.team.global.response.ResponseBuilder; -import org.festimate.team.infra.jwt.JwtService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -15,27 +14,24 @@ @RequiredArgsConstructor public class ParticipantController { - private final JwtService jwtService; private final ParticipantService participantService; private final ParticipantFacade participantFacade; @PostMapping("/{festivalId}/participants/type") public ResponseEntity> getFestivalType( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable Long festivalId, @RequestBody TypeRequest request ) { - jwtService.parseTokenAndGetUserId(accessToken); TypeResponse response = participantService.getTypeResult(request); return ResponseBuilder.ok(response); } @GetMapping("/{festivalId}/participants/me") public ResponseEntity> entryFestival( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); EntryResponse response = participantFacade.entryFestival(userId, festivalId); return ResponseBuilder.ok(response); @@ -43,31 +39,28 @@ public ResponseEntity> entryFestival( @PostMapping("/{festivalId}/participants") public ResponseEntity> createParticipant( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @RequestBody ProfileRequest request ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); EntryResponse response = participantFacade.createParticipant(userId, festivalId, request); return ResponseBuilder.created(response); } @GetMapping("/{festivalId}/participants/me/profile") public ResponseEntity> getMyProfile( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); ProfileResponse response = participantFacade.getParticipantProfile(userId, festivalId); return ResponseBuilder.ok(response); } @GetMapping("/{festivalId}/participants/me/summary") public ResponseEntity> getParticipantAndPoint( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); MainUserInfoResponse response = participantFacade.getParticipantSummary(userId, festivalId); return ResponseBuilder.ok(response); @@ -75,10 +68,9 @@ public ResponseEntity> getParticipantAndPoint( @GetMapping("/{festivalId}/participants/me/type") public ResponseEntity> getParticipantType( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); DetailProfileResponse response = participantFacade.getParticipantType(userId, festivalId); return ResponseBuilder.ok(response); @@ -86,11 +78,10 @@ public ResponseEntity> getParticipantType( @PatchMapping("/{festivalId}/participants/me/message") public ResponseEntity> modifyMyMessage( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @RequestBody MessageRequest request ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); participantFacade.modifyMessage(userId, festivalId, request); return ResponseBuilder.created(null); diff --git a/src/main/java/org/festimate/team/api/point/PointController.java b/src/main/java/org/festimate/team/api/point/PointController.java index 2ee3c39..5596ee6 100644 --- a/src/main/java/org/festimate/team/api/point/PointController.java +++ b/src/main/java/org/festimate/team/api/point/PointController.java @@ -5,7 +5,6 @@ import org.festimate.team.api.point.dto.PointHistoryResponse; import org.festimate.team.global.response.ApiResponse; import org.festimate.team.global.response.ResponseBuilder; -import org.festimate.team.infra.jwt.JwtService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -14,15 +13,13 @@ @RequiredArgsConstructor public class PointController { - private final JwtService jwtService; private final PointFacade pointFacade; @GetMapping("/festivals/{festivalId}/participants/me/points") public ResponseEntity> getMyPointHistory( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); PointHistoryResponse response = pointFacade.getMyPointHistory(userId, festivalId); return ResponseBuilder.ok(response); } diff --git a/src/main/java/org/festimate/team/api/user/UserController.java b/src/main/java/org/festimate/team/api/user/UserController.java index 96480c0..939dcb6 100644 --- a/src/main/java/org/festimate/team/api/user/UserController.java +++ b/src/main/java/org/festimate/team/api/user/UserController.java @@ -56,20 +56,18 @@ public ResponseEntity> signUp( @GetMapping("/me/nickname") public ResponseEntity> getNickname( - @RequestHeader("Authorization") String accessToken + @RequestAttribute("userId") Long userId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); UserInfoDto userInfoDto = userService.getUserNicknameAndAppearanceType(userId); return ResponseBuilder.ok(UserInfoResponse.from(userInfoDto.nickname(), userInfoDto.appearanceType())); } @GetMapping("/me/festivals") public ResponseEntity>> getMyFestival( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @RequestParam("status") String status ) { userRequestValidator.statusValidate(status); - Long userId = jwtService.parseTokenAndGetUserId(accessToken); return ResponseBuilder.ok(festivalFacade.getUserFestivals(userId, status)); } } diff --git a/src/main/java/org/festimate/team/infra/config/SecurityConfig.java b/src/main/java/org/festimate/team/infra/config/SecurityConfig.java index 4cbce38..5a7b5f7 100644 --- a/src/main/java/org/festimate/team/infra/config/SecurityConfig.java +++ b/src/main/java/org/festimate/team/infra/config/SecurityConfig.java @@ -1,24 +1,31 @@ package org.festimate.team.infra.config; +import lombok.RequiredArgsConstructor; +import org.festimate.team.infra.jwt.JwtAuthFilter; +import org.festimate.team.infra.jwt.JwtParser; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { + private final JwtParser jwtParser; + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .cors(cors -> {}) + .cors(cors -> { + }) .csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll() - ); + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + .addFilterBefore(new JwtAuthFilter(jwtParser), UsernamePasswordAuthenticationFilter.class); return http.build(); } diff --git a/src/main/java/org/festimate/team/infra/jwt/JwtAuthFilter.java b/src/main/java/org/festimate/team/infra/jwt/JwtAuthFilter.java new file mode 100644 index 0000000..6075af9 --- /dev/null +++ b/src/main/java/org/festimate/team/infra/jwt/JwtAuthFilter.java @@ -0,0 +1,40 @@ +package org.festimate.team.infra.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + private final JwtParser jwtParser; + private static final String USER_ID = "userId"; + private static final String AUTHORIZATION = "Authorization"; + private static final String BEARER = "Bearer "; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String header = request.getHeader(AUTHORIZATION); + if (header != null && header.startsWith(BEARER)) { + String token = header.substring(BEARER.length()); + try { + Long userId = jwtParser.getUserIdFromToken(token); + request.setAttribute(USER_ID, userId); + } catch (Exception e) { + log.warn("JWT authentication failed: {}", e.getMessage()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token"); + return; + } + } + + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/org/festimate/team/infra/jwt/JwtParser.java b/src/main/java/org/festimate/team/infra/jwt/JwtParser.java new file mode 100644 index 0000000..bdfec5b --- /dev/null +++ b/src/main/java/org/festimate/team/infra/jwt/JwtParser.java @@ -0,0 +1,69 @@ +package org.festimate.team.infra.jwt; + + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.festimate.team.global.exception.FestimateException; +import org.festimate.team.global.response.ResponseError; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtParser { + private final JwtTokenProvider jwtTokenProvider; + private static final String USER_ID = "userId"; + private static final String PLATFORM_ID = "platformId"; + private static final String BEARER = "Bearer "; + + public Claims parseClaims(String token) { + try { + return Jwts.parser() + .verifyWith(jwtTokenProvider.getSecretKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } catch (Exception e) { + log.error("JWT 파싱 오류: {}", e.getMessage()); + throw new FestimateException(ResponseError.INVALID_TOKEN); + } + } + + public Long getUserIdFromToken(String token) { + Claims claims = parseClaims(token); + Object userId = claims.get(USER_ID); + if (userId instanceof Number) return ((Number) userId).longValue(); + if (userId instanceof String) { + try { + return Long.parseLong((String) userId); + } catch (NumberFormatException e) { + log.error("Invalid userId format in token: {}", userId); + } + } + log.error("userId claim missing or invalid type: {}", userId); + throw new FestimateException(ResponseError.INVALID_TOKEN); + } + + public String getPlatformIdFromToken(String token) { + Claims claims = parseClaims(token); + Object platformId = claims.get(PLATFORM_ID); + if (platformId != null) return platformId.toString(); + throw new FestimateException(ResponseError.INVALID_TOKEN); + } + + public void validateToken(String token) { + if (token == null || !token.startsWith(BEARER)) { + throw new FestimateException(ResponseError.INVALID_TOKEN); + } + try { + String splitToken = token.substring(BEARER.length()); + parseClaims(splitToken); + } catch (Exception e) { + log.error("JWT 유효성 오류: {}", e.getMessage()); + throw new FestimateException(ResponseError.INVALID_TOKEN); + } + } +} + diff --git a/src/main/java/org/festimate/team/infra/jwt/JwtService.java b/src/main/java/org/festimate/team/infra/jwt/JwtService.java index c17c04d..13eeeff 100644 --- a/src/main/java/org/festimate/team/infra/jwt/JwtService.java +++ b/src/main/java/org/festimate/team/infra/jwt/JwtService.java @@ -1,192 +1,32 @@ package org.festimate.team.infra.jwt; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.festimate.team.api.auth.dto.TokenResponse; -import org.festimate.team.global.response.ResponseError; -import org.festimate.team.global.exception.FestimateException; import org.festimate.team.domain.user.entity.User; import org.festimate.team.domain.user.service.UserService; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.stereotype.Service; -import javax.crypto.SecretKey; -import java.nio.charset.StandardCharsets; -import java.util.Date; - -@Component @RequiredArgsConstructor @Slf4j +@Service public class JwtService { - private static final String USER_ID = "userId"; - private static final String PLATFROM_ID = "platformId"; - private static final String BEARER = "Bearer "; - private static final ObjectMapper objectMapper = new ObjectMapper(); - private final JwtProperties jwtProperties; + private final JwtTokenProvider tokenProvider; + private final JwtParser jwtParser; private final UserService userService; - public String createAccessToken(final Long userId) { - SecretKey secretKey = getSecretKey(); - return Jwts.builder() - .subject(userId.toString()) - .expiration(new Date(System.currentTimeMillis() + jwtProperties.getAccessExpiration())) - .claim(USER_ID, userId) - .signWith(secretKey) - .compact(); - } - - public String createRefreshToken(final Long userId) { - SecretKey secretKey = getSecretKey(); - return buildRefreshToken(userId, secretKey); - } - - public String createTempAccessToken(final String platformId) { - SecretKey secretKey = getSecretKey(); - return Jwts.builder() - .subject(platformId) - .expiration(new Date(System.currentTimeMillis() + jwtProperties.getAccessExpiration())) - .claim(PLATFROM_ID, platformId) - .signWith(secretKey) - .compact(); - } - - public String createTempRefreshToken(final String platformId) { - SecretKey secretKey = getSecretKey(); - return Jwts.builder() - .subject(platformId) - .expiration(new Date(System.currentTimeMillis() + jwtProperties.getRefreshExpiration())) - .claim(PLATFROM_ID, platformId) - .signWith(secretKey) - .compact(); - } - - - private String buildRefreshToken(Long userId, SecretKey secretKey) { - return Jwts.builder() - .subject(userId.toString()) - .expiration(new Date(System.currentTimeMillis() + jwtProperties.getRefreshExpiration())) - .claim(USER_ID, userId) - .signWith(secretKey) - .compact(); - } - - @Transactional - public TokenResponse reIssueToken(final String refreshToken) { - Long userId = parseTokenAndGetUserId(refreshToken); - User findUser = userService.getUserByIdOrThrow(userId); - String extractPrefixToken = refreshToken.split(" ")[1]; - isValidRefreshToken(findUser, extractPrefixToken); - String renewRefreshToken = createRefreshToken(userId); - findUser.updateRefreshToken(renewRefreshToken); - - return TokenResponse.of(userId, createAccessToken(userId), renewRefreshToken); - } - - private void isValidRefreshToken(User findUser, String refreshToken) { - userService.validateRefreshToken(findUser, refreshToken); - } - - public Long parseTokenAndGetUserId(String token) { - isValidToken(token); - - try { - String splitToken = token.split(" ")[1]; - SecretKey secretKey = getSecretKey(); - Long userId = parseTokenAndGetUserId(secretKey, splitToken); - - if (userService.getUserByIdOrThrow(userId).getRefreshToken() == null) { - throw new FestimateException(ResponseError.INVALID_TOKEN); - } - return userId; - } catch (JwtException | NumberFormatException e) { - log.error("JWT parsing error : {}", e.getMessage()); - throw new FestimateException(ResponseError.INVALID_TOKEN); - } + public TokenResponse reIssueToken(String refreshToken) { + Long userId = jwtParser.getUserIdFromToken(refreshToken); + User user = userService.getUserByIdOrThrow(userId); + userService.validateRefreshToken(user, refreshToken); + String newRefreshToken = tokenProvider.createRefreshToken(userId); + user.updateRefreshToken(newRefreshToken); + return TokenResponse.of(userId, tokenProvider.createAccessToken(userId), newRefreshToken); } - private Long parseTokenAndGetUserId(SecretKey secretKey, String token) { - Claims claims = Jwts.parser() - .verifyWith(secretKey) - .build() - .parseSignedClaims(token) - .getPayload(); - - if (!claims.containsKey(USER_ID)) { - log.error("잘못된 accessToken: userId 없음."); - throw new FestimateException(ResponseError.INVALID_ACCESS_TOKEN); - } - - try { - Object userIdObject = claims.get(USER_ID); - if (userIdObject instanceof Number) { - return ((Number) userIdObject).longValue(); - } else { - throw new FestimateException(ResponseError.INVALID_TOKEN); - } - } catch (Exception e) { - log.error("userId 변환 오류: {}", e.getMessage()); - throw new FestimateException(ResponseError.INVALID_TOKEN); - } - } - - public String extractPlatformUserIdFromToken(String token) { - try { - String splitToken = token.split(" ")[1]; - SecretKey secretKey = getSecretKey(); - return parseTokenAndGetPlatformUserId(secretKey, splitToken); - } catch (JwtException e) { - log.error("JWT parsing error: {}", e.getMessage()); - throw new FestimateException(ResponseError.INVALID_TOKEN); - } - } - - private String parseTokenAndGetPlatformUserId(SecretKey secretKey, String token) { - return Jwts.parser() - .verifyWith(secretKey) - .build() - .parseSignedClaims(token) - .getPayload() - .get(PLATFROM_ID) - .toString(); - } - - - private SecretKey getSecretKey() { - return Keys.hmacShaKeyFor( - jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8)); - } - - - public void isValidToken(String token) { - if (token == null || !token.startsWith(BEARER)) { - throw new FestimateException(ResponseError.INVALID_TOKEN); - } - - try { - String splitToken = token.split(" ")[1]; - SecretKey secretKey = getSecretKey(); - - parseTokenAndGetUserId(secretKey, splitToken); - } catch (JwtException | NumberFormatException e) { - log.error("JWT parsing error : {}", e.getMessage()); - throw new FestimateException(ResponseError.INVALID_TOKEN); - } - } - - public static JsonNode parseJson(String jsonString) { - try { - return objectMapper.readTree(jsonString); - } catch (Exception e) { - log.error("JSON 파싱 실패", e); - throw new IllegalArgumentException("JSON 파싱에 실패했습니다."); - } + return jwtParser.getPlatformIdFromToken(token); } } + diff --git a/src/main/java/org/festimate/team/infra/jwt/JwtTokenProvider.java b/src/main/java/org/festimate/team/infra/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..1b4dece --- /dev/null +++ b/src/main/java/org/festimate/team/infra/jwt/JwtTokenProvider.java @@ -0,0 +1,59 @@ +package org.festimate.team.infra.jwt; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; + + +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + private final JwtProperties jwtProperties; + private static final String USER_ID = "userId"; + private static final String PLATFORM_ID = "platformId"; + + public String createAccessToken(Long userId) { + return Jwts.builder() + .subject(userId.toString()) + .expiration(new Date(System.currentTimeMillis() + jwtProperties.getAccessExpiration())) + .claim(USER_ID, userId) + .signWith(getSecretKey()) + .compact(); + } + + public String createRefreshToken(Long userId) { + return Jwts.builder() + .subject(userId.toString()) + .expiration(new Date(System.currentTimeMillis() + jwtProperties.getRefreshExpiration())) + .claim(USER_ID, userId) + .signWith(getSecretKey()) + .compact(); + } + + public String createTempAccessToken(String platformId) { + return Jwts.builder() + .subject(platformId) + .expiration(new Date(System.currentTimeMillis() + jwtProperties.getAccessExpiration())) + .claim(PLATFORM_ID, platformId) + .signWith(getSecretKey()) + .compact(); + } + + public String createTempRefreshToken(String platformId) { + return Jwts.builder() + .subject(platformId) + .expiration(new Date(System.currentTimeMillis() + jwtProperties.getRefreshExpiration())) + .claim(PLATFORM_ID, platformId) + .signWith(getSecretKey()) + .compact(); + } + + public SecretKey getSecretKey() { + return Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/test/java/org/festimate/team/api/facade/LoginFacadeTest.java b/src/test/java/org/festimate/team/api/facade/LoginFacadeTest.java index fd00ee5..1233322 100644 --- a/src/test/java/org/festimate/team/api/facade/LoginFacadeTest.java +++ b/src/test/java/org/festimate/team/api/facade/LoginFacadeTest.java @@ -5,6 +5,7 @@ import org.festimate.team.domain.user.entity.Platform; import org.festimate.team.domain.user.service.UserService; import org.festimate.team.infra.jwt.JwtService; +import org.festimate.team.infra.jwt.JwtTokenProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -22,6 +23,8 @@ class LoginFacadeTest { @Mock private JwtService jwtService; @Mock + private JwtTokenProvider jwtTokenProvider; + @Mock private UserService userService; @Mock private KakaoLoginService kakaoLoginService; @@ -41,8 +44,8 @@ void login_existingUser_success() { when(userService.getUserIdByPlatform(Platform.KAKAO, "platformId")) .thenReturn(Optional.of(1L)); - when(jwtService.createAccessToken(1L)).thenReturn("access-token"); - when(jwtService.createRefreshToken(1L)).thenReturn("refresh-token"); + when(jwtTokenProvider.createAccessToken(1L)).thenReturn("access-token"); + when(jwtTokenProvider.createRefreshToken(1L)).thenReturn("refresh-token"); // when TokenResponse response = loginFacade.login("platformId", Platform.KAKAO); @@ -60,8 +63,8 @@ void login_newUser_tempToken_success() { when(userService.getUserIdByPlatform(Platform.KAKAO, "platformId")) .thenReturn(Optional.empty()); - when(jwtService.createTempAccessToken("platformId")).thenReturn("temp-access-token"); - when(jwtService.createTempRefreshToken("platformId")).thenReturn("temp-refresh-token"); + when(jwtTokenProvider.createTempAccessToken("platformId")).thenReturn("temp-access-token"); + when(jwtTokenProvider.createTempRefreshToken("platformId")).thenReturn("temp-refresh-token"); // when TokenResponse response = loginFacade.login("platformId", Platform.KAKAO); diff --git a/src/test/java/org/festimate/team/api/facade/SignUpFacadeTest.java b/src/test/java/org/festimate/team/api/facade/SignUpFacadeTest.java index 2d366f4..57135bf 100644 --- a/src/test/java/org/festimate/team/api/facade/SignUpFacadeTest.java +++ b/src/test/java/org/festimate/team/api/facade/SignUpFacadeTest.java @@ -8,7 +8,7 @@ import org.festimate.team.domain.user.validator.NicknameValidator; import org.festimate.team.global.exception.FestimateException; import org.festimate.team.global.response.ResponseError; -import org.festimate.team.infra.jwt.JwtService; +import org.festimate.team.infra.jwt.JwtTokenProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -26,7 +26,7 @@ class SignUpFacadeTest { @Mock private UserService userService; @Mock - private JwtService jwtService; + private JwtTokenProvider jwtTokenProvider; @Mock private NicknameValidator nicknameValidator; @@ -71,8 +71,8 @@ void signUp_success() { when(userService.getUserIdByPlatformId("platformId")).thenReturn(null); when(userService.signUp(signUpRequest, "platformId")).thenReturn(user); - when(jwtService.createAccessToken(1L)).thenReturn("access-token"); - when(jwtService.createRefreshToken(1L)).thenReturn("refresh-token"); + when(jwtTokenProvider.createAccessToken(1L)).thenReturn("access-token"); + when(jwtTokenProvider.createRefreshToken(1L)).thenReturn("refresh-token"); // when TokenResponse response = signUpFacade.signUp("platformId", signUpRequest);