From 13eca92e3766a7bf6844602550ffb6757ae9448e Mon Sep 17 00:00:00 2001 From: sebbbin Date: Sat, 18 May 2024 13:52:17 +0900 Subject: [PATCH] #15 [feat] spring security / jwt --- .../practice/build.gradle" | 9 +++ .../sopt/practice/auth/PrincipalHandler.java" | 26 ++++++ .../sopt/practice/auth/SecurityConfig.java" | 50 ++++++++++++ .../practice/auth/UserAuthentication.java" | 18 +++++ .../filter/CustomAccessDeniedHandler.java" | 23 ++++++ .../CustomJwtAuthenticationEntryPoint.java" | 36 +++++++++ .../auth/filter/JwtAuthenticationFilter.java" | 55 +++++++++++++ .../common/GlobalExceptionHandler.java" | 9 ++- .../practice/common/dto/ErrorMessage.java" | 1 + .../common/jwt/JwtTokenProvider.java" | 79 +++++++++++++++++++ .../common/jwt/JwtValidationType.java" | 10 +++ .../practice/controller/BlogController.java" | 13 ++- .../controller/MemberController.java" | 24 ++++-- .../sopt/practice/dto/UserJoinResponse.java" | 16 ++++ .../exception/BusinessException.java" | 2 + .../exception/UnauthorizedException.java" | 9 +++ .../sopt/practice/service/MemberService.java" | 28 +++++-- 17 files changed, 392 insertions(+), 16 deletions(-) create mode 100644 "\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/PrincipalHandler.java" create mode 100644 "\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java" create mode 100644 "\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/UserAuthentication.java" create mode 100644 "\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/filter/CustomAccessDeniedHandler.java" create mode 100644 "\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/filter/CustomJwtAuthenticationEntryPoint.java" create mode 100644 "\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/filter/JwtAuthenticationFilter.java" create mode 100644 "\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java" create mode 100644 "\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/jwt/JwtValidationType.java" create mode 100644 "\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/dto/UserJoinResponse.java" create mode 100644 "\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/exception/UnauthorizedException.java" diff --git "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/build.gradle" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/build.gradle" index fed8ce9..3a02eb8 100644 --- "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/build.gradle" +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/build.gradle" @@ -30,6 +30,15 @@ dependencies { compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + + //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' } tasks.named('test') { diff --git "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/PrincipalHandler.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/PrincipalHandler.java" new file mode 100644 index 0000000..c10e18c --- /dev/null +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/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_EXCEPTION); + } + } +} diff --git "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java" new file mode 100644 index 0000000..92619e3 --- /dev/null +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java" @@ -0,0 +1,50 @@ +package org.sopt.practice.auth; + + +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.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + + +@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/member"}; + + @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(); + } +} diff --git "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/UserAuthentication.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/UserAuthentication.java" new file mode 100644 index 0000000..09f0e02 --- /dev/null +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/UserAuthentication.java" @@ -0,0 +1,18 @@ +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/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/filter/CustomAccessDeniedHandler.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/filter/CustomAccessDeniedHandler.java" new file mode 100644 index 0000000..2fbaaba --- /dev/null +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/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/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/filter/CustomJwtAuthenticationEntryPoint.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/filter/CustomJwtAuthenticationEntryPoint.java" new file mode 100644 index 0000000..9442298 --- /dev/null +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/filter/CustomJwtAuthenticationEntryPoint.java" @@ -0,0 +1,36 @@ +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 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 +public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + 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_EXCEPTION.getStatus(), + ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION.getMessage()))); + } +} diff --git "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/filter/JwtAuthenticationFilter.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/filter/JwtAuthenticationFilter.java" new file mode 100644 index 0000000..d4dd0ca --- /dev/null +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/auth/filter/JwtAuthenticationFilter.java" @@ -0,0 +1,55 @@ +package org.sopt.practice.auth.filter; + +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 org.sopt.practice.auth.UserAuthentication; +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 +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 { + try { + final String token = getJwtFromRequest(request); + if (jwtTokenProvider.validateToken(token) == VALID_JWT) { + Long memberId = jwtTokenProvider.getUserFromJwt(token); + UserAuthentication authentication = UserAuthentication.createUserAuthentication(memberId); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception exception) { + throw new UnauthorizedException(ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION); + } + 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/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/GlobalExceptionHandler.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/GlobalExceptionHandler.java" index c4e394c..99f3482 100644 --- "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/GlobalExceptionHandler.java" +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/GlobalExceptionHandler.java" @@ -1,9 +1,10 @@ package org.sopt.practice.common; -import jakarta.persistence.EntityNotFoundException; + import org.sopt.practice.common.dto.ErrorMessage; import org.sopt.practice.common.dto.ErrorResponse; 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; @@ -26,4 +27,10 @@ protected ResponseEntity handleEntityNotFoundException (NotFoundE return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.of(ErrorMessage.MEMBER_NOT_FOUND_BY_ID_EXCEPTION)); } + @ExceptionHandler(UnauthorizedException.class) + protected ResponseEntity handlerUnauthorizedException(UnauthorizedException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(ErrorResponse.of(e.getErrorMessage().getStatus(), e.getErrorMessage().getMessage())); + } + } diff --git "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java" index 55b89e5..d53f335 100644 --- "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java" +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/dto/ErrorMessage.java" @@ -9,6 +9,7 @@ public enum ErrorMessage { MEMBER_NOT_FOUND_BY_ID_EXCEPTION(HttpStatus.NOT_FOUND.value(), "ID에 해당하는 사용자가 존재하지 않습니다."), BLOG_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "ID에 해당하는 블로그가 존재하지 않습니다."), + JWT_UNAUTHORIZED_EXCEPTION(HttpStatus.UNAUTHORIZED.value(), "사용자의 로그인 검증을 실패했습니다."), ; private final int status; private final String message; diff --git "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java" new file mode 100644 index 0000000..f2d7893 --- /dev/null +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java" @@ -0,0 +1,79 @@ +package org.sopt.practice.common.jwt; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Base64; +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + + private static final String USER_ID = "userId"; + + private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14; + + @Value("${jwt.secret}") + private String JWT_SECRET; + + + public String issueAccessToken(final Authentication authentication) { + return generateToken(authentication, ACCESS_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(getSigningKey()) // Signature + .compact(); + } + + private SecretKey getSigningKey() { + String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); //SecretKey 통해 서명 생성 + return Keys.hmacShaKeyFor(encodedKey.getBytes()); //일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘 사용 + } + + 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(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public Long getUserFromJwt(String token) { + Claims claims = getBody(token); + return Long.valueOf(claims.get(USER_ID).toString()); + } +} diff --git "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/jwt/JwtValidationType.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/common/jwt/JwtValidationType.java" new file mode 100644 index 0000000..74bfedb --- /dev/null +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/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 +} diff --git "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/controller/BlogController.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/controller/BlogController.java" index caf4ab6..64db10f 100644 --- "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/controller/BlogController.java" +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/controller/BlogController.java" @@ -3,6 +3,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.dto.BlogCreateRequest; @@ -12,18 +13,22 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.net.URI; + @RestController @RequestMapping("/api/v1") @RequiredArgsConstructor public class BlogController { private final BlogService blogService; + private final PrincipalHandler principalHandler; //성공했을 때 값을 반환하기 위한 SuccessStatusResponse @PostMapping("/blog") - public ResponseEntity creatBlog(@RequestHeader Long memberId, - @RequestBody BlogCreateRequest blogCreateRequest){ - return ResponseEntity.status(HttpStatus.CREATED).header("Location",blogService.create(memberId, blogCreateRequest)) - .body(SuccessStatusResponse.of(SuccessMessage.BLOG_CREATE_SUCCESS)); + public ResponseEntity createBlog( + BlogCreateRequest blogCreateRequest + ) { + return ResponseEntity.created(URI.create(blogService.create( + principalHandler.getUserIdFromPrincipal(), blogCreateRequest))).build(); } @PatchMapping("/blog/{blogId}/title") diff --git "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/controller/MemberController.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/controller/MemberController.java" index 8382adb..4379de8 100644 --- "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/controller/MemberController.java" +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/controller/MemberController.java" @@ -3,7 +3,9 @@ import lombok.RequiredArgsConstructor; import org.sopt.practice.dto.MemberCreateDto; import org.sopt.practice.dto.MemberFindDto; +import org.sopt.practice.dto.UserJoinResponse; import org.sopt.practice.service.MemberService; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -16,11 +18,11 @@ public class MemberController { private final MemberService memberService; - @PostMapping - public ResponseEntity postMember(@RequestBody MemberCreateDto memberCreateDto) - { - return ResponseEntity.created(URI.create(memberService.createMember(memberCreateDto))).build(); - } +// @PostMapping +// public ResponseEntity postMember(@RequestBody MemberCreateDto memberCreateDto) +// { +// return ResponseEntity.created(URI.create(memberService.createMember(memberCreateDto))).build(); +// } //ResponseEntity는 generic타입을 넣을 수 있음 @@ -34,4 +36,16 @@ public ResponseEntity deleteMemberById(@PathVariable Long memberId){ memberService.deleteMemberById(memberId); return ResponseEntity.noContent().build(); } + + @PostMapping + public ResponseEntity postMember( + @RequestBody MemberCreateDto memberCreate + ) { + UserJoinResponse userJoinResponse = memberService.createMember(memberCreate); + return ResponseEntity.status(HttpStatus.CREATED) + .header("Location", userJoinResponse.userId()) + .body( + userJoinResponse + ); + } } diff --git "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/dto/UserJoinResponse.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/dto/UserJoinResponse.java" new file mode 100644 index 0000000..d290c88 --- /dev/null +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/dto/UserJoinResponse.java" @@ -0,0 +1,16 @@ +package org.sopt.practice.dto; + +public record UserJoinResponse( + String accessToken, + String userId +) { + + public static UserJoinResponse of( + String accessToken, + String userId + ) { + return new UserJoinResponse(accessToken, userId); + } +} + + diff --git "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/exception/BusinessException.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/exception/BusinessException.java" index daa1a33..54d0c8f 100644 --- "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/exception/BusinessException.java" +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/exception/BusinessException.java" @@ -1,7 +1,9 @@ package org.sopt.practice.exception; +import lombok.Getter; import org.sopt.practice.common.dto.ErrorMessage; +@Getter public class BusinessException extends RuntimeException{ private ErrorMessage errorMessage; public BusinessException(ErrorMessage errorMessage){ diff --git "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/exception/UnauthorizedException.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/exception/UnauthorizedException.java" new file mode 100644 index 0000000..a7a7d7f --- /dev/null +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/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/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/service/MemberService.java" "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/service/MemberService.java" index dc023d6..16b863e 100644 --- "a/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/service/MemberService.java" +++ "b/\354\204\270\353\257\270\353\202\230/6\354\260\250 \354\204\270\353\257\270\353\202\230/practice/src/main/java/org/sopt/practice/service/MemberService.java" @@ -3,11 +3,14 @@ import jakarta.persistence.EntityNotFoundException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.sopt.practice.auth.UserAuthentication; import org.sopt.practice.common.dto.ErrorMessage; import org.sopt.practice.common.dto.ErrorResponse; +import org.sopt.practice.common.jwt.JwtTokenProvider; import org.sopt.practice.domain.Member; import org.sopt.practice.dto.MemberCreateDto; import org.sopt.practice.dto.MemberFindDto; +import org.sopt.practice.dto.UserJoinResponse; import org.sopt.practice.exception.NotFoundException; import org.sopt.practice.repository.MemberRepository; import org.springframework.stereotype.Service; @@ -16,13 +19,26 @@ @RequiredArgsConstructor //의존성 주입 public class MemberService { private final MemberRepository memberRepository; - - @Transactional //DB변경사항을 반영할 때 사용 - public String createMember(MemberCreateDto memberCreateDto) - { - Member member = memberRepository.save(Member.create(memberCreateDto.name(), memberCreateDto.part(), memberCreateDto.age())); - return member.getId().toString(); + private final JwtTokenProvider jwtTokenProvider; + @Transactional + public UserJoinResponse 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 UserJoinResponse.of(accessToken, memberId.toString()); } +// @Transactional //DB변경사항을 반영할 때 사용 +// public String createMember(MemberCreateDto memberCreateDto) +// { +// Member member = memberRepository.save(Member.create(memberCreateDto.name(), memberCreateDto.part(), memberCreateDto.age())); +// return member.getId().toString(); +// } public Member findById(Long memberId) { return memberRepository.findById(memberId).orElseThrow( () -> new NotFoundException(ErrorMessage.MEMBER_NOT_FOUND_BY_ID_EXCEPTION)