diff --git a/src/main/java/com/numberone/backend/LoginTestController.java b/src/main/java/com/numberone/backend/LoginTestController.java index 863d4d30..49e09554 100644 --- a/src/main/java/com/numberone/backend/LoginTestController.java +++ b/src/main/java/com/numberone/backend/LoginTestController.java @@ -1,14 +1,25 @@ package com.numberone.backend; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "login_test", description = "토큰 인증 테스트 관련 API") @RestController public class LoginTestController { - @RequestMapping("/logintest") - public String test(Authentication authentication){ - return "Hello "+authentication.getName(); + @Operation(summary = "토큰 인증 테스트하기", description = + """ + 서버에서 발급받은 액세스 토큰을 "Bearer [발급받은 액세스토큰]" 형태로 http 헤더의 Authorization에 넣어서 요청해주세요. + + 유효한 토큰이라면 사용자의 이메일을 반환합니다. + + 앞으로 서버에 api 요청을 날릴때 이렇게 Authorization 헤더에 발급받은 액세스 토큰을 담아 전달해주세요. + """) + @GetMapping("/api/logintest") + public String test(Authentication authentication) { + return "Hello " + authentication.getName(); } } diff --git a/src/main/java/com/numberone/backend/config/SecurityConfig.java b/src/main/java/com/numberone/backend/config/SecurityConfig.java index 4f285f06..a5c0401d 100644 --- a/src/main/java/com/numberone/backend/config/SecurityConfig.java +++ b/src/main/java/com/numberone/backend/config/SecurityConfig.java @@ -1,37 +1,22 @@ package com.numberone.backend.config; -import com.numberone.backend.config.auth.JwtFilter; -import com.numberone.backend.properties.JwtProperties; -import lombok.RequiredArgsConstructor; import com.numberone.backend.config.auth.JwtFilter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; -import org.springframework.security.config.Customizer; 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.configuration.WebSecurityCustomizer; -import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtEncoder; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint; import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; -import org.springframework.web.servlet.handler.HandlerMappingIntrospector; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; - @Slf4j @Configuration @EnableWebSecurity @@ -67,8 +52,7 @@ public WebSecurityCustomizer webSecurityCustomizer() { "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", - "/token/**", - "/shelters/**"); + "/token/**"); //.requestMatchers("/**"); // 인증 처리 하지 않을 케이스 } } diff --git a/src/main/java/com/numberone/backend/config/SwaggerConfig.java b/src/main/java/com/numberone/backend/config/SwaggerConfig.java index 07184edd..86d6d2b2 100644 --- a/src/main/java/com/numberone/backend/config/SwaggerConfig.java +++ b/src/main/java/com/numberone/backend/config/SwaggerConfig.java @@ -4,7 +4,12 @@ import io.swagger.v3.oas.annotations.info.Contact; import io.swagger.v3.oas.annotations.info.Info; import io.swagger.v3.oas.annotations.servers.Server; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; @OpenAPIDefinition( servers = @Server(url = "/", description = "${host.url}"), @@ -22,4 +27,18 @@ ) @Configuration public class SwaggerConfig { + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .components(new Components().addSecuritySchemes("JWT", bearerAuth())); + } + + public SecurityScheme bearerAuth() { + return new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .in(SecurityScheme.In.HEADER) + .name(HttpHeaders.AUTHORIZATION); + } } diff --git a/src/main/java/com/numberone/backend/config/auth/JwtFilter.java b/src/main/java/com/numberone/backend/config/auth/JwtFilter.java index 51c98674..4b8445af 100644 --- a/src/main/java/com/numberone/backend/config/auth/JwtFilter.java +++ b/src/main/java/com/numberone/backend/config/auth/JwtFilter.java @@ -1,14 +1,19 @@ package com.numberone.backend.config.auth; +import com.fasterxml.jackson.databind.ObjectMapper; import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.domain.member.service.MemberService; import com.numberone.backend.domain.token.util.JwtUtil; +import com.numberone.backend.exception.context.ExceptionContext; +import com.numberone.backend.exception.dto.ErrorResponse; +import com.numberone.backend.exception.notfound.NotFoundMemberException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @@ -18,37 +23,66 @@ import java.io.IOException; import java.util.Collections; +import static com.numberone.backend.exception.context.CustomExceptionContext.*; + @RequiredArgsConstructor @Component public class JwtFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final MemberService memberService; - //토큰이 유효하지 않다면 setAuthentication이 진행되지 않아 UsernamePasswordAuthenticationFilter에서 인증이 되지 않음 + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String path = request.getRequestURI(); + return !path.startsWith("/api/"); + // /api로 시작하는 경로에 대해서만 jwt 인증을 진행합니다. 이렇게 안하면 security에서 무시한 경로라도 모든 경로에 대해서 이 필터를 거치네요.. + // jwt인증이 필요한 api에 대해서는 /api/apple 처럼 /api로 시작하게 만들어야 될것같아요! + } + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) { - filterChain.doFilter(request, response); + setErrorResponse(response, WRONG_ACCESS_TOKEN); return; } - String token = authorizationHeader.split(" ")[1]; + if (!jwtUtil.isValid(token)) { + setErrorResponse(response, WRONG_ACCESS_TOKEN); + return; + } if (jwtUtil.isExpired(token)) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.getWriter().write("Token has expired"); - filterChain.doFilter(request, response); + setErrorResponse(response, EXPIRED_ACCESS_TOKEN); return; } String email = jwtUtil.getEmail(token); - Member member = memberService.findByEmail(email); + try { + Member member = memberService.findByEmail(email); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + member.getEmail(), null, Collections.emptyList()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( - member.getEmail(), null, Collections.emptyList()); - authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + filterChain.doFilter(request, response); + } catch (NotFoundMemberException e) { + setErrorResponse(response, NOT_FOUND_MEMBER); + return; + } + } - SecurityContextHolder.getContext().setAuthentication(authenticationToken); - filterChain.doFilter(request, response); + private void setErrorResponse( + HttpServletResponse response, + ExceptionContext context + ) { + ObjectMapper objectMapper = new ObjectMapper(); + response.setStatus(HttpStatus.FORBIDDEN.value()); + response.setContentType("application/json;charset=UTF-8"); + ErrorResponse errorResponse = new ErrorResponse(context.getCode(), context.getMessage()); + try { + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } catch (IOException e) { + e.printStackTrace(); + } } } diff --git a/src/main/java/com/numberone/backend/domain/member/service/MemberService.java b/src/main/java/com/numberone/backend/domain/member/service/MemberService.java index 67851a0d..9a36d8d6 100644 --- a/src/main/java/com/numberone/backend/domain/member/service/MemberService.java +++ b/src/main/java/com/numberone/backend/domain/member/service/MemberService.java @@ -18,6 +18,7 @@ public Member findByEmail(String email) { .orElseThrow(NotFoundMemberException::new); } + @Transactional public void create(String email) { memberRepository.save(Member.of(email)); } diff --git a/src/main/java/com/numberone/backend/domain/shelter/controller/ShelterController.java b/src/main/java/com/numberone/backend/domain/shelter/controller/ShelterController.java index e61590be..59fd4535 100644 --- a/src/main/java/com/numberone/backend/domain/shelter/controller/ShelterController.java +++ b/src/main/java/com/numberone/backend/domain/shelter/controller/ShelterController.java @@ -18,7 +18,7 @@ @Tag(name = "shelters", description = "대피소 관련 API") @Slf4j @RequiredArgsConstructor -@RequestMapping("/shelters") +@RequestMapping("/api/shelters") @RestController public class ShelterController { private final ShelterService shelterService; @@ -32,6 +32,8 @@ public class ShelterController { distance 는 미터(m) 단위이며 1500 m 이내 대피소만 검색합니다. 검색 결과가 0 개인 경우, NotFound 예외를 터뜨립니다. + + access token 을 헤더에 담아서 요청해주세요. """) @PostMapping @@ -54,6 +56,7 @@ public ResponseEntity getNearestShelter( 검색 결과가 0 개인 경우, NotFound 예외를 터뜨립니다. + access token 을 헤더에 담아서 요청해주세요. """) @PostMapping("/list") public ResponseEntity getNearbyShelterList( diff --git a/src/main/java/com/numberone/backend/domain/token/controller/TokenController.java b/src/main/java/com/numberone/backend/domain/token/controller/TokenController.java index bbbc11e8..d9eeb6d7 100644 --- a/src/main/java/com/numberone/backend/domain/token/controller/TokenController.java +++ b/src/main/java/com/numberone/backend/domain/token/controller/TokenController.java @@ -1,10 +1,13 @@ package com.numberone.backend.domain.token.controller; -import com.numberone.backend.domain.token.dto.request.TokenRequest; -import com.numberone.backend.domain.token.dto.response.TokenResponse; +import com.numberone.backend.domain.token.dto.request.GetTokenRequest; +import com.numberone.backend.domain.token.dto.request.RefreshTokenRequest; +import com.numberone.backend.domain.token.dto.response.GetTokenResponse; +import com.numberone.backend.domain.token.dto.response.RefreshTokenResponse; import com.numberone.backend.domain.token.service.TokenService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -27,7 +30,7 @@ public class TokenController { 이후 서버에 API 요청시 이 JWT 토큰을 같이 담아서 요청해야 정상적으로 API가 호출 됩니다. """) @PostMapping("/kakao") - public TokenResponse loginKakao(@RequestBody TokenRequest tokenRequest) { + public GetTokenResponse loginKakao(@RequestBody @Valid GetTokenRequest tokenRequest) { return tokenService.loginKakao(tokenRequest); } @@ -40,7 +43,7 @@ public TokenResponse loginKakao(@RequestBody TokenRequest tokenRequest) { 이후 서버에 API 요청시 이 JWT 토큰을 같이 담아서 요청해야 정상적으로 API가 호출 됩니다. """) @PostMapping("/naver") - public TokenResponse loginNaver(@RequestBody TokenRequest tokenRequest) { + public GetTokenResponse loginNaver(@RequestBody @Valid GetTokenRequest tokenRequest) { return tokenService.loginNaver(tokenRequest); } @@ -51,7 +54,7 @@ public TokenResponse loginNaver(@RequestBody TokenRequest tokenRequest) { 새로 사용할 수 있는 JWT 토큰이 발급됩니다. """) @PostMapping("/refresh") - public TokenResponse refresh(@RequestBody TokenRequest tokenRequest) { + public RefreshTokenResponse refresh(@RequestBody @Valid RefreshTokenRequest tokenRequest) { return tokenService.refresh(tokenRequest); } } diff --git a/src/main/java/com/numberone/backend/domain/token/dto/request/GetTokenRequest.java b/src/main/java/com/numberone/backend/domain/token/dto/request/GetTokenRequest.java new file mode 100644 index 00000000..4f3e07f0 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/token/dto/request/GetTokenRequest.java @@ -0,0 +1,14 @@ +package com.numberone.backend.domain.token.dto.request; + +import jakarta.validation.constraints.NotNull; +import lombok.*; +import org.hibernate.annotations.Comment; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class GetTokenRequest { + @NotNull(message = "토큰이 NULL 입니다.") + @Comment("카카오 또는 네이버에서 발급된 Access 토큰") + private String token; +} diff --git a/src/main/java/com/numberone/backend/domain/token/dto/request/RefreshTokenRequest.java b/src/main/java/com/numberone/backend/domain/token/dto/request/RefreshTokenRequest.java new file mode 100644 index 00000000..db30d8e9 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/token/dto/request/RefreshTokenRequest.java @@ -0,0 +1,15 @@ +package com.numberone.backend.domain.token.dto.request; + +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RefreshTokenRequest { + @NotNull(message = "토큰이 NULL 입니다.") + @Comment("서버에서 발급받은 Refresh 토큰") + private String token; +} diff --git a/src/main/java/com/numberone/backend/domain/token/dto/request/TokenRequest.java b/src/main/java/com/numberone/backend/domain/token/dto/request/TokenRequest.java deleted file mode 100644 index a700cc6d..00000000 --- a/src/main/java/com/numberone/backend/domain/token/dto/request/TokenRequest.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.numberone.backend.domain.token.dto.request; - -import lombok.*; - -@ToString -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class TokenRequest { - private String token; -} diff --git a/src/main/java/com/numberone/backend/domain/token/dto/response/GetTokenResponse.java b/src/main/java/com/numberone/backend/domain/token/dto/response/GetTokenResponse.java new file mode 100644 index 00000000..9820040b --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/token/dto/response/GetTokenResponse.java @@ -0,0 +1,26 @@ +package com.numberone.backend.domain.token.dto.response; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class GetTokenResponse { + private String accessToken; + private String refreshToken; + + @Builder + public GetTokenResponse(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } + + public static GetTokenResponse of(String accessToken, String refreshToken) { + return GetTokenResponse.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/main/java/com/numberone/backend/domain/token/dto/response/KakaoInfoResponse.java b/src/main/java/com/numberone/backend/domain/token/dto/response/KakaoInfoResponse.java index ddf88a50..694fb425 100644 --- a/src/main/java/com/numberone/backend/domain/token/dto/response/KakaoInfoResponse.java +++ b/src/main/java/com/numberone/backend/domain/token/dto/response/KakaoInfoResponse.java @@ -1,5 +1,6 @@ package com.numberone.backend.domain.token.dto.response; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -8,6 +9,7 @@ @ToString @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@JsonIgnoreProperties(ignoreUnknown = true) public class KakaoInfoResponse { private Long id; private String connected_at; @@ -17,6 +19,7 @@ public class KakaoInfoResponse { @ToString @Getter @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) public class Properties { private String nickname; private String profile_image; @@ -26,6 +29,7 @@ public class Properties { @ToString @Getter @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) public class KakaoAccount { static class profile { private String nickname; diff --git a/src/main/java/com/numberone/backend/domain/token/dto/response/NaverInfoResponse.java b/src/main/java/com/numberone/backend/domain/token/dto/response/NaverInfoResponse.java index 4cceb11b..c91273e2 100644 --- a/src/main/java/com/numberone/backend/domain/token/dto/response/NaverInfoResponse.java +++ b/src/main/java/com/numberone/backend/domain/token/dto/response/NaverInfoResponse.java @@ -1,5 +1,6 @@ package com.numberone.backend.domain.token.dto.response; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -8,6 +9,7 @@ @ToString @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@JsonIgnoreProperties(ignoreUnknown = true) public class NaverInfoResponse { private String resultcode; private String message; @@ -16,6 +18,7 @@ public class NaverInfoResponse { @ToString @Getter @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) public class Response { private String id; private String nickname; diff --git a/src/main/java/com/numberone/backend/domain/token/dto/response/RefreshTokenResponse.java b/src/main/java/com/numberone/backend/domain/token/dto/response/RefreshTokenResponse.java new file mode 100644 index 00000000..fc3b7d09 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/token/dto/response/RefreshTokenResponse.java @@ -0,0 +1,23 @@ +package com.numberone.backend.domain.token.dto.response; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class RefreshTokenResponse { + private String accessToken; + + @Builder + public RefreshTokenResponse(String accessToken) { + this.accessToken = accessToken; + } + + public static RefreshTokenResponse of(String accessToken) { + return RefreshTokenResponse.builder() + .accessToken(accessToken) + .build(); + } +} diff --git a/src/main/java/com/numberone/backend/domain/token/dto/response/TokenResponse.java b/src/main/java/com/numberone/backend/domain/token/dto/response/TokenResponse.java deleted file mode 100644 index c942186b..00000000 --- a/src/main/java/com/numberone/backend/domain/token/dto/response/TokenResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.numberone.backend.domain.token.dto.response; - -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class TokenResponse { - private String token; - - @Builder - public TokenResponse(String token) { - this.token = token; - } - - public static TokenResponse of(String token){ - return TokenResponse.builder() - .token(token) - .build(); - } -} diff --git a/src/main/java/com/numberone/backend/domain/token/entity/Token.java b/src/main/java/com/numberone/backend/domain/token/entity/Token.java index c65cacf5..7baa053b 100644 --- a/src/main/java/com/numberone/backend/domain/token/entity/Token.java +++ b/src/main/java/com/numberone/backend/domain/token/entity/Token.java @@ -17,10 +17,10 @@ public class Token implements Serializable { private String email; @Indexed//특정 컬럼을 인덱스(보조 id)로 지정하여 효율적인 검색이 가능하게 함 - private String accessToken; - private String refreshToken; + private String accessToken; + @Builder public Token(String email, String accessToken, String refreshToken) { this.email = email; diff --git a/src/main/java/com/numberone/backend/domain/token/repository/TokenRepository.java b/src/main/java/com/numberone/backend/domain/token/repository/TokenRepository.java index a8d45c98..ae624042 100644 --- a/src/main/java/com/numberone/backend/domain/token/repository/TokenRepository.java +++ b/src/main/java/com/numberone/backend/domain/token/repository/TokenRepository.java @@ -6,5 +6,5 @@ import java.util.Optional; public interface TokenRepository extends CrudRepository { - Optional findByAccessToken(String accessToken); + Optional findByRefreshToken(String refreshToken); } diff --git a/src/main/java/com/numberone/backend/domain/token/service/TokenService.java b/src/main/java/com/numberone/backend/domain/token/service/TokenService.java index 4b7e87a6..f80e2ed8 100644 --- a/src/main/java/com/numberone/backend/domain/token/service/TokenService.java +++ b/src/main/java/com/numberone/backend/domain/token/service/TokenService.java @@ -2,11 +2,14 @@ import com.numberone.backend.domain.member.repository.MemberRepository; import com.numberone.backend.domain.member.service.MemberService; -import com.numberone.backend.domain.token.dto.request.TokenRequest; +import com.numberone.backend.domain.token.dto.request.GetTokenRequest; +import com.numberone.backend.domain.token.dto.request.RefreshTokenRequest; import com.numberone.backend.domain.token.dto.response.*; import com.numberone.backend.domain.token.entity.Token; import com.numberone.backend.domain.token.repository.TokenRepository; -import com.numberone.backend.exception.badrequest.BadRequestTokenException; +import com.numberone.backend.exception.badrequest.BadRequestSocialTokenException; +import com.numberone.backend.exception.forbidden.WrongAccessTokenException; +import com.numberone.backend.exception.notfound.NotFoundRefreshTokenException; import com.numberone.backend.properties.KakaoProperties; import com.numberone.backend.properties.NaverProperties; import com.numberone.backend.domain.token.util.JwtUtil; @@ -27,50 +30,65 @@ public class TokenService { private final MemberService memberService; private final TokenRepository tokenRepository; private final MemberRepository memberRepository; + private long refreshPeroid = 1000L * 60 * 60 * 24 * 14;//14일 + private long accessPeroid = 1000L * 60 * 30;//30분 - public TokenResponse loginKakao(TokenRequest tokenRequest) { + @Transactional + public GetTokenResponse loginKakao(GetTokenRequest tokenRequest) { HttpHeaders headers = new HttpHeaders(); headers.add("Content-type", "application/x-www-form-urlencoded; charset=utf-8"); headers.add("Authorization", "Bearer " + tokenRequest.getToken()); - ResponseEntity response = restTemplate.exchange(kakaoProperties.getUser_api_url(), HttpMethod.GET, new HttpEntity<>(null, headers), KakaoInfoResponse.class); - String email = response.getBody().getKakao_account().getEmail(); - return TokenResponse.of(getAccessToken(email)); + try { + ResponseEntity response = restTemplate.exchange(kakaoProperties.getUser_api_url(), HttpMethod.GET, new HttpEntity<>(null, headers), KakaoInfoResponse.class); + String email = response.getBody().getKakao_account().getEmail(); + return getTokenResponse(email); + } catch (Exception e) { + throw new BadRequestSocialTokenException(); + } } - public TokenResponse loginNaver(TokenRequest tokenRequest) { + @Transactional + public GetTokenResponse loginNaver(GetTokenRequest tokenRequest) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.add("Authorization", "Bearer " + tokenRequest.getToken()); - ResponseEntity response = restTemplate.exchange(naverProperties.getUser_api_url(), HttpMethod.GET, new HttpEntity<>(null, headers), NaverInfoResponse.class); - String email = response.getBody().getResponse().getEmail(); - return TokenResponse.of(getAccessToken(email)); + try { + ResponseEntity response = restTemplate.exchange(naverProperties.getUser_api_url(), HttpMethod.GET, new HttpEntity<>(null, headers), NaverInfoResponse.class); + String email = response.getBody().getResponse().getEmail(); + return getTokenResponse(email); + } catch (Exception e) { + throw new BadRequestSocialTokenException(); + } } @Transactional - public TokenResponse refresh(TokenRequest tokenRequest) { + public RefreshTokenResponse refresh(RefreshTokenRequest tokenRequest) { + if(!jwtUtil.isValid(tokenRequest.getToken())) + throw new NotFoundRefreshTokenException(); + Token token = tokenRepository.findByRefreshToken(tokenRequest.getToken()) + .orElseThrow(NotFoundRefreshTokenException::new); String email = jwtUtil.getEmail(tokenRequest.getToken()); - Token token = tokenRepository.findById(email) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 토큰")); - String newToken = jwtUtil.createToken(email, 1000L * 60 * 60 * 24 * 14); + String newToken = jwtUtil.createToken(email, accessPeroid); token.updateAccessToken(newToken); tokenRepository.save(token);//redis의 경우 jpa와 달리 transactional을 이용해도 데이터 수정시에 명시적으로 save를 해줘야 함 - return TokenResponse.of(newToken); + return RefreshTokenResponse.of(newToken); } - private String getAccessToken(String email) { + @Transactional + private GetTokenResponse getTokenResponse(String email) { if (!memberRepository.existsByEmail(email)) memberService.create(email); if (tokenRepository.existsById(email)) { Token token = tokenRepository.findById(email) - .orElseThrow(BadRequestTokenException::new); - return token.getAccessToken(); + .orElseThrow(WrongAccessTokenException::new); + return GetTokenResponse.of(token.getAccessToken(), token.getRefreshToken()); } else { - String refreshToken = jwtUtil.createToken(email, 1000L * 60 * 60 * 24 * 14);//14일 - String accessToken = jwtUtil.createToken(email, 1000L * 60 * 30);//30분 - tokenRepository.save(Token.of(email, accessToken, refreshToken)); - return accessToken; + String refreshToken = jwtUtil.createToken(email, refreshPeroid); + String accessToken = jwtUtil.createToken(email, accessPeroid); + Token token = tokenRepository.save(Token.of(email, accessToken, refreshToken)); + return GetTokenResponse.of(token.getAccessToken(), token.getRefreshToken()); } } } diff --git a/src/main/java/com/numberone/backend/domain/token/util/JwtUtil.java b/src/main/java/com/numberone/backend/domain/token/util/JwtUtil.java index 61c1fa59..b2c8fb5e 100644 --- a/src/main/java/com/numberone/backend/domain/token/util/JwtUtil.java +++ b/src/main/java/com/numberone/backend/domain/token/util/JwtUtil.java @@ -2,10 +2,7 @@ import com.numberone.backend.domain.token.service.TokenService; import com.numberone.backend.properties.JwtProperties; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jws; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.*; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -31,12 +28,29 @@ public String createToken(String email, long period) { .compact(); } - public String getEmail(String token) { - return getClaims(token).getSubject(); + + public boolean isValid(String token) { + try { + getClaims(token); + return true; + } catch (ExpiredJwtException e) { + return true; + } catch (Exception e) { + return false; + } } public boolean isExpired(String token) { - return getClaims(token).getExpiration().before(new Date()); + try { + Jws claims = Jwts.parser().setSigningKey(jwtProperties.getSecret().getBytes()).parseClaimsJws(token); + return getClaims(token).getExpiration().before(new Date()); + } catch (ExpiredJwtException e) { + return true; + } + } + + public String getEmail(String token) { + return getClaims(token).getSubject(); } private Claims getClaims(String token) { diff --git a/src/main/java/com/numberone/backend/exception/badrequest/BadRequestSocialTokenException.java b/src/main/java/com/numberone/backend/exception/badrequest/BadRequestSocialTokenException.java new file mode 100644 index 00000000..a78ebfe3 --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/badrequest/BadRequestSocialTokenException.java @@ -0,0 +1,11 @@ +package com.numberone.backend.exception.badrequest; + +import com.numberone.backend.exception.context.ExceptionContext; + +import static com.numberone.backend.exception.context.CustomExceptionContext.BAD_REQUEST_SOCIAL_TOKEN; + +public class BadRequestSocialTokenException extends BadRequestException{ + public BadRequestSocialTokenException() { + super(BAD_REQUEST_SOCIAL_TOKEN); + } +} diff --git a/src/main/java/com/numberone/backend/exception/badrequest/BadRequestTokenException.java b/src/main/java/com/numberone/backend/exception/badrequest/BadRequestTokenException.java deleted file mode 100644 index b3ebb219..00000000 --- a/src/main/java/com/numberone/backend/exception/badrequest/BadRequestTokenException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.numberone.backend.exception.badrequest; - -import com.numberone.backend.exception.context.ExceptionContext; - -import static com.numberone.backend.exception.context.CustomExceptionContext.BAD_REQUEST_TOKEN; - -public class BadRequestTokenException extends BadRequestException{ - public BadRequestTokenException() { - super(BAD_REQUEST_TOKEN); - } -} diff --git a/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java b/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java index 337a3b00..a42686ee 100644 --- a/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java +++ b/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java @@ -9,26 +9,28 @@ public enum CustomExceptionContext implements ExceptionContext { // MEMBER 관련 예외 NOT_FOUND_MEMBER("존재하지 않는 회원을 조회할 수 없습니다.", 1000), - BAD_REQUEST_TOKEN("존재하지 않는 토큰을 요청하였습니다.", 2000), + // TOKEN 관련 예외 + WRONG_ACCESS_TOKEN("존재하지 않는 액세스 토큰입니다.", 2001), + EXPIRED_ACCESS_TOKEN("만료된 액세스 토큰입니다. 리프레쉬 토큰을 이용하여 갱신해주세요.", 2002), + WRONG_REFRESH_TOKEN("존재하지 않거나 만료된 리프레쉬 토큰입니다. 다시 리프레쉬 토큰을 발급받아주세요.", 2003), + BAD_REQUEST_SOCIAL_TOKEN("요청하신 네이버 또는 카카오 소셜 토큰이 유효하지 않습니다.", 2004), // SHELTER 관련 예외 - NOT_FOUND_SHELTER("주변에 가까운 대피소가 존재하지 않습니다.", 3000) - ; - - - - /** 2023. 10. 02. versatile0010 - - 이 곳에서 각 예외 별 메세지와 예외 코드를 관리합니다. - - 예를 들어, - // MEMBER - NOT_FOUND_MEMBER("존재하지 않는 회원을 조회할 수 없습니다.", 2000), - DUPLICATED_MEMBER_NAME("회원의 이름은 중복될 수 없습니다, 2001), - // STATION - NOT_FOUND_STATION("존재하지 않는 지하철역을 조회할 수 없습니다.", 3000); - 과 같이 작성할 수 있습니다. - + NOT_FOUND_SHELTER("주변에 가까운 대피소가 존재하지 않습니다.", 3000); + + + /** + * 2023. 10. 02. versatile0010 + *

+ * 이 곳에서 각 예외 별 메세지와 예외 코드를 관리합니다. + *

+ * 예를 들어, + * // MEMBER + * NOT_FOUND_MEMBER("존재하지 않는 회원을 조회할 수 없습니다.", 2000), + * DUPLICATED_MEMBER_NAME("회원의 이름은 중복될 수 없습니다, 2001), + * // STATION + * NOT_FOUND_STATION("존재하지 않는 지하철역을 조회할 수 없습니다.", 3000); + * 과 같이 작성할 수 있습니다. */ private final String message; private final int code; diff --git a/src/main/java/com/numberone/backend/exception/dto/ErrorResponse.java b/src/main/java/com/numberone/backend/exception/dto/ErrorResponse.java index 78282e49..973375a3 100644 --- a/src/main/java/com/numberone/backend/exception/dto/ErrorResponse.java +++ b/src/main/java/com/numberone/backend/exception/dto/ErrorResponse.java @@ -1,6 +1,10 @@ package com.numberone.backend.exception.dto; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -17,6 +21,8 @@ public class ErrorResponse { @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) private LocalDateTime time; public ErrorResponse(int code, String message) { diff --git a/src/main/java/com/numberone/backend/exception/forbidden/ExpiredAccessTokenException.java b/src/main/java/com/numberone/backend/exception/forbidden/ExpiredAccessTokenException.java new file mode 100644 index 00000000..ad7b8ee3 --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/forbidden/ExpiredAccessTokenException.java @@ -0,0 +1,11 @@ +package com.numberone.backend.exception.forbidden; + +import com.numberone.backend.exception.context.ExceptionContext; + +import static com.numberone.backend.exception.context.CustomExceptionContext.EXPIRED_ACCESS_TOKEN; + +public class ExpiredAccessTokenException extends ForbiddenException{ + public ExpiredAccessTokenException() { + super(EXPIRED_ACCESS_TOKEN); + } +} diff --git a/src/main/java/com/numberone/backend/exception/forbidden/ForbiddenException.java b/src/main/java/com/numberone/backend/exception/forbidden/ForbiddenException.java new file mode 100644 index 00000000..f168673f --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/forbidden/ForbiddenException.java @@ -0,0 +1,11 @@ +package com.numberone.backend.exception.forbidden; + +import com.numberone.backend.exception.NumberOneException; +import com.numberone.backend.exception.context.ExceptionContext; +import org.springframework.http.HttpStatus; + +public class ForbiddenException extends NumberOneException { + public ForbiddenException(ExceptionContext context) { + super(HttpStatus.FORBIDDEN,context.getMessage(),context.getCode()); + } +} diff --git a/src/main/java/com/numberone/backend/exception/forbidden/WrongAccessTokenException.java b/src/main/java/com/numberone/backend/exception/forbidden/WrongAccessTokenException.java new file mode 100644 index 00000000..d72b162e --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/forbidden/WrongAccessTokenException.java @@ -0,0 +1,9 @@ +package com.numberone.backend.exception.forbidden; + +import static com.numberone.backend.exception.context.CustomExceptionContext.WRONG_ACCESS_TOKEN; + +public class WrongAccessTokenException extends ForbiddenException { + public WrongAccessTokenException() { + super(WRONG_ACCESS_TOKEN); + } +} diff --git a/src/main/java/com/numberone/backend/exception/notfound/NotFoundRefreshTokenException.java b/src/main/java/com/numberone/backend/exception/notfound/NotFoundRefreshTokenException.java new file mode 100644 index 00000000..85958b7e --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/notfound/NotFoundRefreshTokenException.java @@ -0,0 +1,11 @@ +package com.numberone.backend.exception.notfound; + +import com.numberone.backend.exception.forbidden.ForbiddenException; + +import static com.numberone.backend.exception.context.CustomExceptionContext.WRONG_REFRESH_TOKEN; + +public class NotFoundRefreshTokenException extends NotFoundException { + public NotFoundRefreshTokenException() { + super(WRONG_REFRESH_TOKEN); + } +}