From f44fcd021db7c01a3214f8db162ebb96c1bbef38 Mon Sep 17 00:00:00 2001 From: eunseo5343 <130284467+eunseo5343@users.noreply.github.com> Date: Mon, 24 Jun 2024 22:55:18 +0900 Subject: [PATCH] =?UTF-8?q?Prefix=20[#5]=20jwt=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 ++ .idea/Jaksim-Server.iml | 9 ++ .idea/gradle.xml | 15 ++++ .idea/misc.xml | 7 ++ .idea/modules.xml | 8 ++ .idea/vcs.xml | 6 ++ jaksim/build.gradle | 9 ++ .../sopt/jaksim/auth/PrincipalHandler.java | 26 ++++++ .../org/sopt/jaksim/auth/SecurityConfig.java | 48 +++++++++++ .../sopt/jaksim/auth/UserAuthentication.java | 17 ++++ .../filter/CustomAccessDeniedHandler.java | 23 +++++ .../CustomJwtAuthenticationEntryPoint.java | 34 ++++++++ .../auth/filter/JwtAuthenticationFilter.java | 54 ++++++++++++ .../global/common/jwt/JwtTokenProvider.java | 84 +++++++++++++++++++ .../global/common/jwt/JwtValidationType.java | 10 +++ .../jaksim/global/message/ErrorMessage.java | 2 + .../jaksim/user/api/UserApiController.java | 41 ++++++++- .../user/dto/request/UserSignInRequest.java | 5 +- .../user/dto/response/UserSignInResponse.java | 16 +++- .../sopt/jaksim/user/service/UserService.java | 64 ++++++++++++++ jaksim/src/main/resources/application.yml | 6 ++ 21 files changed, 487 insertions(+), 5 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/Jaksim-Server.iml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 jaksim/src/main/java/org/sopt/jaksim/auth/PrincipalHandler.java create mode 100644 jaksim/src/main/java/org/sopt/jaksim/auth/SecurityConfig.java create mode 100644 jaksim/src/main/java/org/sopt/jaksim/auth/UserAuthentication.java create mode 100644 jaksim/src/main/java/org/sopt/jaksim/auth/filter/CustomAccessDeniedHandler.java create mode 100644 jaksim/src/main/java/org/sopt/jaksim/auth/filter/CustomJwtAuthenticationEntryPoint.java create mode 100644 jaksim/src/main/java/org/sopt/jaksim/auth/filter/JwtAuthenticationFilter.java create mode 100644 jaksim/src/main/java/org/sopt/jaksim/global/common/jwt/JwtTokenProvider.java create mode 100644 jaksim/src/main/java/org/sopt/jaksim/global/common/jwt/JwtValidationType.java create mode 100644 jaksim/src/main/resources/application.yml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/Jaksim-Server.iml b/.idea/Jaksim-Server.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/Jaksim-Server.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..c7ef476 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3b65e25 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..628aaed --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/jaksim/build.gradle b/jaksim/build.gradle index e43a0bd..e5d06a4 100644 --- a/jaksim/build.gradle +++ b/jaksim/build.gradle @@ -36,6 +36,15 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + //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/jaksim/src/main/java/org/sopt/jaksim/auth/PrincipalHandler.java b/jaksim/src/main/java/org/sopt/jaksim/auth/PrincipalHandler.java new file mode 100644 index 0000000..f71a68c --- /dev/null +++ b/jaksim/src/main/java/org/sopt/jaksim/auth/PrincipalHandler.java @@ -0,0 +1,26 @@ +package org.sopt.jaksim.auth; + +import org.sopt.jaksim.global.exception.UnauthorizedException; +import org.sopt.jaksim.global.message.ErrorMessage; +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); + } + } +} \ No newline at end of file diff --git a/jaksim/src/main/java/org/sopt/jaksim/auth/SecurityConfig.java b/jaksim/src/main/java/org/sopt/jaksim/auth/SecurityConfig.java new file mode 100644 index 0000000..05683ee --- /dev/null +++ b/jaksim/src/main/java/org/sopt/jaksim/auth/SecurityConfig.java @@ -0,0 +1,48 @@ +package org.sopt.jaksim.auth; + +import lombok.RequiredArgsConstructor; +import org.sopt.jaksim.auth.filter.CustomAccessDeniedHandler; +import org.sopt.jaksim.auth.filter.CustomJwtAuthenticationEntryPoint; +import org.sopt.jaksim.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/jaksim/src/main/java/org/sopt/jaksim/auth/UserAuthentication.java b/jaksim/src/main/java/org/sopt/jaksim/auth/UserAuthentication.java new file mode 100644 index 0000000..91984f5 --- /dev/null +++ b/jaksim/src/main/java/org/sopt/jaksim/auth/UserAuthentication.java @@ -0,0 +1,17 @@ +package org.sopt.jaksim.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/jaksim/src/main/java/org/sopt/jaksim/auth/filter/CustomAccessDeniedHandler.java b/jaksim/src/main/java/org/sopt/jaksim/auth/filter/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..139ba1a --- /dev/null +++ b/jaksim/src/main/java/org/sopt/jaksim/auth/filter/CustomAccessDeniedHandler.java @@ -0,0 +1,23 @@ +package org.sopt.jaksim.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); + } +} \ No newline at end of file diff --git a/jaksim/src/main/java/org/sopt/jaksim/auth/filter/CustomJwtAuthenticationEntryPoint.java b/jaksim/src/main/java/org/sopt/jaksim/auth/filter/CustomJwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..53a904a --- /dev/null +++ b/jaksim/src/main/java/org/sopt/jaksim/auth/filter/CustomJwtAuthenticationEntryPoint.java @@ -0,0 +1,34 @@ +package org.sopt.jaksim.auth.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.sopt.jaksim.global.message.ErrorMessage; +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( + ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION.getMessage())); + } +} diff --git a/jaksim/src/main/java/org/sopt/jaksim/auth/filter/JwtAuthenticationFilter.java b/jaksim/src/main/java/org/sopt/jaksim/auth/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..d2fd7ab --- /dev/null +++ b/jaksim/src/main/java/org/sopt/jaksim/auth/filter/JwtAuthenticationFilter.java @@ -0,0 +1,54 @@ +package org.sopt.jaksim.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.jaksim.auth.UserAuthentication; +import org.sopt.jaksim.global.common.jwt.JwtTokenProvider; +import org.sopt.jaksim.global.exception.UnauthorizedException; +import org.sopt.jaksim.global.message.ErrorMessage; +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.jaksim.global.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; + } +} \ No newline at end of file diff --git a/jaksim/src/main/java/org/sopt/jaksim/global/common/jwt/JwtTokenProvider.java b/jaksim/src/main/java/org/sopt/jaksim/global/common/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..88b5d47 --- /dev/null +++ b/jaksim/src/main/java/org/sopt/jaksim/global/common/jwt/JwtTokenProvider.java @@ -0,0 +1,84 @@ +package org.sopt.jaksim.global.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; + private static final Long REFRESH_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 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(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/jaksim/src/main/java/org/sopt/jaksim/global/common/jwt/JwtValidationType.java b/jaksim/src/main/java/org/sopt/jaksim/global/common/jwt/JwtValidationType.java new file mode 100644 index 0000000..e081aab --- /dev/null +++ b/jaksim/src/main/java/org/sopt/jaksim/global/common/jwt/JwtValidationType.java @@ -0,0 +1,10 @@ +package org.sopt.jaksim.global.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/jaksim/src/main/java/org/sopt/jaksim/global/message/ErrorMessage.java b/jaksim/src/main/java/org/sopt/jaksim/global/message/ErrorMessage.java index 31bc9cb..85b7f99 100644 --- a/jaksim/src/main/java/org/sopt/jaksim/global/message/ErrorMessage.java +++ b/jaksim/src/main/java/org/sopt/jaksim/global/message/ErrorMessage.java @@ -47,4 +47,6 @@ public enum ErrorMessage { private final HttpStatus httpStatus; private final String code; private final String message; + + } diff --git a/jaksim/src/main/java/org/sopt/jaksim/user/api/UserApiController.java b/jaksim/src/main/java/org/sopt/jaksim/user/api/UserApiController.java index 3b2e577..d916fac 100644 --- a/jaksim/src/main/java/org/sopt/jaksim/user/api/UserApiController.java +++ b/jaksim/src/main/java/org/sopt/jaksim/user/api/UserApiController.java @@ -1,12 +1,49 @@ package org.sopt.jaksim.user.api; import lombok.RequiredArgsConstructor; +import org.sopt.jaksim.auth.PrincipalHandler; +import org.sopt.jaksim.global.message.SuccessMessage; +import org.sopt.jaksim.user.dto.request.UserSignInRequest; +import org.sopt.jaksim.user.dto.response.UserSignInResponse; +import org.sopt.jaksim.user.service.UserService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import static org.sopt.jaksim.global.message.SuccessMessage.USER_SIGN_IN_SUCCESS; + +@RestController @RequiredArgsConstructor -@RequestMapping("/api/v1/users") -@Controller +@RequestMapping("/api/v1") public class UserApiController { + private final UserService userService; + private final PrincipalHandler principalHandler; + + @PostMapping("/user/login") + public ResponseEntity> login(@RequestBody UserSignInRequest userSignInRequest) { + UserSignInResponse response = userService.login(userSignInRequest); + return ResponseEntity.status(HttpStatus.OK) + .body(SuccessStatusResponse.of(SuccessMessage.USER_SIGN_IN_SUCCESS, response)); + } +//ApiResponseUtil>> + // public ApiResponseUtil refreshToken() { + Long userId = principalHandler.getUserIdFromPrincipal(); + UserSignInResponse response = userService.refreshToken(userId); + return ResponseEntity.status(HttpStatus.CREATED) + .header("Location", response.userId()) + .body(response); + } +} + + + } + + diff --git a/jaksim/src/main/java/org/sopt/jaksim/user/dto/request/UserSignInRequest.java b/jaksim/src/main/java/org/sopt/jaksim/user/dto/request/UserSignInRequest.java index 0eae9bc..8a8ea47 100644 --- a/jaksim/src/main/java/org/sopt/jaksim/user/dto/request/UserSignInRequest.java +++ b/jaksim/src/main/java/org/sopt/jaksim/user/dto/request/UserSignInRequest.java @@ -1,4 +1,7 @@ package org.sopt.jaksim.user.dto.request; -public record UserSignInRequest() { +public record UserSignInRequest( + String userId, + String password +){ } diff --git a/jaksim/src/main/java/org/sopt/jaksim/user/dto/response/UserSignInResponse.java b/jaksim/src/main/java/org/sopt/jaksim/user/dto/response/UserSignInResponse.java index 36601b7..ffd0967 100644 --- a/jaksim/src/main/java/org/sopt/jaksim/user/dto/response/UserSignInResponse.java +++ b/jaksim/src/main/java/org/sopt/jaksim/user/dto/response/UserSignInResponse.java @@ -1,4 +1,16 @@ package org.sopt.jaksim.user.dto.response; -public record UserSignInResponse() { -} +public record UserSignInResponse( + String accessToken, + String refreshToken, + String userId +) { + + public static UserSignInResponse of( + String accessToken, + String refreshToken, + String userId + ) { + return new UserSignInResponse(accessToken, refreshToken, userId); + } +} \ No newline at end of file diff --git a/jaksim/src/main/java/org/sopt/jaksim/user/service/UserService.java b/jaksim/src/main/java/org/sopt/jaksim/user/service/UserService.java index 6e2efd6..e59ccd7 100644 --- a/jaksim/src/main/java/org/sopt/jaksim/user/service/UserService.java +++ b/jaksim/src/main/java/org/sopt/jaksim/user/service/UserService.java @@ -1,4 +1,68 @@ package org.sopt.jaksim.user.service; +import lombok.RequiredArgsConstructor; +import org.sopt.jaksim.auth.UserAuthentication; +import org.sopt.jaksim.global.common.jwt.JwtTokenProvider; +import org.sopt.jaksim.global.exception.UnauthorizedException; +import org.sopt.jaksim.global.message.ErrorMessage; +import org.sopt.jaksim.user.domain.User; +import org.sopt.jaksim.user.dto.request.UserSignInRequest; +import org.sopt.jaksim.user.dto.response.UserSignInResponse; +import org.sopt.jaksim.user.repository.UserRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.lang.reflect.Member; + +@Service +@RequiredArgsConstructor public class UserService { + + private final UserRepository userRepository; + private final JwtTokenProvider jwtTokenProvider; + private final RedisTokenRepository redisTokenRepository; + + @Transactional + public UserSignInResponse signIn(UserSignInRequest request) { + // 1. 사용자 자격 증명 검증 + Member member = userRepository.findByUserIdAndPassword(request.getUserId(), request.getPassword()) + .orElseThrow(() -> new RuntimeException("Invalid credentials")); // 클래스명을 userRepository로 수정 + + // 2. 토큰 발행 + Long memberId = member.getId(); + String accessToken = jwtTokenProvider.issueAccessToken( + UserAuthentication.createUserAuthentication(memberId) + ); + String refreshToken = jwtTokenProvider.issueRefreshToken( + UserAuthentication.createUserAuthentication(memberId) + ); + + // 3. 리프레시 토큰 저장 + redisTokenRepository.save(Token.of(memberId.toString(), refreshToken)); + + // 4. 토큰 반환 + return new UserSignInResponse(accessToken, refreshToken, memberId.toString()); + } + + public UserSignInResponse refreshToken(Long memberId) { + if (!redisTokenRepository.existsById(memberId.toString())) { + throw new UnauthorizedException("Invalid refresh token"); // 예외 메시지로 수정 + } + + // 유저 ID 검증 및 토큰 재발급 + User user = userRepository.findById(memberId) + .orElseThrow(() -> new UnauthorizedException(ErrorMessage.NOT_FOUND)); + + String accessToken = jwtTokenProvider.issueAccessToken( + UserAuthentication.createUserAuthentication(memberId) + ); + String refreshToken = jwtTokenProvider.issueRefreshToken( + UserAuthentication.createUserAuthentication(memberId) + ); + + redisTokenRepository.save(Token.of(memberId.toString(), refreshToken)); // 토큰 저장 구문 정리 + + return new UserSignInResponse(accessToken, refreshToken, memberId.toString()); + } } + diff --git a/jaksim/src/main/resources/application.yml b/jaksim/src/main/resources/application.yml new file mode 100644 index 0000000..06342b4 --- /dev/null +++ b/jaksim/src/main/resources/application.yml @@ -0,0 +1,6 @@ +spring: + profiles: + active: dev + +jwt: + secret: nowsoptnowsoptnownownononwonwownwownownwowno \ No newline at end of file