diff --git a/src/main/java/com/anywayclear/config/CorsConfig.java b/src/main/java/com/anywayclear/config/CorsConfig.java index 7112ef7..9fb6c9d 100644 --- a/src/main/java/com/anywayclear/config/CorsConfig.java +++ b/src/main/java/com/anywayclear/config/CorsConfig.java @@ -19,12 +19,16 @@ public CorsFilter corsFilter() { // 요청과 응답에 인증 정보를 사용할 수 있도록 설정 config.setAllowCredentials(true); + // 모든 도메인에서 요청을 허용 (필요에 따라 특정 도메인만 허용 가능) config.addAllowedOriginPattern("*"); - // 모든 헤더를 허용 (필요한 경우 특정 헤더만 허용 가능) config.addAllowedHeader("*"); + + // 특정 exposed 헤더 허용 + config.addExposedHeader("newAccessToken"); + // 모든 HTTP 메서드를 허용 (필요한 경우 특정 메서드만 허용 가능) config.addAllowedMethod("*"); diff --git a/src/main/java/com/anywayclear/config/SecurityConfig.java b/src/main/java/com/anywayclear/config/SecurityConfig.java index 45278d4..f271931 100644 --- a/src/main/java/com/anywayclear/config/SecurityConfig.java +++ b/src/main/java/com/anywayclear/config/SecurityConfig.java @@ -56,21 +56,21 @@ public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws .exceptionHandling() .accessDeniedHandler(jwtAccessDeniedHandler()); - // 요청 설정 + // 인가 설정 httpSecurity .authorizeHttpRequests(authorize -> authorize .antMatchers(HttpMethod.OPTIONS).permitAll() // OPTIONS 메서드는 모두 허용 -// .antMatchers("/api/members/**").authenticated() // jwt없이 요청한건지 재확인 가능 - .antMatchers("/api/subscribes/**").authenticated() // jwt없이 요청한건지 재확인 가능 - .antMatchers("/api/dibs/**").authenticated() // jwt없이 요청한건지 재확인 가능 + .antMatchers("/api/members/**").authenticated() + .antMatchers("/api/reviews/**").authenticated() + .antMatchers("/api/points/**").authenticated() + .antMatchers("/api/subscribes/**").authenticated() + .antMatchers("/api/dibs/**").authenticated() .anyRequest().permitAll() ); // OAuth2 로그인 설정 httpSecurity .oauth2Login(oauth2Login -> oauth2Login - // 로그인 페이지 지정 -// .loginPage("https://mokumoku-git-develop-mokumoku.vercel.app") .userInfoEndpoint() .userService(custumOAuth2UserService) .and() diff --git a/src/main/java/com/anywayclear/config/jwt/JwtAuthorizationFilter.java b/src/main/java/com/anywayclear/config/jwt/JwtAuthorizationFilter.java index b18c6ef..6a2e0eb 100644 --- a/src/main/java/com/anywayclear/config/jwt/JwtAuthorizationFilter.java +++ b/src/main/java/com/anywayclear/config/jwt/JwtAuthorizationFilter.java @@ -10,6 +10,7 @@ import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.data.redis.RedisConnectionFailureException; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -26,8 +27,10 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; public class JwtAuthorizationFilter extends BasicAuthenticationFilter { @@ -44,43 +47,60 @@ public JwtAuthorizationFilter(AuthenticationManager authenticationManager, Membe @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - System.out.println("JwtAuthorizationFilter : 인증이나 권한이 필요한 주소 요청이 됨"); + System.out.println("JwtAuthorizationFilter : JWT 유효성 검사"); -// // 특정 경로에 대한 요청이라면 JWT 검사를 하지 않음 -// if (!request.getRequestURI().startsWith("/api/auctions")) { -// chain.doFilter(request, response); -// return; -// } + // 특정 경로에 대한 요청이라면 JWT 검사를 하지 않음 + if (request.getMethod().equals("GET") && request.getRequestURI().startsWith("/api/produces")) { + if (!request.getRequestURI().startsWith("/api/produces/")) { + chain.doFilter(request, response); + return; + } + } String jwtHeader = request.getHeader(jwtConfig.getHeader()); - System.out.println("jwtHeader = " + jwtHeader); - // JWT 토큰을 검증을 해서 정상적인 사용자인지 확인 (Header, Prefix 확인) + // JWT Header, Prefix 확인 if (jwtHeader == null || !jwtHeader.startsWith(jwtConfig.getPrefix())) { sendJsonResponse(response, ExceptionCode.INVALID_TOKEN); return; } - // JWT 토큰을 검증해서 정상적인 사용자인지 확인 (토큰 검증) + // JWT 유효성 검사 + String accessToken = request.getHeader(jwtConfig.getHeader()).replace(jwtConfig.getPrefix() + " ", ""); try { - String accessToken = request.getHeader(jwtConfig.getHeader()).replace(jwtConfig.getPrefix() + " ", ""); String userId = JWT.require(Algorithm.HMAC512(jwtConfig.getKey())).build().verify(accessToken).getClaim("userId").asString(); if (userId != null) { - Member member = memberRepository.findByUserId(userId).orElseThrow(() -> new EntityNotFoundException("해당 JWT의 member가 없습니다. userId: " + userId)); - if (!member.isDeleted()) { - if (checkDuplicatedLogin(userId, accessToken, response)) return; // 중복로그인 시 예외처리 - processValidJwt(member); - chain.doFilter(request, response); - - } else { // 탈퇴한 회원일 때 - sendJsonResponse(response, ExceptionCode.INVALID_DELETED_MEMBER); + try { + Member member = memberRepository.findByUserId(userId).orElseThrow(() -> new EntityNotFoundException("해당 JWT의 member가 없습니다. userId: " + userId)); + if (!member.isDeleted()) { + if (checkDuplicatedLogin(userId, accessToken, response)) return; // 중복로그인 시 예외 처리 + saveMemberInSecurityContextHolder(member); + chain.doFilter(request, response); + } else { + sendJsonResponse(response, ExceptionCode.INVALID_DELETED_MEMBER); // 탈퇴한 회원 예외 처리 + } + } catch (EntityNotFoundException ex) { + sendJsonResponse(response, ExceptionCode.INVALID_USER_ID); // 해당 userId의 회원이 없을 때 예외 처리 } - } else { // 해당 토큰에 담긴 userId를 가진 사용자가 없을 때 - sendJsonResponse(response, ExceptionCode.INVALID_TOKEN); + } else { + sendJsonResponse(response, ExceptionCode.INVALID_TOKEN); // 복호화 했지만 userId 정보가 없는 잘못된 토큰 예외 처리 } } catch (TokenExpiredException ex) { - // 만료된 토큰 처리 - sendJsonResponse(response, ExceptionCode.INVALID_EXPIRED_TOKEN); + //refreshToken 확인 + String refreshToken = getRefreshToken(accessToken); + if (refreshToken != null) { + // accessToken 재발급 + String userId = JWT.require(Algorithm.HMAC512(jwtConfig.getKey())).build().verify(refreshToken).getClaim("userId").asString(); + String newAccessToken = createAccessToken(userId, accessToken,refreshToken,response); + if (newAccessToken != null) { + System.out.println("accessToken 재발급"); + response.setHeader("newAccessToken", newAccessToken); + chain.doFilter(request, response); + } + } else { + // redis에 refreshToken없으면 만료된 토큰 처리 + sendJsonResponse(response, ExceptionCode.INVALID_EXPIRED_TOKEN); + } } catch (JWTDecodeException ex) { // JWT 디코딩 예외 처리 sendJsonResponse(response, ExceptionCode.INVALID_TOKEN); @@ -88,12 +108,56 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } private boolean checkDuplicatedLogin(String userId, String token, HttpServletResponse response) throws IOException { - String tokenInRedis = redisAuthenticatedUserTemplate.opsForValue().get(userId); - if (tokenInRedis != null && !tokenInRedis.equals(token)) { // redis에 저장된 토큰과 다른 값이면 - sendJsonResponse(response, ExceptionCode.INVALID_DUPLICATED_AUTHENTICATION); // 중복로그인 토큰 만료 처리 + try { + String tokenInRedis = redisAuthenticatedUserTemplate.opsForValue().get(userId); + if (tokenInRedis != null && !tokenInRedis.equals(token)) { // redis에 저장된 토큰과 다른 값이면 + sendJsonResponse(response, ExceptionCode.INVALID_DUPLICATED_AUTHENTICATION); // 중복로그인 토큰 만료 처리 + return true; + } + return false; + } catch (RedisConnectionFailureException ex) { + sendJsonResponse(response, ExceptionCode.UNCONNECTED_REDIS); return true; } - return false; + } + + private String getRefreshToken(String accessToken) { + String refreshToken = redisAuthenticatedUserTemplate.opsForValue().get(accessToken); + return refreshToken; + } + + private String createAccessToken(String userId, String accessToken, String refreshToken,HttpServletResponse response) throws IOException { + int second = Integer.parseInt(jwtConfig.getSecond()); + int minute = Integer.parseInt(jwtConfig.getMinute()); + int hour = Integer.parseInt(jwtConfig.getHour()); + try { + Member member = memberRepository.findByUserId(userId).orElseThrow(() -> new EntityNotFoundException("해당 JWT의 member가 없습니다. userId: " + userId)); + saveMemberInSecurityContextHolder(member); + + String newAccessToken = JWT.create() + .withSubject("mokumokuAccess") + .withExpiresAt(new Date(System.currentTimeMillis() + (second * minute))) + .withClaim("userId", userId) + .withClaim("role", member.getRole()) + .sign(Algorithm.HMAC512(jwtConfig.getKey())); + + // 기존 user 및 jwt 데이터 삭제 + redisAuthenticatedUserTemplate.delete(userId); + redisAuthenticatedUserTemplate.delete(accessToken); + + // redis에 newAccessToken : refreshToken 저장 + redisAuthenticatedUserTemplate.opsForValue().set(userId, newAccessToken); + redisAuthenticatedUserTemplate.opsForValue().set(newAccessToken, refreshToken); + redisAuthenticatedUserTemplate.expire(newAccessToken, second * minute * hour, TimeUnit.MILLISECONDS); + + return newAccessToken; + } catch (EntityNotFoundException ex) { + sendJsonResponse(response, ExceptionCode.INVALID_USER_ID); + return null; + } catch (RedisConnectionFailureException ex) { + sendJsonResponse(response, ExceptionCode.UNCONNECTED_REDIS); + return null; + } } private void sendJsonResponse(HttpServletResponse response, ExceptionCode exceptionCode) throws IOException { @@ -104,7 +168,7 @@ private void sendJsonResponse(HttpServletResponse response, ExceptionCode except new ObjectMapper().writeValue(response.getOutputStream(), errorResponse); } - private void processValidJwt(Member member) { + private void saveMemberInSecurityContextHolder(Member member) { Map userAttributes = createNewAttribute(member); DefaultOAuth2User oAuth2User = new DefaultOAuth2User( Collections.singleton(new SimpleGrantedAuthority(member.getRole())), diff --git a/src/main/java/com/anywayclear/config/oauth/CustumOAuth2UserService.java b/src/main/java/com/anywayclear/config/oauth/CustumOAuth2UserService.java index 57d43f6..28934da 100644 --- a/src/main/java/com/anywayclear/config/oauth/CustumOAuth2UserService.java +++ b/src/main/java/com/anywayclear/config/oauth/CustumOAuth2UserService.java @@ -35,7 +35,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic Map userAttributes = createNewAttribute(member); - // Spring Security의 세션에 OAuth2User객체 저장됨 + // Spring Security의 세션에 OAuth2User객체 저장 return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(member.getRole())), userAttributes, "id"); } @@ -62,7 +62,7 @@ private Member getMember(Optional memberOptional) { if (member.isDeleted()) { member.setDeleted(false); memberRepository.save(member); - System.out.println("CustumOAuth2UserService : 재가입합니다"); + System.out.println("CustumOAuth2UserService : 재가입합니다"); // 탈퇴회원이 다시 소셜 로그인하면 재가입 } else { System.out.println("CustumOAuth2UserService : 이미 회원입니다"); } diff --git a/src/main/java/com/anywayclear/config/oauth/OAuth2AuthenticationSuccessHandler.java b/src/main/java/com/anywayclear/config/oauth/OAuth2AuthenticationSuccessHandler.java index 51707dc..3d71153 100644 --- a/src/main/java/com/anywayclear/config/oauth/OAuth2AuthenticationSuccessHandler.java +++ b/src/main/java/com/anywayclear/config/oauth/OAuth2AuthenticationSuccessHandler.java @@ -5,6 +5,7 @@ import com.auth0.jwt.algorithms.Algorithm; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.springframework.data.redis.RedisConnectionFailureException; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.user.OAuth2User; @@ -28,9 +29,9 @@ public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { - System.out.println("OAuth2AuthenticationSuccessHandler : 로그인 성공"); + // 이미 응답이 커밋됐는데 response하면 예외 발생하므로 return if (response.isCommitted()) { log.debug("Response has already been committed"); return; @@ -39,21 +40,50 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); String userId = (String) oAuth2User.getAttributes().get("userId"); String role = (String) oAuth2User.getAttributes().get("role"); - int expirationTime = setExpirationTime(); - System.out.println("expirationTime = " + expirationTime); - - String accessToken = createToken(userId, role, expirationTime); - setTokenInRedis(userId, accessToken, expirationTime); - System.out.println("accessToken = " + accessToken); + int[] expirationTimes = setExpirationTime(); + int accessExpirationTime = expirationTimes[0]; + int refreshExpirationTime = expirationTimes[1]; + + // token 생성 + String[] tokens = createToken(userId, role, accessExpirationTime, refreshExpirationTime); + String accessToken = tokens[0]; + String refreshToken = tokens[1]; + if (!setTokenInRedis(userId, accessToken, accessExpirationTime, refreshToken, refreshExpirationTime, request, response)) { + return; + } String redirectUrl = createRedirectUrl(accessToken, userId); + // token 발급 및 리턴 getRedirectStrategy().sendRedirect(request,response,redirectUrl); } - private void setTokenInRedis(String userId, String token, int expirationTime) { - redisAuthenticatedUserTemplate.opsForValue().set(userId, token); - redisAuthenticatedUserTemplate.expire(userId, expirationTime, TimeUnit.MILLISECONDS); + private boolean setTokenInRedis(String userId, String accessToken, int accessExpirationTime, String refreshToken, int refreshExpirationTime, HttpServletRequest request, HttpServletResponse response) throws IOException { + + try { + redisAuthenticatedUserTemplate.opsForValue().set(userId, accessToken); + redisAuthenticatedUserTemplate.expire(userId, accessExpirationTime, TimeUnit.MILLISECONDS); + + redisAuthenticatedUserTemplate.opsForValue().set(accessToken, refreshToken); + redisAuthenticatedUserTemplate.expire(accessToken, refreshExpirationTime, TimeUnit.MILLISECONDS); + return true; + } catch (RedisConnectionFailureException ex) { + sendRedisConnectionFailureException(request, response, ex); + return false; + } + } + + private void sendRedisConnectionFailureException(HttpServletRequest request,HttpServletResponse response,RedisConnectionFailureException ex) throws IOException { + String redirectUrl = jwtConfig.getFail(); + + String errorMessage = ex.getLocalizedMessage(); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + redirectUrl = UriComponentsBuilder.fromUriString(redirectUrl) + .queryParam("error", errorMessage) + .build().toUriString(); + + getRedirectStrategy().sendRedirect(request, response, redirectUrl); } private String createRedirectUrl(String accessToken, String userId) { @@ -65,21 +95,29 @@ private String createRedirectUrl(String accessToken, String userId) { return redirectUrl; } - private int setExpirationTime() { + private int[] setExpirationTime() { int second = Integer.parseInt(jwtConfig.getSecond()); int minute = Integer.parseInt(jwtConfig.getMinute()); int hour = Integer.parseInt(jwtConfig.getHour()); - return 1000 * second * minute * hour; + return new int[] {1000 * second * minute, 1000 * second * minute * hour}; } - private String createToken(String userId, String role, int expirationTime) { + private String[] createToken(String userId, String role, int accessExpirationTime, int refreshExpirationTime) { String accessToken = JWT.create() - .withSubject("mokumoku") - .withExpiresAt(new Date(System.currentTimeMillis() + (expirationTime))) + .withSubject("mokumokuAccess") + .withExpiresAt(new Date(System.currentTimeMillis() + (accessExpirationTime))) .withClaim("userId", userId) .withClaim("role", role) .sign(Algorithm.HMAC512(jwtConfig.getKey())); - return accessToken; + + String refreshToken = JWT.create() + .withSubject("mokumokuRefresh") + .withExpiresAt(new Date(System.currentTimeMillis() + (refreshExpirationTime))) + .withClaim("userId", userId) + .withClaim("role", role) + .sign(Algorithm.HMAC512(jwtConfig.getKey())); + + return new String[] {accessToken, refreshToken}; } } diff --git a/src/main/java/com/anywayclear/controller/ProduceController.java b/src/main/java/com/anywayclear/controller/ProduceController.java index e5f07b4..12bf081 100644 --- a/src/main/java/com/anywayclear/controller/ProduceController.java +++ b/src/main/java/com/anywayclear/controller/ProduceController.java @@ -13,6 +13,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.web.bind.annotation.*; @@ -36,8 +37,7 @@ public ProduceController(ProduceService produceService, AuctionService auctionSe } @PostMapping -// @PreAuthorize("hasRole('ROLE_CONSUMER')") -// @Secured({"ROLE_CONSUMER", "ROLE_SELLER"}) + @PreAuthorize("hasRole('ROLE_SELLER')") public ResponseEntity createProduce(@AuthenticationPrincipal OAuth2User oAuth2User, @Valid @RequestBody ProduceCreateRequest request) { String sellerId = (String) oAuth2User.getAttributes().get("userId"); final Long id = produceService.createProduce(request, sellerId); @@ -45,7 +45,6 @@ public ResponseEntity createProduce(@AuthenticationPrincipal OAuth2User oA } @GetMapping("/{id}") -// @Secured({"ROLE_CONSUMER", "ROLE_SELLER"}) public ResponseEntity getProduce(@Positive @PathVariable("id") long id) { produceService.updateProduceStatus(); return ResponseEntity.ok(produceService.getProduce(id)); diff --git a/src/main/java/com/anywayclear/controller/ReviewController.java b/src/main/java/com/anywayclear/controller/ReviewController.java index 231c299..98c936e 100644 --- a/src/main/java/com/anywayclear/controller/ReviewController.java +++ b/src/main/java/com/anywayclear/controller/ReviewController.java @@ -20,6 +20,16 @@ public ReviewController(ReviewService reviewService) { this.reviewService = reviewService; } + @PostMapping("{dealId}") + public ResponseEntity createReview( + @RequestBody ReviewRequest request, + @PathVariable("dealId") Long dealId, + @AuthenticationPrincipal OAuth2User oAuth2User + ) { + String userId = (String) oAuth2User.getAttributes().get("userId"); + return ResponseEntity.ok(reviewService.createReview(userId, dealId, request)); + } + @GetMapping("/{dealId}") public ResponseEntity getReview( @PathVariable("dealId") Long dealId) { diff --git a/src/main/java/com/anywayclear/exception/ExceptionCode.java b/src/main/java/com/anywayclear/exception/ExceptionCode.java index f3210f9..ff15524 100644 --- a/src/main/java/com/anywayclear/exception/ExceptionCode.java +++ b/src/main/java/com/anywayclear/exception/ExceptionCode.java @@ -36,6 +36,7 @@ public enum ExceptionCode { // 404 Not Found : 요청한 URI에 대한 리소스 없음 INVALID_RESOURCE(NOT_FOUND, "요청한 리소스가 없습니다", 404), + INVALID_USER_ID(NOT_FOUND, "존재하지 않는 사용자 입니다", 404), INVALID_REVIEW(NOT_FOUND, "존재하지 않는 리뷰입니다", 404), INVALID_DEAL(NOT_FOUND, "존재하지 않는 거래입니다", 404), @@ -46,6 +47,7 @@ public enum ExceptionCode { DUPLICATE_RESOURCE(CONFLICT, "", 409), // 500 INTERNAL_SERVER_ERROR : 서버 에러 + UNCONNECTED_REDIS(INTERNAL_SERVER_ERROR, "Redis 연결 실패했습니다.", 500), SERVER_ERROR(INTERNAL_SERVER_ERROR,"", 500); private final HttpStatus httpStatus;