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 extends GrantedAuthority> 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