From 61a282e2d98e61ce4b43f612051297f5ed626917 Mon Sep 17 00:00:00 2001 From: Parkjyun Date: Thu, 30 May 2024 04:06:34 +0900 Subject: [PATCH 1/3] [feat] add spring security and jwt of sixth seminar --- practice/build.gradle | 22 ++++- .../auth/JwtAuthenticationFilter.java | 52 ++++++++++ .../sopt/practice/auth/PrincipalHandler.java | 26 +++++ .../sopt/practice/auth/SecurityConfig.java | 47 +++++++++ .../practice/auth/UserAuthentication.java | 17 ++++ .../filter/CustomAccessDeniedHandler.java | 23 +++++ .../CustomJwtAuthenticationEntryPoint.java | 38 ++++++++ .../practice/auth/redis/domain/Token.java | 28 ++++++ .../redis/repository/TokenRepository.java | 11 +++ .../{dto => }/GlobalExceptionHandler.java | 10 +- .../practice/common/dto/ErrorMessage.java | 13 ++- .../practice/common/jwt/JwtTokenProvider.java | 95 +++++++++++++++++++ .../common/jwt/JwtValidationType.java | 10 ++ .../practice/controller/BlogController.java | 10 +- .../practice/controller/MemberController.java | 9 +- .../exception/UnauthorizedException.java | 9 ++ .../sopt/practice/service/MemberService.java | 12 ++- .../dto/response/MemberJoinResponse.java | 14 +++ 18 files changed, 432 insertions(+), 14 deletions(-) create mode 100644 practice/src/main/java/org/sopt/practice/auth/JwtAuthenticationFilter.java create mode 100644 practice/src/main/java/org/sopt/practice/auth/PrincipalHandler.java create mode 100644 practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java create mode 100644 practice/src/main/java/org/sopt/practice/auth/UserAuthentication.java create mode 100644 practice/src/main/java/org/sopt/practice/auth/filter/CustomAccessDeniedHandler.java create mode 100644 practice/src/main/java/org/sopt/practice/auth/filter/CustomJwtAuthenticationEntryPoint.java create mode 100644 practice/src/main/java/org/sopt/practice/auth/redis/domain/Token.java create mode 100644 practice/src/main/java/org/sopt/practice/auth/redis/repository/TokenRepository.java rename practice/src/main/java/org/sopt/practice/common/{dto => }/GlobalExceptionHandler.java (75%) create mode 100644 practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java create mode 100644 practice/src/main/java/org/sopt/practice/common/jwt/JwtValidationType.java create mode 100644 practice/src/main/java/org/sopt/practice/exception/UnauthorizedException.java create mode 100644 practice/src/main/java/org/sopt/practice/service/dto/response/MemberJoinResponse.java diff --git a/practice/build.gradle b/practice/build.gradle index 4a068c6..e794775 100644 --- a/practice/build.gradle +++ b/practice/build.gradle @@ -22,14 +22,34 @@ repositories { } dependencies { + //Web implementation 'org.springframework.boot:spring-boot-starter-web' + + //Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + + //Test testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.rest-assured:rest-assured' + + //Database implementation 'org.postgresql:postgresql' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + //Validation implementation 'org.springframework.boot:spring-boot-starter-validation' - testImplementation 'io.rest-assured:rest-assured' + + //Jwt + implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' + implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' + implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' + + //Security + implementation 'org.springframework.boot:spring-boot-starter-security' + + //Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { diff --git a/practice/src/main/java/org/sopt/practice/auth/JwtAuthenticationFilter.java b/practice/src/main/java/org/sopt/practice/auth/JwtAuthenticationFilter.java new file mode 100644 index 0000000..9124ed2 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/auth/JwtAuthenticationFilter.java @@ -0,0 +1,52 @@ +package org.sopt.practice.auth; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.sopt.practice.common.dto.ErrorMessage; +import org.sopt.practice.common.jwt.JwtTokenProvider; +import org.sopt.practice.exception.UnauthorizedException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static org.sopt.practice.common.jwt.JwtValidationType.VALID_JWT; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + + + final String token = getJwtFromRequest(request); + if (jwtTokenProvider.validateToken(token) == VALID_JWT) {//유효하다면 authentication 등록 + Long memberId = jwtTokenProvider.getUserFromJwt(token); + UserAuthentication authentication = UserAuthentication.createUserAuthentication(memberId); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + } + + private String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring("Bearer ".length()); + } + return null; + }} diff --git a/practice/src/main/java/org/sopt/practice/auth/PrincipalHandler.java b/practice/src/main/java/org/sopt/practice/auth/PrincipalHandler.java new file mode 100644 index 0000000..bb4bf7d --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/auth/PrincipalHandler.java @@ -0,0 +1,26 @@ +package org.sopt.practice.auth; + +import org.sopt.practice.common.dto.ErrorMessage; +import org.sopt.practice.exception.UnauthorizedException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +public class PrincipalHandler { + + private static final String ANONYMOUS_USER = "anonymousUser"; + + public Long getUserIdFromPrincipal() { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + isPrincipalNull(principal); + return Long.valueOf(principal.toString()); + } + + public void isPrincipalNull( + final Object principal + ) { + if (principal.toString().equals(ANONYMOUS_USER)) { + throw new UnauthorizedException(ErrorMessage.JWT_UNAUTHORIZED); + } + } +} \ No newline at end of file diff --git a/practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java b/practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java new file mode 100644 index 0000000..20c8fba --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java @@ -0,0 +1,47 @@ +package org.sopt.practice.auth; + +import lombok.RequiredArgsConstructor; +import org.sopt.practice.auth.filter.CustomAccessDeniedHandler; +import org.sopt.practice.auth.filter.CustomJwtAuthenticationEntryPoint; +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.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer; + +@Configuration +@RequiredArgsConstructor +@EnableWebSecurity //web Security를 사용할 수 있게 +public class SecurityConfig { + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + + + private static final String[] AUTH_WHITE_LIST = {"/api/v1/members"}; + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .requestCache(RequestCacheConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .exceptionHandling(exception -> + { + exception.authenticationEntryPoint(customJwtAuthenticationEntryPoint); + exception.accessDeniedHandler(customAccessDeniedHandler); + }); + + + http.authorizeHttpRequests(auth -> { + auth.requestMatchers(AUTH_WHITE_LIST).permitAll(); + auth.anyRequest().authenticated(); + }) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} \ No newline at end of file diff --git a/practice/src/main/java/org/sopt/practice/auth/UserAuthentication.java b/practice/src/main/java/org/sopt/practice/auth/UserAuthentication.java new file mode 100644 index 0000000..38656f3 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/auth/UserAuthentication.java @@ -0,0 +1,17 @@ +package org.sopt.practice.auth; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class UserAuthentication extends UsernamePasswordAuthenticationToken { + + public UserAuthentication(Object principal, Object credentials, Collection authorities) { + super(principal, credentials, authorities); + } + + public static UserAuthentication createUserAuthentication(Long userId) { + return new UserAuthentication(userId, null, null); + } +} diff --git a/practice/src/main/java/org/sopt/practice/auth/filter/CustomAccessDeniedHandler.java b/practice/src/main/java/org/sopt/practice/auth/filter/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..2fbaaba --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/auth/filter/CustomAccessDeniedHandler.java @@ -0,0 +1,23 @@ +package org.sopt.practice.auth.filter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + setResponse(response); + } + + private void setResponse(HttpServletResponse response) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } +} + diff --git a/practice/src/main/java/org/sopt/practice/auth/filter/CustomJwtAuthenticationEntryPoint.java b/practice/src/main/java/org/sopt/practice/auth/filter/CustomJwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..49ac00e --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/auth/filter/CustomJwtAuthenticationEntryPoint.java @@ -0,0 +1,38 @@ +package org.sopt.practice.auth.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.sopt.practice.common.dto.ErrorMessage; +import org.sopt.practice.common.dto.ErrorResponse; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +@Slf4j +public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + log.error("Unauthorized error: {}", authException.getMessage()); + setResponse(response); + } + + private void setResponse(HttpServletResponse response) throws IOException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + response.getWriter() + .write(objectMapper.writeValueAsString( + ErrorResponse.of(ErrorMessage.JWT_UNAUTHORIZED))); + } +} \ No newline at end of file diff --git a/practice/src/main/java/org/sopt/practice/auth/redis/domain/Token.java b/practice/src/main/java/org/sopt/practice/auth/redis/domain/Token.java new file mode 100644 index 0000000..d3f2554 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/auth/redis/domain/Token.java @@ -0,0 +1,28 @@ +package org.sopt.practice.auth.redis.domain; + +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +@RedisHash(value = "", timeToLive = 24 * 60 * 60 * 1000L * 1) +@AllArgsConstructor +@Getter +@Builder +public class Token { + + @Id + private Long id; + + @Indexed//해당 어노테이션 사용시 이 값으로 객ㄱ체 값을 찾을 수 있음 + private String refreshToken; + + public static Token of(final Long id, final String refreshToken) { + return Token.builder() + .id(id) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/practice/src/main/java/org/sopt/practice/auth/redis/repository/TokenRepository.java b/practice/src/main/java/org/sopt/practice/auth/redis/repository/TokenRepository.java new file mode 100644 index 0000000..4d3c23b --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/auth/redis/repository/TokenRepository.java @@ -0,0 +1,11 @@ +package org.sopt.practice.auth.redis.repository; + +import org.sopt.practice.auth.redis.domain.Token; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface TokenRepository extends CrudRepository { + Optional findByRefreshToken(final String refreshToken); + Optional findById(final Long id); +} diff --git a/practice/src/main/java/org/sopt/practice/common/dto/GlobalExceptionHandler.java b/practice/src/main/java/org/sopt/practice/common/GlobalExceptionHandler.java similarity index 75% rename from practice/src/main/java/org/sopt/practice/common/dto/GlobalExceptionHandler.java rename to practice/src/main/java/org/sopt/practice/common/GlobalExceptionHandler.java index 68555fd..0d87645 100644 --- a/practice/src/main/java/org/sopt/practice/common/dto/GlobalExceptionHandler.java +++ b/practice/src/main/java/org/sopt/practice/common/GlobalExceptionHandler.java @@ -1,7 +1,10 @@ -package org.sopt.practice.common.dto; +package org.sopt.practice.common; +import lombok.extern.slf4j.Slf4j; +import org.sopt.practice.common.dto.ErrorResponse; import org.sopt.practice.exception.ForbiddenException; import org.sopt.practice.exception.NotFoundException; +import org.sopt.practice.exception.UnauthorizedException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -13,6 +16,11 @@ @RestControllerAdvice public class GlobalExceptionHandler { + @ExceptionHandler(UnauthorizedException.class) + public ResponseEntity handleUnauthorizedException(final UnauthorizedException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse.of(e.getErrorMessage())); + } + @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.of(HttpStatus.BAD_REQUEST.value(), Objects.requireNonNull(e.getBindingResult().getFieldError().getDefaultMessage()))); diff --git a/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java b/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java index 16c766d..a03ec91 100644 --- a/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java +++ b/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java @@ -3,20 +3,25 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @Getter public enum ErrorMessage { + /** + * 401 UNAUTHORIZED + * */ + JWT_UNAUTHORIZED(HttpStatus.UNAUTHORIZED.value(), "사용자의 로그인 검증을 실패하였습니다."), /** * 403 FORBIDDEN * */ - MEMBER_FORBIDDEN(403, "회원의 권한이 없습니다."), + MEMBER_FORBIDDEN(HttpStatus.FORBIDDEN.value(), "회원의 권한이 없습니다."), /** * 404 BAD_REQUEST * */ - MEMBER_NOT_FOUND(404, "해당 회원을 찾을 수 없습니다"), - BLOG_NOT_FOUND(404, "해당하는 블로그를 찾을 수 없습니다"), - POST_NOT_FOUND(404, "해당하는 포스트를 찾을 수 없습니다"); + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 회원을 찾을 수 없습니다"), + BLOG_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당하는 블로그를 찾을 수 없습니다"), + POST_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당하는 포스트를 찾을 수 없습니다"); private final int status; private final String message; diff --git a/practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java b/practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..f1b94c1 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java @@ -0,0 +1,95 @@ +package org.sopt.practice.common.jwt; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Base64; +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class JwtTokenProvider implements InitializingBean { + + private static final String USER_ID = "userId"; + + private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14; + private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 1; + + @Value("${jwt.secret}") + private String JWT_SECRET; + + private Key key; + + @Override + public void afterPropertiesSet() throws Exception {//스프링빈의 모든 속성이 설정된 후에 실행될 초기화 로직 + byte[] keyBytes = Decoders.BASE64.decode(JWT_SECRET); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + public String issueAccessToken(final Authentication authentication) { + return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME); + } + + public String issueRefreshToken(final Authentication authentication) { + return generateToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME); + } + + + public String generateToken(Authentication authentication, Long tokenExpirationTime) { + final Date now = new Date(); + final Claims claims = Jwts.claims() + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + tokenExpirationTime)); // 만료 시간 + + claims.put(USER_ID, authentication.getPrincipal()); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // Header + .setClaims(claims) // Claim + .signWith(key, SignatureAlgorithm.HS512) // Signature + .compact(); + } + + private SecretKey getSigningKey() { + String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); + return Keys.hmacShaKeyFor(encodedKey.getBytes()); + } + + public JwtValidationType validateToken(String token) { + try { + final Claims claims = getBody(token); + return JwtValidationType.VALID_JWT; + } catch (MalformedJwtException ex) { + return JwtValidationType.INVALID_JWT_TOKEN; + } catch (ExpiredJwtException ex) { + return JwtValidationType.EXPIRED_JWT_TOKEN; + } catch (UnsupportedJwtException ex) { + return JwtValidationType.UNSUPPORTED_JWT_TOKEN; + } catch (IllegalArgumentException ex) { + return JwtValidationType.EMPTY_JWT; + } + } + + private Claims getBody(final String token) { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public Long getUserFromJwt(String token) { + Claims claims = getBody(token); + return Long.valueOf(claims.get(USER_ID).toString()); + } +} diff --git a/practice/src/main/java/org/sopt/practice/common/jwt/JwtValidationType.java b/practice/src/main/java/org/sopt/practice/common/jwt/JwtValidationType.java new file mode 100644 index 0000000..6e7c2cf --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/common/jwt/JwtValidationType.java @@ -0,0 +1,10 @@ +package org.sopt.practice.common.jwt; + +public enum JwtValidationType { + VALID_JWT, // 유효한 JWT + INVALID_JWT_SIGNATURE, // 유효하지 않은 서명 + INVALID_JWT_TOKEN, // 유효하지 않은 토큰 + EXPIRED_JWT_TOKEN, // 만료된 토큰 + UNSUPPORTED_JWT_TOKEN, // 지원하지 않는 형식의 토큰 + EMPTY_JWT // 빈 JWT +} \ No newline at end of file diff --git a/practice/src/main/java/org/sopt/practice/controller/BlogController.java b/practice/src/main/java/org/sopt/practice/controller/BlogController.java index ed4204c..aad2c29 100644 --- a/practice/src/main/java/org/sopt/practice/controller/BlogController.java +++ b/practice/src/main/java/org/sopt/practice/controller/BlogController.java @@ -2,6 +2,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.sopt.practice.auth.PrincipalHandler; import org.sopt.practice.common.dto.SuccessMessage; import org.sopt.practice.common.dto.SuccessStatusResponse; import org.sopt.practice.controller.headers.Headers; @@ -19,6 +20,7 @@ public class BlogController { private final BlogService blogService; + private final PrincipalHandler principalHandler; @GetMapping("/blogs/{blogId}") ResponseEntity> getBlog(@PathVariable final Long blogId) { @@ -26,9 +28,11 @@ ResponseEntity> getBlog(@PathVariable final } @PostMapping("/blogs") - ResponseEntity createBlog(@RequestBody final BlogCreateRequest request, - @RequestHeader(name = Headers.MEMBER_ID) Long memberId) { - return ResponseEntity.status(HttpStatus.CREATED).header("Location", blogService.create(memberId, request)) + ResponseEntity createBlog(@RequestBody final BlogCreateRequest request) { + Long id = principalHandler.getUserIdFromPrincipal(); + System.out.println(id); + return ResponseEntity.status(HttpStatus.CREATED) + .header("Location", blogService.create(id, request)) .body(SuccessStatusResponse.of(SuccessMessage.BLOG_CREATE_SUCCESS)); } diff --git a/practice/src/main/java/org/sopt/practice/controller/MemberController.java b/practice/src/main/java/org/sopt/practice/controller/MemberController.java index 3166c1c..9f34f40 100644 --- a/practice/src/main/java/org/sopt/practice/controller/MemberController.java +++ b/practice/src/main/java/org/sopt/practice/controller/MemberController.java @@ -4,6 +4,8 @@ import org.sopt.practice.service.MemberService; import org.sopt.practice.service.dto.MemberCreateDto; import org.sopt.practice.service.dto.MemberFindDto; +import org.sopt.practice.service.dto.response.MemberJoinResponse; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -18,8 +20,11 @@ public class MemberController { private final MemberService memberService; @PostMapping("/members") - public ResponseEntity createMember(@RequestBody MemberCreateDto memberCreate) { - return ResponseEntity.created(URI.create(memberService.createMember(memberCreate))).build(); + public ResponseEntity createMember(@RequestBody MemberCreateDto memberCreate) { + MemberJoinResponse memberJoinResponse = memberService.createMember(memberCreate); + return ResponseEntity.status(HttpStatus.CREATED) + .header("Location", memberJoinResponse.memberId()) + .body(memberJoinResponse); } @GetMapping("/members/{memberId}") diff --git a/practice/src/main/java/org/sopt/practice/exception/UnauthorizedException.java b/practice/src/main/java/org/sopt/practice/exception/UnauthorizedException.java new file mode 100644 index 0000000..a7a7d7f --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/exception/UnauthorizedException.java @@ -0,0 +1,9 @@ +package org.sopt.practice.exception; + +import org.sopt.practice.common.dto.ErrorMessage; + +public class UnauthorizedException extends BusinessException { + public UnauthorizedException(ErrorMessage errorMessage) { + super(errorMessage); + } +} diff --git a/practice/src/main/java/org/sopt/practice/service/MemberService.java b/practice/src/main/java/org/sopt/practice/service/MemberService.java index 8c90f4b..9980c05 100644 --- a/practice/src/main/java/org/sopt/practice/service/MemberService.java +++ b/practice/src/main/java/org/sopt/practice/service/MemberService.java @@ -2,12 +2,15 @@ import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; +import org.sopt.practice.auth.UserAuthentication; import org.sopt.practice.common.dto.ErrorMessage; +import org.sopt.practice.common.jwt.JwtTokenProvider; import org.sopt.practice.entity.Member; import org.sopt.practice.exception.NotFoundException; import org.sopt.practice.repository.MemberRepository; import org.sopt.practice.service.dto.MemberCreateDto; import org.sopt.practice.service.dto.MemberFindDto; +import org.sopt.practice.service.dto.response.MemberJoinResponse; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,11 +23,14 @@ public class MemberService { private final MemberRepository memberRepository; - + private final JwtTokenProvider jwtTokenProvider; @Transactional - public String createMember(MemberCreateDto memberCreate) { + public MemberJoinResponse createMember(MemberCreateDto memberCreate) { Member member = memberRepository.save(Member.create(memberCreate.name(), memberCreate.part(), memberCreate.age())); - return member.getId().toString(); + Long memberId = member.getId(); + + String accessToken = jwtTokenProvider.issueAccessToken(UserAuthentication.createUserAuthentication(memberId)); + return MemberJoinResponse.of(accessToken, memberId.toString()); } public Member findById(Long memberId) { diff --git a/practice/src/main/java/org/sopt/practice/service/dto/response/MemberJoinResponse.java b/practice/src/main/java/org/sopt/practice/service/dto/response/MemberJoinResponse.java new file mode 100644 index 0000000..ba25d53 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/service/dto/response/MemberJoinResponse.java @@ -0,0 +1,14 @@ +package org.sopt.practice.service.dto.response; + +public record MemberJoinResponse( + String accessToken, + String memberId +) { + + public static MemberJoinResponse of( + String accessToken, + String userId + ) { + return new MemberJoinResponse(accessToken, userId); + } +} From ecd2350dd701fc1a824b87a680e20266993e9407 Mon Sep 17 00:00:00 2001 From: Parkjyun Date: Fri, 31 May 2024 10:05:20 +0900 Subject: [PATCH 2/3] [feat] add sixth seminar assignment --- .../auth/{ => config}/SecurityConfig.java | 9 +- .../{ => filter}/JwtAuthenticationFilter.java | 26 +++-- .../CustomAccessDeniedHandler.java | 2 +- .../CustomJwtAuthenticationEntryPoint.java | 25 +++-- .../auth/redis/RefreshTokenService.java | 35 +++++++ .../auth/redis/config/RedisConfig.java | 32 ++++++ .../practice/auth/redis/domain/Token.java | 6 +- .../redis/repository/TokenRepository.java | 1 + .../practice/common/dto/ErrorMessage.java | 12 ++- .../common/jwt/JwtValidationType.java | 10 -- .../practice/controller/AuthController.java | 32 ++++++ .../practice/controller/BlogController.java | 5 +- .../practice/controller/MemberController.java | 14 +-- .../practice/controller/headers/Headers.java | 1 + .../java/org/sopt/practice/entity/Blog.java | 2 +- .../java/org/sopt/practice/entity/Member.java | 14 +-- .../java/org/sopt/practice/entity/Part.java | 19 ++-- .../exception/BadRequestException.java | 9 ++ .../practice/repository/MemberRepository.java | 5 + .../sopt/practice/service/AuthService.java | 99 +++++++++++++++++++ .../sopt/practice/service/BlogService.java | 5 +- .../sopt/practice/service/MemberHelper.java | 33 +++++++ .../sopt/practice/service/MemberService.java | 15 +-- .../dto/{ => request}/BlogCreateRequest.java | 2 +- .../{ => request}/BlogTitleUpdateRequest.java | 2 +- .../dto/{ => request}/MemberCreateDto.java | 2 +- .../dto/{ => response}/MemberFindDto.java | 2 +- .../dto/response/MemberJoinResponse.java | 14 --- .../service/dto/response/TokenResponse.java | 16 +++ .../jwt/JwtTokenProvider.java | 78 ++++++++------- .../service/jwt/JwtValidationType.java | 6 ++ .../sopt/practice/service/jwt/TokenType.java | 12 +++ .../controller/BlogControllerTest.java | 2 +- .../controller/MemberControllerTest.java | 2 +- 34 files changed, 409 insertions(+), 140 deletions(-) rename practice/src/main/java/org/sopt/practice/auth/{ => config}/SecurityConfig.java (85%) rename practice/src/main/java/org/sopt/practice/auth/{ => filter}/JwtAuthenticationFilter.java (62%) rename practice/src/main/java/org/sopt/practice/auth/{filter => handler}/CustomAccessDeniedHandler.java (95%) rename practice/src/main/java/org/sopt/practice/auth/{filter => handler}/CustomJwtAuthenticationEntryPoint.java (52%) create mode 100644 practice/src/main/java/org/sopt/practice/auth/redis/RefreshTokenService.java create mode 100644 practice/src/main/java/org/sopt/practice/auth/redis/config/RedisConfig.java delete mode 100644 practice/src/main/java/org/sopt/practice/common/jwt/JwtValidationType.java create mode 100644 practice/src/main/java/org/sopt/practice/controller/AuthController.java create mode 100644 practice/src/main/java/org/sopt/practice/exception/BadRequestException.java create mode 100644 practice/src/main/java/org/sopt/practice/service/AuthService.java create mode 100644 practice/src/main/java/org/sopt/practice/service/MemberHelper.java rename practice/src/main/java/org/sopt/practice/service/dto/{ => request}/BlogCreateRequest.java (60%) rename practice/src/main/java/org/sopt/practice/service/dto/{ => request}/BlogTitleUpdateRequest.java (77%) rename practice/src/main/java/org/sopt/practice/service/dto/{ => request}/MemberCreateDto.java (69%) rename practice/src/main/java/org/sopt/practice/service/dto/{ => response}/MemberFindDto.java (86%) delete mode 100644 practice/src/main/java/org/sopt/practice/service/dto/response/MemberJoinResponse.java create mode 100644 practice/src/main/java/org/sopt/practice/service/dto/response/TokenResponse.java rename practice/src/main/java/org/sopt/practice/{common => service}/jwt/JwtTokenProvider.java (60%) create mode 100644 practice/src/main/java/org/sopt/practice/service/jwt/JwtValidationType.java create mode 100644 practice/src/main/java/org/sopt/practice/service/jwt/TokenType.java diff --git a/practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java b/practice/src/main/java/org/sopt/practice/auth/config/SecurityConfig.java similarity index 85% rename from practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java rename to practice/src/main/java/org/sopt/practice/auth/config/SecurityConfig.java index 20c8fba..0ea4573 100644 --- a/practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java +++ b/practice/src/main/java/org/sopt/practice/auth/config/SecurityConfig.java @@ -1,8 +1,9 @@ -package org.sopt.practice.auth; +package org.sopt.practice.auth.config; import lombok.RequiredArgsConstructor; -import org.sopt.practice.auth.filter.CustomAccessDeniedHandler; -import org.sopt.practice.auth.filter.CustomJwtAuthenticationEntryPoint; +import org.sopt.practice.auth.filter.JwtAuthenticationFilter; +import org.sopt.practice.auth.handler.CustomAccessDeniedHandler; +import org.sopt.practice.auth.handler.CustomJwtAuthenticationEntryPoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -21,7 +22,7 @@ public class SecurityConfig { private final CustomAccessDeniedHandler customAccessDeniedHandler; - private static final String[] AUTH_WHITE_LIST = {"/api/v1/members"}; + private static final String[] AUTH_WHITE_LIST = {"/api/v1/login", "/api/v1/reissue"}; @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { diff --git a/practice/src/main/java/org/sopt/practice/auth/JwtAuthenticationFilter.java b/practice/src/main/java/org/sopt/practice/auth/filter/JwtAuthenticationFilter.java similarity index 62% rename from practice/src/main/java/org/sopt/practice/auth/JwtAuthenticationFilter.java rename to practice/src/main/java/org/sopt/practice/auth/filter/JwtAuthenticationFilter.java index 9124ed2..b545fb9 100644 --- a/practice/src/main/java/org/sopt/practice/auth/JwtAuthenticationFilter.java +++ b/practice/src/main/java/org/sopt/practice/auth/filter/JwtAuthenticationFilter.java @@ -1,4 +1,4 @@ -package org.sopt.practice.auth; +package org.sopt.practice.auth.filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -7,9 +7,9 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.sopt.practice.common.dto.ErrorMessage; -import org.sopt.practice.common.jwt.JwtTokenProvider; -import org.sopt.practice.exception.UnauthorizedException; +import org.sopt.practice.auth.UserAuthentication; +import org.sopt.practice.service.jwt.JwtTokenProvider; +import org.sopt.practice.exception.BusinessException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; @@ -18,7 +18,7 @@ import java.io.IOException; -import static org.sopt.practice.common.jwt.JwtValidationType.VALID_JWT; +import static org.sopt.practice.service.jwt.JwtValidationType.VALID_ACCESS; @Component @RequiredArgsConstructor @@ -34,12 +34,18 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, final String token = getJwtFromRequest(request); - if (jwtTokenProvider.validateToken(token) == VALID_JWT) {//유효하다면 authentication 등록 - Long memberId = jwtTokenProvider.getUserFromJwt(token); - UserAuthentication authentication = UserAuthentication.createUserAuthentication(memberId); - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authentication); + + try { + if (jwtTokenProvider.validateToken(token) == VALID_ACCESS) {//유효하다면 authentication 등록 + Long memberId = jwtTokenProvider.getUserFromJwt(token); + UserAuthentication authentication = UserAuthentication.createUserAuthentication(memberId); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (BusinessException e) { + request.setAttribute("errorMessage", e.getErrorMessage()); } + filterChain.doFilter(request, response); } diff --git a/practice/src/main/java/org/sopt/practice/auth/filter/CustomAccessDeniedHandler.java b/practice/src/main/java/org/sopt/practice/auth/handler/CustomAccessDeniedHandler.java similarity index 95% rename from practice/src/main/java/org/sopt/practice/auth/filter/CustomAccessDeniedHandler.java rename to practice/src/main/java/org/sopt/practice/auth/handler/CustomAccessDeniedHandler.java index 2fbaaba..3455135 100644 --- a/practice/src/main/java/org/sopt/practice/auth/filter/CustomAccessDeniedHandler.java +++ b/practice/src/main/java/org/sopt/practice/auth/handler/CustomAccessDeniedHandler.java @@ -1,4 +1,4 @@ -package org.sopt.practice.auth.filter; +package org.sopt.practice.auth.handler; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; diff --git a/practice/src/main/java/org/sopt/practice/auth/filter/CustomJwtAuthenticationEntryPoint.java b/practice/src/main/java/org/sopt/practice/auth/handler/CustomJwtAuthenticationEntryPoint.java similarity index 52% rename from practice/src/main/java/org/sopt/practice/auth/filter/CustomJwtAuthenticationEntryPoint.java rename to practice/src/main/java/org/sopt/practice/auth/handler/CustomJwtAuthenticationEntryPoint.java index 49ac00e..668b10e 100644 --- a/practice/src/main/java/org/sopt/practice/auth/filter/CustomJwtAuthenticationEntryPoint.java +++ b/practice/src/main/java/org/sopt/practice/auth/handler/CustomJwtAuthenticationEntryPoint.java @@ -1,4 +1,4 @@ -package org.sopt.practice.auth.filter; +package org.sopt.practice.auth.handler; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; @@ -7,7 +7,6 @@ import lombok.extern.slf4j.Slf4j; import org.sopt.practice.common.dto.ErrorMessage; import org.sopt.practice.common.dto.ErrorResponse; -import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; @@ -24,15 +23,23 @@ public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoi @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { log.error("Unauthorized error: {}", authException.getMessage()); - setResponse(response); + ErrorMessage errorMessage = (ErrorMessage) request.getAttribute("errorMessage"); + setResponse(response, errorMessage); } - private void setResponse(HttpServletResponse response) throws IOException { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); + private void setResponse(HttpServletResponse response, ErrorMessage errorMessage) throws IOException { + response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); - response.getWriter() - .write(objectMapper.writeValueAsString( - ErrorResponse.of(ErrorMessage.JWT_UNAUTHORIZED))); + response.setStatus(errorMessage.getStatus()); + + response.getWriter().write(objectMapper.writeValueAsString(ErrorResponse.of(errorMessage))); } +// private void setResponse(HttpServletResponse response) throws IOException { +// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); +// response.setContentType(MediaType.APPLICATION_JSON_VALUE); +// response.setCharacterEncoding("UTF-8"); +// response.getWriter() +// .write(objectMapper.writeValueAsString( +// ErrorResponse.of(ErrorMessage.JWT_UNAUTHORIZED))); +// } } \ No newline at end of file diff --git a/practice/src/main/java/org/sopt/practice/auth/redis/RefreshTokenService.java b/practice/src/main/java/org/sopt/practice/auth/redis/RefreshTokenService.java new file mode 100644 index 0000000..91b21f9 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/auth/redis/RefreshTokenService.java @@ -0,0 +1,35 @@ +package org.sopt.practice.auth.redis; + +import lombok.RequiredArgsConstructor; +import org.sopt.practice.auth.redis.domain.Token; +import org.sopt.practice.auth.redis.repository.TokenRepository; +import org.sopt.practice.common.dto.ErrorMessage; +import org.sopt.practice.exception.NotFoundException; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class RefreshTokenService { + + private final RedisTemplate redisTemplate; + private final TokenRepository tokenRepository; + + @Transactional + public void saveRefreshToken(final Long userId, final String refreshToken) { + tokenRepository.save(Token.of(userId, refreshToken)); + } + + public Long findUserIdByRefreshToken(final String refreshToken) { + Token token = tokenRepository.findByRefreshToken(refreshToken) + .orElseThrow(() -> new NotFoundException(ErrorMessage.REFRESHTOKEN_NOT_FOUND)); + return token.getId(); + } + + @Transactional + public void deleteRefreshToken(final Long userId) { + Token token = tokenRepository.findById(userId).orElseThrow(() -> new NotFoundException(ErrorMessage.REFRESHTOKEN_NOT_FOUND)); + tokenRepository.delete(token); + } +} diff --git a/practice/src/main/java/org/sopt/practice/auth/redis/config/RedisConfig.java b/practice/src/main/java/org/sopt/practice/auth/redis/config/RedisConfig.java new file mode 100644 index 0000000..bdfecf3 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/auth/redis/config/RedisConfig.java @@ -0,0 +1,32 @@ +package org.sopt.practice.auth.redis.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } +} diff --git a/practice/src/main/java/org/sopt/practice/auth/redis/domain/Token.java b/practice/src/main/java/org/sopt/practice/auth/redis/domain/Token.java index d3f2554..bcfc13b 100644 --- a/practice/src/main/java/org/sopt/practice/auth/redis/domain/Token.java +++ b/practice/src/main/java/org/sopt/practice/auth/redis/domain/Token.java @@ -1,19 +1,19 @@ package org.sopt.practice.auth.redis.domain; -import jakarta.persistence.Id; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import org.springframework.data.annotation.Id; import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.index.Indexed; -@RedisHash(value = "", timeToLive = 24 * 60 * 60 * 1000L * 1) +@RedisHash(value = "", timeToLive = 24 * 60 * 60 * 1000L * 1)//value = key에 붙을 prefix @AllArgsConstructor @Getter @Builder public class Token { - @Id + @Id//키는 prefix:id로 저장됨 private Long id; @Indexed//해당 어노테이션 사용시 이 값으로 객ㄱ체 값을 찾을 수 있음 diff --git a/practice/src/main/java/org/sopt/practice/auth/redis/repository/TokenRepository.java b/practice/src/main/java/org/sopt/practice/auth/redis/repository/TokenRepository.java index 4d3c23b..302df6f 100644 --- a/practice/src/main/java/org/sopt/practice/auth/redis/repository/TokenRepository.java +++ b/practice/src/main/java/org/sopt/practice/auth/redis/repository/TokenRepository.java @@ -8,4 +8,5 @@ public interface TokenRepository extends CrudRepository { Optional findByRefreshToken(final String refreshToken); Optional findById(final Long id); + } diff --git a/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java b/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java index a03ec91..76344da 100644 --- a/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java +++ b/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java @@ -8,9 +8,18 @@ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @Getter public enum ErrorMessage { + /** + * 400 BAD_REQUEST + * */ + BAD_HEADER_STRUCTURE(HttpStatus.BAD_REQUEST.value(), "헤더의 구조가 잘못되었습니다."), /** * 401 UNAUTHORIZED * */ + INVALID_JWT_SIGNATURE(HttpStatus.UNAUTHORIZED.value(), "유효하지 않은 서명입니다."), + INVALID_JWT_TOKEN(HttpStatus.UNAUTHORIZED.value(), "유효하지 않은 토큰입니다."), + EXPIRED_JWT_TOKEN(HttpStatus.UNAUTHORIZED.value(), "만료된 토큰입니다."), + UNSUPPORTED_JWT_TOKEN(HttpStatus.UNAUTHORIZED.value(), "지원하지 않는 형식의 토큰입니다."), + EMPTY_JWT(HttpStatus.UNAUTHORIZED.value(), "빈 토큰입니다."), JWT_UNAUTHORIZED(HttpStatus.UNAUTHORIZED.value(), "사용자의 로그인 검증을 실패하였습니다."), /** * 403 FORBIDDEN @@ -21,7 +30,8 @@ public enum ErrorMessage { * */ MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 회원을 찾을 수 없습니다"), BLOG_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당하는 블로그를 찾을 수 없습니다"), - POST_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당하는 포스트를 찾을 수 없습니다"); + POST_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당하는 포스트를 찾을 수 없습니다"), + REFRESHTOKEN_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당하는 리프레시 토큰을 찾을 수 없습니다."); private final int status; private final String message; diff --git a/practice/src/main/java/org/sopt/practice/common/jwt/JwtValidationType.java b/practice/src/main/java/org/sopt/practice/common/jwt/JwtValidationType.java deleted file mode 100644 index 6e7c2cf..0000000 --- a/practice/src/main/java/org/sopt/practice/common/jwt/JwtValidationType.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.sopt.practice.common.jwt; - -public enum JwtValidationType { - VALID_JWT, // 유효한 JWT - INVALID_JWT_SIGNATURE, // 유효하지 않은 서명 - INVALID_JWT_TOKEN, // 유효하지 않은 토큰 - EXPIRED_JWT_TOKEN, // 만료된 토큰 - UNSUPPORTED_JWT_TOKEN, // 지원하지 않는 형식의 토큰 - EMPTY_JWT // 빈 JWT -} \ No newline at end of file diff --git a/practice/src/main/java/org/sopt/practice/controller/AuthController.java b/practice/src/main/java/org/sopt/practice/controller/AuthController.java new file mode 100644 index 0000000..02ddf06 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/controller/AuthController.java @@ -0,0 +1,32 @@ +package org.sopt.practice.controller; + +import lombok.RequiredArgsConstructor; +import org.sopt.practice.controller.headers.Headers; +import org.sopt.practice.service.AuthService; +import org.sopt.practice.service.dto.request.MemberCreateDto; +import org.sopt.practice.service.dto.response.TokenResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/login") + public ResponseEntity login(@RequestBody MemberCreateDto memberCreateDto) { + TokenResponse tokenResponse = authService.login(memberCreateDto); + return ResponseEntity.status(HttpStatus.CREATED) + .header("Location", tokenResponse.memberId().toString()) + .body(tokenResponse); + } + + @PostMapping("/reissue") + public ResponseEntity reissue(@RequestHeader(Headers.AUTHOTIZATION_HEADER) String refreshToken) { + return ResponseEntity.ok(authService.reissue(refreshToken)); + } +} + diff --git a/practice/src/main/java/org/sopt/practice/controller/BlogController.java b/practice/src/main/java/org/sopt/practice/controller/BlogController.java index aad2c29..722d7e1 100644 --- a/practice/src/main/java/org/sopt/practice/controller/BlogController.java +++ b/practice/src/main/java/org/sopt/practice/controller/BlogController.java @@ -5,10 +5,9 @@ import org.sopt.practice.auth.PrincipalHandler; import org.sopt.practice.common.dto.SuccessMessage; import org.sopt.practice.common.dto.SuccessStatusResponse; -import org.sopt.practice.controller.headers.Headers; import org.sopt.practice.service.BlogService; -import org.sopt.practice.service.dto.BlogCreateRequest; -import org.sopt.practice.service.dto.BlogTitleUpdateRequest; +import org.sopt.practice.service.dto.request.BlogCreateRequest; +import org.sopt.practice.service.dto.request.BlogTitleUpdateRequest; import org.sopt.practice.service.dto.response.BlogResponse; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/practice/src/main/java/org/sopt/practice/controller/MemberController.java b/practice/src/main/java/org/sopt/practice/controller/MemberController.java index 9f34f40..b8c4245 100644 --- a/practice/src/main/java/org/sopt/practice/controller/MemberController.java +++ b/practice/src/main/java/org/sopt/practice/controller/MemberController.java @@ -2,14 +2,10 @@ import lombok.RequiredArgsConstructor; import org.sopt.practice.service.MemberService; -import org.sopt.practice.service.dto.MemberCreateDto; -import org.sopt.practice.service.dto.MemberFindDto; -import org.sopt.practice.service.dto.response.MemberJoinResponse; -import org.springframework.http.HttpStatus; +import org.sopt.practice.service.dto.response.MemberFindDto; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.net.URI; import java.util.List; @RestController @@ -19,14 +15,6 @@ public class MemberController { private final MemberService memberService; - @PostMapping("/members") - public ResponseEntity createMember(@RequestBody MemberCreateDto memberCreate) { - MemberJoinResponse memberJoinResponse = memberService.createMember(memberCreate); - return ResponseEntity.status(HttpStatus.CREATED) - .header("Location", memberJoinResponse.memberId()) - .body(memberJoinResponse); - } - @GetMapping("/members/{memberId}") public ResponseEntity findMemberById(@PathVariable("memberId") Long memberId) { return ResponseEntity.ok(memberService.findMemberById(memberId)); diff --git a/practice/src/main/java/org/sopt/practice/controller/headers/Headers.java b/practice/src/main/java/org/sopt/practice/controller/headers/Headers.java index 1ac31e9..a006f3f 100644 --- a/practice/src/main/java/org/sopt/practice/controller/headers/Headers.java +++ b/practice/src/main/java/org/sopt/practice/controller/headers/Headers.java @@ -6,4 +6,5 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Headers { public static final String MEMBER_ID = "memberId"; + public static final String AUTHOTIZATION_HEADER = "Authorization"; } diff --git a/practice/src/main/java/org/sopt/practice/entity/Blog.java b/practice/src/main/java/org/sopt/practice/entity/Blog.java index 5e38273..be103d4 100644 --- a/practice/src/main/java/org/sopt/practice/entity/Blog.java +++ b/practice/src/main/java/org/sopt/practice/entity/Blog.java @@ -4,7 +4,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import org.sopt.practice.service.dto.BlogCreateRequest; +import org.sopt.practice.service.dto.request.BlogCreateRequest; @Entity @Getter diff --git a/practice/src/main/java/org/sopt/practice/entity/Member.java b/practice/src/main/java/org/sopt/practice/entity/Member.java index 5c0e29f..a1c455f 100644 --- a/practice/src/main/java/org/sopt/practice/entity/Member.java +++ b/practice/src/main/java/org/sopt/practice/entity/Member.java @@ -19,13 +19,6 @@ public class Member { private int age; - @Builder - private Member(String name, Part part, int age) { - this.name = name; - this.part = part; - this.age = age; - } - public static Member create(String name, Part part, int age) { return Member.builder() .name(name) @@ -34,4 +27,11 @@ public static Member create(String name, Part part, int age) { .build(); } + + @Builder + private Member(String name, Part part, int age) { + this.name = name; + this.part = part; + this.age = age; + } } diff --git a/practice/src/main/java/org/sopt/practice/entity/Part.java b/practice/src/main/java/org/sopt/practice/entity/Part.java index 4a0b328..3457fea 100644 --- a/practice/src/main/java/org/sopt/practice/entity/Part.java +++ b/practice/src/main/java/org/sopt/practice/entity/Part.java @@ -1,10 +1,17 @@ package org.sopt.practice.entity; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor public enum Part { - IOS, - ANDROID, - SERVER, - WEB, - DESIGN, - PLAN; + IOS("IOS"), + ANDROID("ANDROID"), + SERVER("SERVER"), + WEB("WEB"), + DESIGN("DESGIN"), + PLAN("PLAN"); + + private final String value; } diff --git a/practice/src/main/java/org/sopt/practice/exception/BadRequestException.java b/practice/src/main/java/org/sopt/practice/exception/BadRequestException.java new file mode 100644 index 0000000..bc99bae --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/exception/BadRequestException.java @@ -0,0 +1,9 @@ +package org.sopt.practice.exception; + +import org.sopt.practice.common.dto.ErrorMessage; + +public class BadRequestException extends BusinessException { + public BadRequestException(ErrorMessage errorMessage) { + super(errorMessage); + } +} diff --git a/practice/src/main/java/org/sopt/practice/repository/MemberRepository.java b/practice/src/main/java/org/sopt/practice/repository/MemberRepository.java index ecd240b..8079f0a 100644 --- a/practice/src/main/java/org/sopt/practice/repository/MemberRepository.java +++ b/practice/src/main/java/org/sopt/practice/repository/MemberRepository.java @@ -1,8 +1,13 @@ package org.sopt.practice.repository; import org.sopt.practice.entity.Member; +import org.sopt.practice.entity.Part; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface MemberRepository extends JpaRepository { + Boolean existsByNameAndPartAndAge(String name, Part part, int age); + Optional findByNameAndPartAndAge(String name, Part part, int age); } diff --git a/practice/src/main/java/org/sopt/practice/service/AuthService.java b/practice/src/main/java/org/sopt/practice/service/AuthService.java new file mode 100644 index 0000000..6caacf8 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/service/AuthService.java @@ -0,0 +1,99 @@ +package org.sopt.practice.service; + +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import org.sopt.practice.auth.UserAuthentication; +import org.sopt.practice.auth.redis.domain.Token; +import org.sopt.practice.auth.redis.repository.TokenRepository; +import org.sopt.practice.common.dto.ErrorMessage; +import org.sopt.practice.exception.NotFoundException; +import org.sopt.practice.service.jwt.JwtTokenProvider; +import org.sopt.practice.service.jwt.TokenType; +import org.sopt.practice.entity.Member; +import org.sopt.practice.exception.BadRequestException; +import org.sopt.practice.service.dto.request.MemberCreateDto; +import org.sopt.practice.service.dto.response.TokenResponse; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private static final String BEARER_PREFIX = "Bearer "; + + private final MemberHelper memberHelper; + private final JwtTokenProvider jwtTokenProvider; + private final TokenRepository tokenRepository; + + + @Transactional + public TokenResponse login(final MemberCreateDto memberCreate) { + final Member member = loadOrCreateMember(memberCreate); + TokenResponse tokenResponse = createTokenResponse(member.getId()); + updateRefreshToken(member.getId(), tokenResponse.refreshToken()); + return tokenResponse; + } + + @Transactional + public TokenResponse reissue(String token) { + String refreshToken = getToken(token); + Claims claims = jwtTokenProvider.getBody(refreshToken); + if (isAccessToken(claims)) { + throw new BadRequestException(ErrorMessage.INVALID_JWT_TOKEN); + } + Long memberId = claims.get(jwtTokenProvider.USER_ID, Long.class); + + Token savedRefreshToken = tokenRepository.findById(memberId).orElseThrow(() -> new NotFoundException(ErrorMessage.REFRESHTOKEN_NOT_FOUND)); + if (!isSameRefreshToken(refreshToken, savedRefreshToken)) { + throw new BadRequestException(ErrorMessage.INVALID_JWT_TOKEN); + } + + TokenResponse tokenResponse = createTokenResponse(memberId); + updateRefreshToken(memberId, tokenResponse.refreshToken()); + return tokenResponse; + } + + private boolean isSameRefreshToken(final String refreshToken, final Token savedRefreshToken) { + return savedRefreshToken.getRefreshToken().equals(refreshToken); + } + + private boolean isAccessToken(Claims claims) { + return claims.get(jwtTokenProvider.TOKEN_TYPE).equals(TokenType.ACCESS.getValue()); + } + + private TokenResponse createTokenResponse(final Long memberId) { + final String accessToken = jwtTokenProvider.issueAccessToken(UserAuthentication.createUserAuthentication(memberId)); + final String refreshToken = jwtTokenProvider.issueRefreshToken(UserAuthentication.createUserAuthentication(memberId)); + return TokenResponse.of(accessToken, refreshToken, memberId); + } + + private Member loadOrCreateMember(final MemberCreateDto memberCreate) { + + Member member; + + if(!isExistMember(memberCreate)) { + member = memberHelper.saveMember(Member.create(memberCreate.name(), memberCreate.part(), memberCreate.age())); + } else { + member = memberHelper.findByNameAndPartAndAge(memberCreate.name(), memberCreate.part(), memberCreate.age()); + } + return member; + } + + private boolean isExistMember(final MemberCreateDto memberCreateDto) {//이름, 파트, 나이가 같은 회원이 존재하는지 확인 + return memberHelper.isExistMember(memberCreateDto); + } + + private String getToken(String token) { + if (token.startsWith(BEARER_PREFIX)) { + return token.substring(BEARER_PREFIX.length()); + } else { + return token; + } + } + + private void updateRefreshToken(final Long memberId, final String refreshToken) { + tokenRepository.deleteById(memberId); + tokenRepository.save(Token.of(memberId, refreshToken)); + } +} diff --git a/practice/src/main/java/org/sopt/practice/service/BlogService.java b/practice/src/main/java/org/sopt/practice/service/BlogService.java index c4ba4b2..95d14cf 100644 --- a/practice/src/main/java/org/sopt/practice/service/BlogService.java +++ b/practice/src/main/java/org/sopt/practice/service/BlogService.java @@ -2,13 +2,12 @@ import lombok.RequiredArgsConstructor; import org.sopt.practice.common.dto.ErrorMessage; -import org.sopt.practice.common.dto.SuccessStatusResponse; import org.sopt.practice.entity.Blog; import org.sopt.practice.entity.Member; import org.sopt.practice.exception.NotFoundException; import org.sopt.practice.repository.BlogRepository; -import org.sopt.practice.service.dto.BlogCreateRequest; -import org.sopt.practice.service.dto.BlogTitleUpdateRequest; +import org.sopt.practice.service.dto.request.BlogCreateRequest; +import org.sopt.practice.service.dto.request.BlogTitleUpdateRequest; import org.sopt.practice.service.dto.response.BlogResponse; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/practice/src/main/java/org/sopt/practice/service/MemberHelper.java b/practice/src/main/java/org/sopt/practice/service/MemberHelper.java new file mode 100644 index 0000000..a6dc3a6 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/service/MemberHelper.java @@ -0,0 +1,33 @@ +package org.sopt.practice.service; + +import lombok.RequiredArgsConstructor; +import org.sopt.practice.common.dto.ErrorMessage; +import org.sopt.practice.entity.Member; +import org.sopt.practice.entity.Part; +import org.sopt.practice.exception.NotFoundException; +import org.sopt.practice.repository.MemberRepository; +import org.sopt.practice.service.dto.request.MemberCreateDto; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MemberHelper { + + private final MemberRepository memberRepository; + + protected Member saveMember(Member member) { + return memberRepository.save(member); + } + + protected Member findByNameAndPartAndAge(String name, Part part, int age) { + return memberRepository.findByNameAndPartAndAge(name, part, age).orElseThrow(() -> new NotFoundException(ErrorMessage.MEMBER_NOT_FOUND)); + } + + protected boolean isExistMember(MemberCreateDto memberCreateDto) {//이름, 파트, 나이가 같은 회원이 존재하는지 확인 + return memberRepository.existsByNameAndPartAndAge(memberCreateDto.name(), memberCreateDto.part(), memberCreateDto.age()); + } + + public Member findById(Long memberId) { + return memberRepository.findById(memberId).orElseThrow(() -> new NotFoundException(ErrorMessage.MEMBER_NOT_FOUND)); + } +} diff --git a/practice/src/main/java/org/sopt/practice/service/MemberService.java b/practice/src/main/java/org/sopt/practice/service/MemberService.java index 9980c05..a614653 100644 --- a/practice/src/main/java/org/sopt/practice/service/MemberService.java +++ b/practice/src/main/java/org/sopt/practice/service/MemberService.java @@ -2,15 +2,11 @@ import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; -import org.sopt.practice.auth.UserAuthentication; import org.sopt.practice.common.dto.ErrorMessage; -import org.sopt.practice.common.jwt.JwtTokenProvider; import org.sopt.practice.entity.Member; import org.sopt.practice.exception.NotFoundException; import org.sopt.practice.repository.MemberRepository; -import org.sopt.practice.service.dto.MemberCreateDto; -import org.sopt.practice.service.dto.MemberFindDto; -import org.sopt.practice.service.dto.response.MemberJoinResponse; +import org.sopt.practice.service.dto.response.MemberFindDto; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,15 +19,6 @@ public class MemberService { private final MemberRepository memberRepository; - private final JwtTokenProvider jwtTokenProvider; - @Transactional - public MemberJoinResponse createMember(MemberCreateDto memberCreate) { - Member member = memberRepository.save(Member.create(memberCreate.name(), memberCreate.part(), memberCreate.age())); - Long memberId = member.getId(); - - String accessToken = jwtTokenProvider.issueAccessToken(UserAuthentication.createUserAuthentication(memberId)); - return MemberJoinResponse.of(accessToken, memberId.toString()); - } public Member findById(Long memberId) { return memberRepository.findById(memberId) diff --git a/practice/src/main/java/org/sopt/practice/service/dto/BlogCreateRequest.java b/practice/src/main/java/org/sopt/practice/service/dto/request/BlogCreateRequest.java similarity index 60% rename from practice/src/main/java/org/sopt/practice/service/dto/BlogCreateRequest.java rename to practice/src/main/java/org/sopt/practice/service/dto/request/BlogCreateRequest.java index 4ce44cb..4fef02b 100644 --- a/practice/src/main/java/org/sopt/practice/service/dto/BlogCreateRequest.java +++ b/practice/src/main/java/org/sopt/practice/service/dto/request/BlogCreateRequest.java @@ -1,4 +1,4 @@ -package org.sopt.practice.service.dto; +package org.sopt.practice.service.dto.request; public record BlogCreateRequest (String title, String description) { diff --git a/practice/src/main/java/org/sopt/practice/service/dto/BlogTitleUpdateRequest.java b/practice/src/main/java/org/sopt/practice/service/dto/request/BlogTitleUpdateRequest.java similarity index 77% rename from practice/src/main/java/org/sopt/practice/service/dto/BlogTitleUpdateRequest.java rename to practice/src/main/java/org/sopt/practice/service/dto/request/BlogTitleUpdateRequest.java index c25d2e0..f1a6617 100644 --- a/practice/src/main/java/org/sopt/practice/service/dto/BlogTitleUpdateRequest.java +++ b/practice/src/main/java/org/sopt/practice/service/dto/request/BlogTitleUpdateRequest.java @@ -1,4 +1,4 @@ -package org.sopt.practice.service.dto; +package org.sopt.practice.service.dto.request; import jakarta.validation.constraints.Size; diff --git a/practice/src/main/java/org/sopt/practice/service/dto/MemberCreateDto.java b/practice/src/main/java/org/sopt/practice/service/dto/request/MemberCreateDto.java similarity index 69% rename from practice/src/main/java/org/sopt/practice/service/dto/MemberCreateDto.java rename to practice/src/main/java/org/sopt/practice/service/dto/request/MemberCreateDto.java index b3098f0..1868ad0 100644 --- a/practice/src/main/java/org/sopt/practice/service/dto/MemberCreateDto.java +++ b/practice/src/main/java/org/sopt/practice/service/dto/request/MemberCreateDto.java @@ -1,4 +1,4 @@ -package org.sopt.practice.service.dto; +package org.sopt.practice.service.dto.request; import org.sopt.practice.entity.Part; diff --git a/practice/src/main/java/org/sopt/practice/service/dto/MemberFindDto.java b/practice/src/main/java/org/sopt/practice/service/dto/response/MemberFindDto.java similarity index 86% rename from practice/src/main/java/org/sopt/practice/service/dto/MemberFindDto.java rename to practice/src/main/java/org/sopt/practice/service/dto/response/MemberFindDto.java index 4136a52..ebfb7c4 100644 --- a/practice/src/main/java/org/sopt/practice/service/dto/MemberFindDto.java +++ b/practice/src/main/java/org/sopt/practice/service/dto/response/MemberFindDto.java @@ -1,4 +1,4 @@ -package org.sopt.practice.service.dto; +package org.sopt.practice.service.dto.response; import org.sopt.practice.entity.Member; import org.sopt.practice.entity.Part; diff --git a/practice/src/main/java/org/sopt/practice/service/dto/response/MemberJoinResponse.java b/practice/src/main/java/org/sopt/practice/service/dto/response/MemberJoinResponse.java deleted file mode 100644 index ba25d53..0000000 --- a/practice/src/main/java/org/sopt/practice/service/dto/response/MemberJoinResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.sopt.practice.service.dto.response; - -public record MemberJoinResponse( - String accessToken, - String memberId -) { - - public static MemberJoinResponse of( - String accessToken, - String userId - ) { - return new MemberJoinResponse(accessToken, userId); - } -} diff --git a/practice/src/main/java/org/sopt/practice/service/dto/response/TokenResponse.java b/practice/src/main/java/org/sopt/practice/service/dto/response/TokenResponse.java new file mode 100644 index 0000000..c90930c --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/service/dto/response/TokenResponse.java @@ -0,0 +1,16 @@ +package org.sopt.practice.service.dto.response; + +public record TokenResponse( + String accessToken, + String refreshToken, + Long memberId +) { + + public static TokenResponse of( + String accessToken, + String refreshToken, + Long userId + ) { + return new TokenResponse(accessToken, refreshToken, userId); + } +} diff --git a/practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java b/practice/src/main/java/org/sopt/practice/service/jwt/JwtTokenProvider.java similarity index 60% rename from practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java rename to practice/src/main/java/org/sopt/practice/service/jwt/JwtTokenProvider.java index f1b94c1..a05d731 100644 --- a/practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java +++ b/practice/src/main/java/org/sopt/practice/service/jwt/JwtTokenProvider.java @@ -1,26 +1,25 @@ -package org.sopt.practice.common.jwt; +package org.sopt.practice.service.jwt; import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; -import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; +import org.sopt.practice.common.dto.ErrorMessage; +import org.sopt.practice.exception.UnauthorizedException; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; -import javax.crypto.SecretKey; -import java.nio.charset.StandardCharsets; import java.security.Key; -import java.util.Base64; import java.util.Date; @Component @RequiredArgsConstructor public class JwtTokenProvider implements InitializingBean { - private static final String USER_ID = "userId"; + public static final String USER_ID = "userId"; + public static final String TOKEN_TYPE = "tokenType"; private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14; private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 1; @@ -37,50 +36,40 @@ public void afterPropertiesSet() throws Exception {//스프링빈의 모든 속 } public String issueAccessToken(final Authentication authentication) { - return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME); + return createToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME, TokenType.ACCESS); } public String issueRefreshToken(final Authentication authentication) { - return generateToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME); + return createToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME, TokenType.REFRESH); } - - public String generateToken(Authentication authentication, Long tokenExpirationTime) { - final Date now = new Date(); - final Claims claims = Jwts.claims() - .setIssuedAt(now) - .setExpiration(new Date(now.getTime() + tokenExpirationTime)); // 만료 시간 - - claims.put(USER_ID, authentication.getPrincipal()); - - return Jwts.builder() - .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // Header - .setClaims(claims) // Claim - .signWith(key, SignatureAlgorithm.HS512) // Signature - .compact(); + public Long getUserFromJwt(String token) { + Claims claims = getBody(token); + return Long.valueOf(claims.get(USER_ID).toString()); } - private SecretKey getSigningKey() { - String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); - return Keys.hmacShaKeyFor(encodedKey.getBytes()); - } public JwtValidationType validateToken(String token) { try { final Claims claims = getBody(token); - return JwtValidationType.VALID_JWT; + if (claims.get(TOKEN_TYPE).equals(TokenType.ACCESS.getValue())) { + return JwtValidationType.VALID_ACCESS; + } else if (claims.get(TOKEN_TYPE).equals(TokenType.REFRESH.getValue())) { + return JwtValidationType.VALID_REFRESH; + } + throw new UnauthorizedException(ErrorMessage.INVALID_JWT_SIGNATURE); } catch (MalformedJwtException ex) { - return JwtValidationType.INVALID_JWT_TOKEN; + throw new UnauthorizedException(ErrorMessage.INVALID_JWT_SIGNATURE); } catch (ExpiredJwtException ex) { - return JwtValidationType.EXPIRED_JWT_TOKEN; + throw new UnauthorizedException(ErrorMessage.EXPIRED_JWT_TOKEN); } catch (UnsupportedJwtException ex) { - return JwtValidationType.UNSUPPORTED_JWT_TOKEN; + throw new UnauthorizedException(ErrorMessage.UNSUPPORTED_JWT_TOKEN); } catch (IllegalArgumentException ex) { - return JwtValidationType.EMPTY_JWT; + throw new UnauthorizedException(ErrorMessage.EMPTY_JWT); } } - private Claims getBody(final String token) { + public Claims getBody(final String token) { return Jwts.parserBuilder() .setSigningKey(key) .build() @@ -88,8 +77,27 @@ private Claims getBody(final String token) { .getBody(); } - public Long getUserFromJwt(String token) { - Claims claims = getBody(token); - return Long.valueOf(claims.get(USER_ID).toString()); + private String createToken(Authentication authentication, Long tokenExpirationTime, TokenType tokenType) { + final Date now = new Date(); + final Claims claims = Jwts.claims() + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + tokenExpirationTime)); // 만료 시간 + claims.put(USER_ID, authentication.getPrincipal()); + + switch (tokenType) { + case ACCESS: + claims.put(TOKEN_TYPE, TokenType.ACCESS.getValue()); + break; + case REFRESH: + claims.put(TOKEN_TYPE, TokenType.REFRESH.getValue()); + break; + } + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // Header + .setClaims(claims) // Claim + .signWith(key, SignatureAlgorithm.HS512) // Signature + .compact(); } + } diff --git a/practice/src/main/java/org/sopt/practice/service/jwt/JwtValidationType.java b/practice/src/main/java/org/sopt/practice/service/jwt/JwtValidationType.java new file mode 100644 index 0000000..0e8eb54 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/service/jwt/JwtValidationType.java @@ -0,0 +1,6 @@ +package org.sopt.practice.service.jwt; + +public enum JwtValidationType { + VALID_ACCESS, // 유효한 access 토큰 + VALID_REFRESH, // 유효한 refresh 토큰 +} \ No newline at end of file diff --git a/practice/src/main/java/org/sopt/practice/service/jwt/TokenType.java b/practice/src/main/java/org/sopt/practice/service/jwt/TokenType.java new file mode 100644 index 0000000..d57d1cd --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/service/jwt/TokenType.java @@ -0,0 +1,12 @@ +package org.sopt.practice.service.jwt; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum TokenType { + ACCESS("ACCESS"), REFRESH("REFRESH"); + + private final String value; +} diff --git a/practice/src/test/java/org/sopt/practice/controller/BlogControllerTest.java b/practice/src/test/java/org/sopt/practice/controller/BlogControllerTest.java index f7aa76f..fe32301 100644 --- a/practice/src/test/java/org/sopt/practice/controller/BlogControllerTest.java +++ b/practice/src/test/java/org/sopt/practice/controller/BlogControllerTest.java @@ -8,7 +8,7 @@ import org.sopt.practice.repository.MemberRepository; import org.sopt.practice.service.BlogService; import org.sopt.practice.service.MemberService; -import org.sopt.practice.service.dto.BlogCreateRequest; +import org.sopt.practice.service.dto.request.BlogCreateRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; diff --git a/practice/src/test/java/org/sopt/practice/controller/MemberControllerTest.java b/practice/src/test/java/org/sopt/practice/controller/MemberControllerTest.java index 5bcd5b7..1fec9ca 100644 --- a/practice/src/test/java/org/sopt/practice/controller/MemberControllerTest.java +++ b/practice/src/test/java/org/sopt/practice/controller/MemberControllerTest.java @@ -8,7 +8,7 @@ import org.sopt.practice.entity.Part; import org.sopt.practice.repository.MemberRepository; import org.sopt.practice.service.MemberService; -import org.sopt.practice.service.dto.MemberCreateDto; +import org.sopt.practice.service.dto.request.MemberCreateDto; import org.sopt.practice.settings.ApiTest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; From f9497b87abc4203cad142b93ff7bd2a237a3fd63 Mon Sep 17 00:00:00 2001 From: Parkjyun Date: Fri, 31 May 2024 10:27:14 +0900 Subject: [PATCH 3/3] [chore] remove comments and format code --- .../practice/auth/filter/JwtAuthenticationFilter.java | 3 ++- .../handler/CustomJwtAuthenticationEntryPoint.java | 8 -------- .../org/sopt/practice/common/dto/ErrorMessage.java | 8 ++++---- .../org/sopt/practice/common/dto/ErrorResponse.java | 4 ++-- .../org/sopt/practice/common/dto/SuccessMessage.java | 4 ++-- .../practice/common/dto/SuccessStatusResponse.java | 5 +++-- .../java/org/sopt/practice/service/AuthService.java | 4 ++-- .../java/org/sopt/practice/service/PostService.java | 2 +- .../sopt/practice/service/jwt/JwtTokenProvider.java | 11 +++++------ .../sopt/practice/service/jwt/JwtValidationType.java | 4 ++-- 10 files changed, 23 insertions(+), 30 deletions(-) diff --git a/practice/src/main/java/org/sopt/practice/auth/filter/JwtAuthenticationFilter.java b/practice/src/main/java/org/sopt/practice/auth/filter/JwtAuthenticationFilter.java index b545fb9..3f038fd 100644 --- a/practice/src/main/java/org/sopt/practice/auth/filter/JwtAuthenticationFilter.java +++ b/practice/src/main/java/org/sopt/practice/auth/filter/JwtAuthenticationFilter.java @@ -55,4 +55,5 @@ private String getJwtFromRequest(HttpServletRequest request) { return bearerToken.substring("Bearer ".length()); } return null; - }} + } +} diff --git a/practice/src/main/java/org/sopt/practice/auth/handler/CustomJwtAuthenticationEntryPoint.java b/practice/src/main/java/org/sopt/practice/auth/handler/CustomJwtAuthenticationEntryPoint.java index 668b10e..228f634 100644 --- a/practice/src/main/java/org/sopt/practice/auth/handler/CustomJwtAuthenticationEntryPoint.java +++ b/practice/src/main/java/org/sopt/practice/auth/handler/CustomJwtAuthenticationEntryPoint.java @@ -34,12 +34,4 @@ private void setResponse(HttpServletResponse response, ErrorMessage errorMessage response.getWriter().write(objectMapper.writeValueAsString(ErrorResponse.of(errorMessage))); } -// private void setResponse(HttpServletResponse response) throws IOException { -// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); -// response.setContentType(MediaType.APPLICATION_JSON_VALUE); -// response.setCharacterEncoding("UTF-8"); -// response.getWriter() -// .write(objectMapper.writeValueAsString( -// ErrorResponse.of(ErrorMessage.JWT_UNAUTHORIZED))); -// } } \ No newline at end of file diff --git a/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java b/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java index 76344da..16352e9 100644 --- a/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java +++ b/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java @@ -10,11 +10,11 @@ public enum ErrorMessage { /** * 400 BAD_REQUEST - * */ + */ BAD_HEADER_STRUCTURE(HttpStatus.BAD_REQUEST.value(), "헤더의 구조가 잘못되었습니다."), /** * 401 UNAUTHORIZED - * */ + */ INVALID_JWT_SIGNATURE(HttpStatus.UNAUTHORIZED.value(), "유효하지 않은 서명입니다."), INVALID_JWT_TOKEN(HttpStatus.UNAUTHORIZED.value(), "유효하지 않은 토큰입니다."), EXPIRED_JWT_TOKEN(HttpStatus.UNAUTHORIZED.value(), "만료된 토큰입니다."), @@ -23,11 +23,11 @@ public enum ErrorMessage { JWT_UNAUTHORIZED(HttpStatus.UNAUTHORIZED.value(), "사용자의 로그인 검증을 실패하였습니다."), /** * 403 FORBIDDEN - * */ + */ MEMBER_FORBIDDEN(HttpStatus.FORBIDDEN.value(), "회원의 권한이 없습니다."), /** * 404 BAD_REQUEST - * */ + */ MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 회원을 찾을 수 없습니다"), BLOG_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당하는 블로그를 찾을 수 없습니다"), POST_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당하는 포스트를 찾을 수 없습니다"), diff --git a/practice/src/main/java/org/sopt/practice/common/dto/ErrorResponse.java b/practice/src/main/java/org/sopt/practice/common/dto/ErrorResponse.java index 82dfa9c..604d0eb 100644 --- a/practice/src/main/java/org/sopt/practice/common/dto/ErrorResponse.java +++ b/practice/src/main/java/org/sopt/practice/common/dto/ErrorResponse.java @@ -2,11 +2,11 @@ public record ErrorResponse(int status, String message) { - public static ErrorResponse of (int status, String message) { + public static ErrorResponse of(int status, String message) { return new ErrorResponse(status, message); } - public static ErrorResponse of (ErrorMessage errorMessage) { + public static ErrorResponse of(ErrorMessage errorMessage) { return new ErrorResponse(errorMessage.getStatus(), errorMessage.getMessage()); } } diff --git a/practice/src/main/java/org/sopt/practice/common/dto/SuccessMessage.java b/practice/src/main/java/org/sopt/practice/common/dto/SuccessMessage.java index 78bf553..39c06e2 100644 --- a/practice/src/main/java/org/sopt/practice/common/dto/SuccessMessage.java +++ b/practice/src/main/java/org/sopt/practice/common/dto/SuccessMessage.java @@ -10,12 +10,12 @@ public enum SuccessMessage { /** * 200 OK - * */ + */ BLOG_GET_SUCCESS(HttpStatus.OK.value(), "블로그 조회에 성공했습니다"), POST_GET_SUCCESS(HttpStatus.OK.value(), "포스트 조회에 성공했습니다"), /** * 201 CREATED - * */ + */ BLOG_CREATE_SUCCESS(HttpStatus.CREATED.value(), "블로그 생성이 완료되었습니다"); private final int status; diff --git a/practice/src/main/java/org/sopt/practice/common/dto/SuccessStatusResponse.java b/practice/src/main/java/org/sopt/practice/common/dto/SuccessStatusResponse.java index f0ab94c..2257099 100644 --- a/practice/src/main/java/org/sopt/practice/common/dto/SuccessStatusResponse.java +++ b/practice/src/main/java/org/sopt/practice/common/dto/SuccessStatusResponse.java @@ -2,11 +2,12 @@ import com.fasterxml.jackson.annotation.JsonInclude; -public record SuccessStatusResponse(int status, String message, @JsonInclude(JsonInclude.Include.NON_NULL)T data) { +public record SuccessStatusResponse(int status, String message, @JsonInclude(JsonInclude.Include.NON_NULL) T data) { public static SuccessStatusResponse of(SuccessMessage successMessage) { return new SuccessStatusResponse(successMessage.getStatus(), successMessage.getMessage(), null); } - public static SuccessStatusResponse of(SuccessMessage successMessage, T data) { + + public static SuccessStatusResponse of(SuccessMessage successMessage, T data) { return new SuccessStatusResponse(successMessage.getStatus(), successMessage.getMessage(), data); } } diff --git a/practice/src/main/java/org/sopt/practice/service/AuthService.java b/practice/src/main/java/org/sopt/practice/service/AuthService.java index 6caacf8..cd2393f 100644 --- a/practice/src/main/java/org/sopt/practice/service/AuthService.java +++ b/practice/src/main/java/org/sopt/practice/service/AuthService.java @@ -72,10 +72,10 @@ private Member loadOrCreateMember(final MemberCreateDto memberCreate) { Member member; - if(!isExistMember(memberCreate)) { + if (!isExistMember(memberCreate)) { member = memberHelper.saveMember(Member.create(memberCreate.name(), memberCreate.part(), memberCreate.age())); } else { - member = memberHelper.findByNameAndPartAndAge(memberCreate.name(), memberCreate.part(), memberCreate.age()); + member = memberHelper.findByNameAndPartAndAge(memberCreate.name(), memberCreate.part(), memberCreate.age()); } return member; } diff --git a/practice/src/main/java/org/sopt/practice/service/PostService.java b/practice/src/main/java/org/sopt/practice/service/PostService.java index 55ebd4e..ac08b56 100644 --- a/practice/src/main/java/org/sopt/practice/service/PostService.java +++ b/practice/src/main/java/org/sopt/practice/service/PostService.java @@ -22,7 +22,7 @@ public class PostService { @Transactional public String createPost(final CreatePostRequest createPostRequest, final Long blogId, final Long memberId) { Blog blog = blogService.findById(blogId); - if(!blogService.isBlogOwner(blogId, memberId)) throw new ForbiddenException(ErrorMessage.MEMBER_FORBIDDEN); + if (!blogService.isBlogOwner(blogId, memberId)) throw new ForbiddenException(ErrorMessage.MEMBER_FORBIDDEN); return postRespository.save(Post.of(createPostRequest, blog)).getId().toString(); } diff --git a/practice/src/main/java/org/sopt/practice/service/jwt/JwtTokenProvider.java b/practice/src/main/java/org/sopt/practice/service/jwt/JwtTokenProvider.java index a05d731..b694fbc 100644 --- a/practice/src/main/java/org/sopt/practice/service/jwt/JwtTokenProvider.java +++ b/practice/src/main/java/org/sopt/practice/service/jwt/JwtTokenProvider.java @@ -30,7 +30,7 @@ public class JwtTokenProvider implements InitializingBean { private Key key; @Override - public void afterPropertiesSet() throws Exception {//스프링빈의 모든 속성이 설정된 후에 실행될 초기화 로직 + public void afterPropertiesSet() throws Exception { byte[] keyBytes = Decoders.BASE64.decode(JWT_SECRET); this.key = Keys.hmacShaKeyFor(keyBytes); } @@ -81,7 +81,7 @@ private String createToken(Authentication authentication, Long tokenExpirationTi final Date now = new Date(); final Claims claims = Jwts.claims() .setIssuedAt(now) - .setExpiration(new Date(now.getTime() + tokenExpirationTime)); // 만료 시간 + .setExpiration(new Date(now.getTime() + tokenExpirationTime)); claims.put(USER_ID, authentication.getPrincipal()); switch (tokenType) { @@ -94,10 +94,9 @@ private String createToken(Authentication authentication, Long tokenExpirationTi } return Jwts.builder() - .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // Header - .setClaims(claims) // Claim - .signWith(key, SignatureAlgorithm.HS512) // Signature + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setClaims(claims) + .signWith(key, SignatureAlgorithm.HS512) .compact(); } - } diff --git a/practice/src/main/java/org/sopt/practice/service/jwt/JwtValidationType.java b/practice/src/main/java/org/sopt/practice/service/jwt/JwtValidationType.java index 0e8eb54..761ee06 100644 --- a/practice/src/main/java/org/sopt/practice/service/jwt/JwtValidationType.java +++ b/practice/src/main/java/org/sopt/practice/service/jwt/JwtValidationType.java @@ -1,6 +1,6 @@ package org.sopt.practice.service.jwt; public enum JwtValidationType { - VALID_ACCESS, // 유효한 access 토큰 - VALID_REFRESH, // 유효한 refresh 토큰 + VALID_ACCESS, + VALID_REFRESH, } \ No newline at end of file