From b141a231e28c1055b7f92f614a944fdc871f630b Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 15:47:29 +0900 Subject: [PATCH 01/25] =?UTF-8?q?docs:=20jjwt=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20build.gradle=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Joonseok/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Joonseok/build.gradle b/Joonseok/build.gradle index ce009fae..6b6dd270 100644 --- a/Joonseok/build.gradle +++ b/Joonseok/build.gradle @@ -31,6 +31,9 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testAnnotationProcessor 'org.projectlombok:lombok' + // JWT + implementation 'io.jsonwebtoken:jjwt:0.13.0' + // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.3' From d33d75dbd067863d0b6fe49baf34c72ad3fbd29c Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 15:56:58 +0900 Subject: [PATCH 02/25] =?UTF-8?q?refactor:=20=EA=B5=AC=ED=98=84=EC=9D=B4?= =?UTF-8?q?=20=EB=B6=88=EB=B6=84=EB=AA=85=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=AA=85=EB=AA=85=20AuthUser=EB=A5=BC=20CustomUserDetails?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/entity/{AuthUser.java => CustomUserDetails.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Joonseok/src/main/java/com/umc/study/global/security/entity/{AuthUser.java => CustomUserDetails.java} (100%) diff --git a/Joonseok/src/main/java/com/umc/study/global/security/entity/AuthUser.java b/Joonseok/src/main/java/com/umc/study/global/security/entity/CustomUserDetails.java similarity index 100% rename from Joonseok/src/main/java/com/umc/study/global/security/entity/AuthUser.java rename to Joonseok/src/main/java/com/umc/study/global/security/entity/CustomUserDetails.java From b99d0c9d4bb59fb87b9cf4399e8b894ef056c69b Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 15:58:03 +0900 Subject: [PATCH 03/25] =?UTF-8?q?refactor:=20CustomUserDetails=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=A0=84=ED=8C=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc/study/global/security/entity/CustomUserDetails.java | 2 +- .../global/security/service/CustomUserDetailsService.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Joonseok/src/main/java/com/umc/study/global/security/entity/CustomUserDetails.java b/Joonseok/src/main/java/com/umc/study/global/security/entity/CustomUserDetails.java index b1afe0ca..59d490bf 100644 --- a/Joonseok/src/main/java/com/umc/study/global/security/entity/CustomUserDetails.java +++ b/Joonseok/src/main/java/com/umc/study/global/security/entity/CustomUserDetails.java @@ -12,7 +12,7 @@ @Getter @RequiredArgsConstructor -public class AuthUser implements UserDetails { +public class CustomUserDetails implements UserDetails { private final User user; diff --git a/Joonseok/src/main/java/com/umc/study/global/security/service/CustomUserDetailsService.java b/Joonseok/src/main/java/com/umc/study/global/security/service/CustomUserDetailsService.java index 57c47cff..48a77b7b 100644 --- a/Joonseok/src/main/java/com/umc/study/global/security/service/CustomUserDetailsService.java +++ b/Joonseok/src/main/java/com/umc/study/global/security/service/CustomUserDetailsService.java @@ -3,7 +3,7 @@ import com.umc.study.domain.user.entity.User; import com.umc.study.domain.user.exception.UserNotFoundException; import com.umc.study.domain.user.repository.UserRepository; -import com.umc.study.global.security.entity.AuthUser; +import com.umc.study.global.security.entity.CustomUserDetails; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; @@ -24,6 +24,6 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx User user = userRepository.findByEmail(username) .orElseThrow(UserNotFoundException::new); - return new AuthUser(user); + return new CustomUserDetails(user); } } From 9525061118b97138ecf50bc467238335015fdd55 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 15:58:47 +0900 Subject: [PATCH 04/25] =?UTF-8?q?feat:=20JWT=20=ED=86=A0=ED=81=B0=EC=9D=84?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8A=94=20JwtTokenProvider=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/global/jwt/JwtTokenProvider.java | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenProvider.java diff --git a/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenProvider.java b/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenProvider.java new file mode 100644 index 00000000..91f8e628 --- /dev/null +++ b/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenProvider.java @@ -0,0 +1,138 @@ +package com.umc.study.global.jwt; + +import com.umc.study.domain.user.entity.User; +import com.umc.study.domain.user.exception.UserNotFoundException; +import com.umc.study.domain.user.repository.UserRepository; +import com.umc.study.global.security.entity.CustomUserDetails; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +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.time.Duration; +import java.time.Instant; +import java.util.Date; + + +@Component +public class JwtTokenProvider { + + private final UserRepository userRepository; + + private final String secretKey; + private Key key; + + // jwt signature를 final, env로 주입하는 생성자 + public JwtTokenProvider( + UserRepository userRepository, + @Value("${jwt.secret}") String secretKey + ) { + this.userRepository = userRepository; + this.secretKey = secretKey; + } + + // signature -> Byte 배열 -> hmac sha 이용하여 Key 객체에 할당 + @PostConstruct + public void init() { + byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8); + if(keyBytes.length < 32) { + throw new IllegalArgumentException("jwt.secret must be at least 256 bits (32 bytes)"); + } + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + /** + * + * Date와 Instant의 차이 + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
 정밀도가변성thread-safetytestable
java.util.Date밀리초 단위mutable안전하지 않음유닛 테스트에서 시간 조작 불가
java.time.Instant나노초 단위immutable안전유닛 테스트에서 시간 조작 가능
+ */ + public String generateToken(User user, Duration expiration) { + + // 현재 시각을 기준으로 만료일 설정 + Instant now = Instant.now(); + Instant expiry = now.plus(expiration); + + return Jwts.builder() + + // subject : User의 고유 식별 필드 + .subject(String.valueOf(user.getId())) + + // claim : User에서 자주 참조되는 필드(DB 접근을 최소화하기 위해) + .claim("email", user.getEmail()) + .claim("role", user.getRole() != null ? user.getRole() : null) + + // issuedAt : 토큰 발행 시각 + .issuedAt(Date.from(now)) + + // expiration : 토큰 만료 시각 + .expiration(Date.from(expiry)) + + // signWith : signature를 위한 키 설정 (default : HS256) + .signWith(key) + + // JWT 토큰(subject.claim.signature)로 발행 + .compact(); + } + + private Claims getClaims(String token) { + return Jwts.parser() + .verifyWith((SecretKey) key) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + // JWT 토큰에서 사용자 정보를 추출하고, Spring Security에서 사용할 수 있게 Authentication 객체로 가공 + public Authentication getAuthentication(String token) { + + // extract Unique field + Long id = Long.parseLong(getClaims(token).getSubject()); + + User found = userRepository.findById(id) + .orElseThrow(UserNotFoundException::new); + + CustomUserDetails userDetails = new CustomUserDetails(found); + + return new UsernamePasswordAuthenticationToken( + userDetails, + token, + userDetails.getAuthorities() + ); + } + + public boolean isExpired(String token) { + return getClaims(token).getExpiration().before(Date.from(Instant.now())); + } + +} From 802d4dc9487fbf6c89ebadb8a6a3509b75049a3d Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 16:12:43 +0900 Subject: [PATCH 05/25] =?UTF-8?q?feat:=20Bearer=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=EC=9D=B4=20=EC=9E=85=EB=A0=A5=EB=90=9C=20=EA=B2=BD=EC=9A=B0,?= =?UTF-8?q?=20SecurityContextHolder=EC=97=90=20UserDetails=EC=9D=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=B2=B4=EC=9D=B8=20CustomUserDetails?= =?UTF-8?q?=EB=A5=BC=20=EB=93=B1=EB=A1=9D,=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EC=95=88=20=EB=90=9C=20=EA=B2=BD=EC=9A=B0=EC=97=90=EB=8A=94=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=8C=20=ED=95=84=ED=84=B0=20=EC=B2=B4=EC=9D=B8?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C,=20=EB=93=B1=EB=A1=9D=20=EB=90=98=EC=96=B4?= =?UTF-8?q?=EC=9E=88=EC=A7=80=EB=A7=8C=20=EC=9C=A0=ED=9A=A8=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EA=B2=BD=EC=9A=B0=EC=97=90?= =?UTF-8?q?=EB=8A=94=20=ED=95=84=ED=84=B0=20=EC=B2=B4=EC=9D=B8=EC=9D=84=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=8C=EC=9C=BC=EB=A1=9C=20=EB=84=98=EA=B8=B0?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EA=B3=A0,=20=EC=98=88=EC=99=B8=EB=A5=BC?= =?UTF-8?q?=20=EC=9C=84=EB=A1=9C=20=EC=A0=84=ED=8C=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc/study/global/jwt/JwtTokenFilter.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenFilter.java diff --git a/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenFilter.java b/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenFilter.java new file mode 100644 index 00000000..906df1b3 --- /dev/null +++ b/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenFilter.java @@ -0,0 +1,76 @@ +package com.umc.study.global.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.umc.study.global.apiPayload.ApiResponse; +import com.umc.study.global.apiPayload.code.GeneralErrorCode; +import com.umc.study.global.security.service.CustomUserDetailsService; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +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.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtTokenFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + private final ObjectMapper objectMapper; + private final CustomUserDetailsService customUserDetailsService; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + + // token 추출 + String accessToken = extractToken(request); + + // token이 없으면 다음 필터 체인 진행 + if(accessToken == null || accessToken.isBlank()) + filterChain.doFilter(request,response); + + try { + + // 토큰이 만료되었으면 다음 필터로 넘기지 않고 예외 + jwtTokenProvider.isExpired(accessToken); + + } catch(JwtException e) { + set401Response(response); + } + + Authentication authentication = jwtTokenProvider.getAuthentication(accessToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + + filterChain.doFilter(request,response); + + } + + private void set401Response(@org.jspecify.annotations.NonNull HttpServletResponse response) throws IOException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + ApiResponse errorResponse = ApiResponse.onFailure(GeneralErrorCode.UNAUTHORIZED, null); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } + + private String extractToken(HttpServletRequest request) { + + // HttpServletRequest에서 Authorization 필드 추출 + String bearerToken = request.getHeader("Authorization"); + + if(bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } else { + return null; + } + } +} From 6d88eb79ae5aaccf629e8352d78260b56ca39bd1 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 16:16:51 +0900 Subject: [PATCH 06/25] =?UTF-8?q?feat:=20=EB=88=84=EB=9D=BD=EB=90=9C=20Com?= =?UTF-8?q?ponent=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/umc/study/global/jwt/JwtTokenFilter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenFilter.java b/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenFilter.java index 906df1b3..89128c4a 100644 --- a/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenFilter.java +++ b/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenFilter.java @@ -4,7 +4,6 @@ import com.umc.study.global.apiPayload.ApiResponse; import com.umc.study.global.apiPayload.code.GeneralErrorCode; import com.umc.study.global.security.service.CustomUserDetailsService; -import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -14,10 +13,12 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +@Component @RequiredArgsConstructor public class JwtTokenFilter extends OncePerRequestFilter { From f38b7612928af0ee8af89cab7d8bac1c3cb3480b Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 16:28:31 +0900 Subject: [PATCH 07/25] =?UTF-8?q?fix:=20Date=EB=A1=9C=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=EB=90=98=EC=97=88=EB=8D=98=20=EA=B2=83=EB=93=A4=20Insatnce=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EA=B5=AC=EB=8F=99?= =?UTF-8?q?=ED=95=98=EA=B2=8C=EB=81=94=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/umc/study/global/jwt/JwtTokenProvider.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenProvider.java b/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenProvider.java index 91f8e628..4f753bf9 100644 --- a/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenProvider.java +++ b/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenProvider.java @@ -28,14 +28,17 @@ public class JwtTokenProvider { private final String secretKey; private Key key; + private final Duration expiration; // jwt signature를 final, env로 주입하는 생성자 public JwtTokenProvider( UserRepository userRepository, - @Value("${jwt.secret}") String secretKey + @Value("${jwt.secret}") String secretKey, + @Value("${jwt.expiration}") Long expiration ) { this.userRepository = userRepository; this.secretKey = secretKey; + this.expiration = Duration.ofMillis(expiration); } // signature -> Byte 배열 -> hmac sha 이용하여 Key 객체에 할당 @@ -77,7 +80,7 @@ public void init() { * * */ - public String generateToken(User user, Duration expiration) { + public String generateToken(User user) { // 현재 시각을 기준으로 만료일 설정 Instant now = Instant.now(); From bcb8101c06336ef03978714443f78f50bd38d1d3 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 16:29:11 +0900 Subject: [PATCH 08/25] =?UTF-8?q?feat:=20=EB=AA=85=EC=8B=9C=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=8F=BC=20=EA=B8=B0=EB=B0=98=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=EC=9D=84=20=EB=AC=B4=ED=9A=A8=ED=99=94,=20?= =?UTF-8?q?=EC=84=B8=EC=85=98=20=EB=AC=B4=ED=9A=A8=ED=99=94,=20=ED=8F=BC?= =?UTF-8?q?=20=EA=B8=B0=EB=B0=98=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D-=EC=9D=B8=EA=B0=80=20=ED=95=84=ED=84=B0=20=EC=9D=B4?= =?UTF-8?q?=EC=A0=84=EC=97=90=20=EC=BB=A4=EC=8A=A4=ED=85=80=ED=95=9C=20Jwt?= =?UTF-8?q?TokenFilter=EA=B0=80=20=EC=9E=91=EB=8F=99=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/global/config/SecurityConfig.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java b/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java index 83dfa31d..90b80e3d 100644 --- a/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java +++ b/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java @@ -1,8 +1,11 @@ package com.umc.study.global.config; import com.fasterxml.jackson.databind.ObjectMapper; +import com.umc.study.global.jwt.JwtTokenFilter; import com.umc.study.global.security.CustomEntryPoint; import com.umc.study.global.security.exception.CustomAccessDenied; +import com.umc.study.global.security.service.CustomUserDetailsService; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -11,9 +14,11 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -@EnableWebSecurity @Configuration +@EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { @Bean @@ -21,6 +26,9 @@ public ObjectMapper objectMapper() { return new ObjectMapper(); } + private final JwtTokenFilter jwtTokenFilter; + private final CustomUserDetailsService customUserDetailsService; + private final String[] allowUris = { "/api/swagger-ui/**", "/api/swagger-resources/**", @@ -37,9 +45,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomAccessDe .requestMatchers(allowUris).permitAll() .anyRequest().authenticated() ) - .formLogin(form -> form - .defaultSuccessUrl("/api/swagger-ui/index.html", true) - .permitAll()) + .formLogin(AbstractHttpConfigurer::disable) + .sessionManagement(AbstractHttpConfigurer::disable) + + .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class) + .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") From c79393a64945ed6258c5e98bedfc29876422b7a0 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 16:29:36 +0900 Subject: [PATCH 09/25] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD,=20=EC=9D=91=EB=8B=B5=EC=97=90=20=EA=B4=80?= =?UTF-8?q?=ED=95=9C=20DTO=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/umc/study/domain/user/web/dto/LoginReq.java | 12 ++++++++++++ .../com/umc/study/domain/user/web/dto/LoginRes.java | 6 ++++++ 2 files changed, 18 insertions(+) create mode 100644 Joonseok/src/main/java/com/umc/study/domain/user/web/dto/LoginReq.java create mode 100644 Joonseok/src/main/java/com/umc/study/domain/user/web/dto/LoginRes.java diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/web/dto/LoginReq.java b/Joonseok/src/main/java/com/umc/study/domain/user/web/dto/LoginReq.java new file mode 100644 index 00000000..cfdcc44a --- /dev/null +++ b/Joonseok/src/main/java/com/umc/study/domain/user/web/dto/LoginReq.java @@ -0,0 +1,12 @@ +package com.umc.study.domain.user.web.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class LoginReq { + + private final String loginId; + private final String password; +} diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/web/dto/LoginRes.java b/Joonseok/src/main/java/com/umc/study/domain/user/web/dto/LoginRes.java new file mode 100644 index 00000000..cdb64215 --- /dev/null +++ b/Joonseok/src/main/java/com/umc/study/domain/user/web/dto/LoginRes.java @@ -0,0 +1,6 @@ +package com.umc.study.domain.user.web.dto; + +public record LoginRes ( + String accessToken +) { +} From 136e0b9033e5ced9442d8b7917a8713389ac40fb Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 16:29:56 +0900 Subject: [PATCH 10/25] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EC=97=90=20=EB=8C=80=ED=95=B4=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=ED=95=A0=20POST=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=A7=84=EC=9E=85=EC=A0=90=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/domain/user/web/controller/UserController.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java b/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java index f3d78f78..6f7ab88a 100644 --- a/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java +++ b/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java @@ -4,6 +4,7 @@ import com.umc.study.domain.user.service.UserService; import com.umc.study.domain.user.web.dto.GetHomeRes; import com.umc.study.domain.user.web.dto.GetMyPageRes; +import com.umc.study.domain.user.web.dto.LoginReq; import com.umc.study.domain.user.web.dto.SignUpReq; import com.umc.study.global.apiPayload.ApiResponse; import jakarta.validation.Valid; @@ -35,6 +36,13 @@ public ResponseEntity> signUp( .body(ApiResponse.onComplete(UserSuccessCode.USER_SIGN_UP_CREATED, null)); } + @PostMapping("/auth/login") + public ResponseEntity> login( + @Valid @RequestBody LoginReq request + ) { + userService.loginUser(request); + } + @PostMapping("/my/pref") public ResponseEntity> createMyPrefFood( @Valid @RequestBody Object request From 8306c8e94722cbf544838f08ba2ecb0dccf4364a Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 16:30:35 +0900 Subject: [PATCH 11/25] =?UTF-8?q?feat:=20=EC=9A=94=EC=B2=AD=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=93=A4=EC=96=B4=EC=98=A8=20email=EC=9D=84=20User?= =?UTF-8?q?Repository=EC=97=90=EC=84=9C=20=EC=B0=BE=EA=B3=A0=20=EC=97=86?= =?UTF-8?q?=EB=8B=A4=EB=A9=B4=20=EC=98=88=EC=99=B8,=20=EC=95=94=ED=98=B8?= =?UTF-8?q?=ED=99=94=EB=90=9C=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8?= =?UTF-8?q?=EC=99=80=20=EB=A7=9E=EC=A7=80=20=EC=95=8A=EB=8B=A4=EB=A9=B4=20?= =?UTF-8?q?=EC=98=88=EC=99=B8,=20=EB=91=98=20=EB=8B=A4=20=ED=86=B5?= =?UTF-8?q?=EA=B3=BC=ED=96=88=EB=8B=A4=EB=A9=B4=20JWT=EB=A5=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=98=EC=97=AC=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/service/UserService.java | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/service/UserService.java b/Joonseok/src/main/java/com/umc/study/domain/user/service/UserService.java index 8ca92a0a..3ff116ec 100644 --- a/Joonseok/src/main/java/com/umc/study/domain/user/service/UserService.java +++ b/Joonseok/src/main/java/com/umc/study/domain/user/service/UserService.java @@ -10,10 +10,9 @@ import com.umc.study.domain.user.enums.Role; import com.umc.study.domain.user.exception.UserNotFoundException; import com.umc.study.domain.user.repository.UserRepository; -import com.umc.study.domain.user.web.dto.GetHomeRes; -import com.umc.study.domain.user.web.dto.GetMyPageRes; -import com.umc.study.domain.user.web.dto.SignInReq; -import com.umc.study.domain.user.web.dto.SignUpReq; +import com.umc.study.domain.user.web.dto.*; +import com.umc.study.global.jwt.JwtTokenProvider; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -32,6 +31,7 @@ public class UserService { private final MissionRepository missionRepository; private final RestaurantRepository restaurantRepository; private final PasswordEncoder encoder; + private final JwtTokenProvider jwtTokenProvider; @Transactional public void saveUser(SignUpReq request) { @@ -120,4 +120,18 @@ public GetHomeRes getHomepage(Long userId, int page, int size) { ); } + public LoginRes loginUser(LoginReq request) { + + // 1. find user by login id + User found = userRepository.findByEmail(request.getLoginId()) + .orElseThrow(UserNotFoundException::new); + + // 2. valid request password equals encoded db password + if(!encoder.matches(request.getPassword(), found.getPassword())) + throw new InvalidPasswordException(); + + String accessToken = jwtTokenProvider.generateToken(found); + + return new LoginRes(accessToken); + } } From bb66d011311057dc5fab623b363b41723f7d11f3 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 16:33:07 +0900 Subject: [PATCH 12/25] =?UTF-8?q?feat:=20User=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=EC=97=90=20=EC=84=B1=EA=B3=B5=ED=96=88=EC=9D=84=20?= =?UTF-8?q?=EB=95=8C=20=EB=B0=98=ED=99=98=ED=95=A0=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C,=20=EC=BD=94=EB=93=9C,=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20UserSuccessCode=EC=97=90=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/exception/code/UserSuccessCode.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserSuccessCode.java b/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserSuccessCode.java index 85042040..9f5044c9 100644 --- a/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserSuccessCode.java +++ b/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserSuccessCode.java @@ -14,36 +14,40 @@ public enum UserSuccessCode implements BaseResponseCode { "USER201_1", "회원가입에 성공했습니다." ), + USER_LOGIN_OK( + HttpStatus.OK, + "USER_200_1" + ), USER_PREF_FOOD_CREATED( HttpStatus.CREATED, "USER201_2", "회원님의 선호하는 음식 종류 저장에 성공했습니다." ), HOME_OK(HttpStatus.OK, - "USER200_1", + "USER200_2", "홈 화면 조회에 성공했습니다."), MY_PAGE_OK(HttpStatus.OK, - "USER200_2", + "USER200_3", "마이 페이지 조회에 성공했습니다."), AUTH_PHONE_NUM_OK(HttpStatus.OK, - "USER200_3", + "USER200_4", "전화번호 인증에 성공했습니다."), MY_ALERT_OK(HttpStatus.OK, - "USER200_4", + "USER200_5", "나의 알림 조회에 성공했습니다."), MY_ALERT_SETTING_OK( HttpStatus.OK, - "USER200_5", + "USER200_6", "나의 알림 설정 조회에 성공했습니다." ), MY_ALERT_SETTING_UPDATE_OK( HttpStatus.OK, - "USER200_6", + "USER200_7", "나의 알림 설정 수정에 성공했습니다." ), MY_ACCOUNT_DELETE_OK( HttpStatus.OK, - "USER200_7", + "USER200_8", "내 계정 삭제에 성공했습니다." ); From 749a104f883136990b78cabf91114bdfb907723b Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 16:33:30 +0900 Subject: [PATCH 13/25] =?UTF-8?q?feat:=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EA=B0=80=20=EB=B0=98=ED=99=98=ED=95=9C=20jwt=EB=A5=BC=20Respon?= =?UTF-8?q?seEntity=EB=A1=9C=20=EB=9E=98=ED=95=91=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/web/controller/UserController.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java b/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java index 6f7ab88a..ad9a603d 100644 --- a/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java +++ b/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java @@ -2,10 +2,7 @@ import com.umc.study.domain.user.exception.code.UserSuccessCode; import com.umc.study.domain.user.service.UserService; -import com.umc.study.domain.user.web.dto.GetHomeRes; -import com.umc.study.domain.user.web.dto.GetMyPageRes; -import com.umc.study.domain.user.web.dto.LoginReq; -import com.umc.study.domain.user.web.dto.SignUpReq; +import com.umc.study.domain.user.web.dto.*; import com.umc.study.global.apiPayload.ApiResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; @@ -40,7 +37,11 @@ public ResponseEntity> signUp( public ResponseEntity> login( @Valid @RequestBody LoginReq request ) { - userService.loginUser(request); + LoginRes response = userService.loginUser(request); + + return ResponseEntity + .status(UserSuccessCode.USER_LOGIN_OK.getStatus()) + .body(ApiResponse.onComplete(UserSuccessCode.USER_LOGIN_OK, response)); } @PostMapping("/my/pref") From e4ad0c1ea3104c4db23c62845c0692dbf768cd14 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 17:52:04 +0900 Subject: [PATCH 14/25] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=EA=B0=80=20=EC=98=AC=EB=B0=94=EB=A5=B4=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EC=9D=84=20=EB=95=8C=20=EB=8D=98=EC=A7=88=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc/study/domain/user/exception/code/UserErrorCode.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserErrorCode.java b/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserErrorCode.java index 661926d0..1c72e26b 100644 --- a/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserErrorCode.java +++ b/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserErrorCode.java @@ -8,7 +8,8 @@ @Getter @AllArgsConstructor public enum UserErrorCode implements BaseResponseCode { - USER_NOT_FOUND("USER_NOT_FOUND_404", HttpStatus.NOT_FOUND, "해당 멤버를 찾는데 실패했습니다."); + USER_NOT_FOUND("USER_404_1", HttpStatus.NOT_FOUND, "해당 멤버를 찾는데 실패했습니다."), + PASSWORD_INVALID("USER_400_1", HttpStatus.BAD_REQUEST, "비밀번호가 잘못되었습니다."); private final String code; private final HttpStatus status; From 8163bfe1c254e23ea787557abd7a5ce2465227b8 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 17:52:21 +0900 Subject: [PATCH 15/25] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=EA=B0=80=20=EC=98=AC=EB=B0=94=EB=A5=B4=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EC=9D=84=20=EB=95=8C=20=EB=8D=98=EC=A7=88=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EA=B0=9D=EC=B2=B4=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/exception/code/InvalidPasswordException.java | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Joonseok/src/main/java/com/umc/study/domain/user/exception/code/InvalidPasswordException.java diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/InvalidPasswordException.java b/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/InvalidPasswordException.java new file mode 100644 index 00000000..b4333687 --- /dev/null +++ b/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/InvalidPasswordException.java @@ -0,0 +1,9 @@ +package com.umc.study.domain.user.exception.code; + +import com.umc.study.global.apiPayload.exception.BaseException; + +public class InvalidPasswordException extends BaseException { + public InvalidPasswordException() { + super(UserErrorCode.PASSWORD_INVALID); + } +} From 4e3a7d9bdd8ef37a61d64669274a632959a7d36a Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 17:52:36 +0900 Subject: [PATCH 16/25] =?UTF-8?q?docs:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=EA=B0=80=20=EC=98=AC=EB=B0=94=EB=A5=B4=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EC=9D=84=20=EB=95=8C=20=EB=8D=98=EC=A7=88=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EA=B0=9D=EC=B2=B4=EB=A5=BC=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=9E=84=ED=8F=AC=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/umc/study/domain/user/service/UserService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/service/UserService.java b/Joonseok/src/main/java/com/umc/study/domain/user/service/UserService.java index 3ff116ec..631de79d 100644 --- a/Joonseok/src/main/java/com/umc/study/domain/user/service/UserService.java +++ b/Joonseok/src/main/java/com/umc/study/domain/user/service/UserService.java @@ -9,6 +9,7 @@ import com.umc.study.domain.user.entity.User; import com.umc.study.domain.user.enums.Role; import com.umc.study.domain.user.exception.UserNotFoundException; +import com.umc.study.domain.user.exception.code.InvalidPasswordException; import com.umc.study.domain.user.repository.UserRepository; import com.umc.study.domain.user.web.dto.*; import com.umc.study.global.jwt.JwtTokenProvider; From 5656dc4df95051580eececb938947074336b25c9 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 17:56:20 +0900 Subject: [PATCH 17/25] =?UTF-8?q?fix:=20UserSuccessCode.USER=5FLOGIN=5FOK?= =?UTF-8?q?=EC=97=90=20message=20=ED=95=84=EB=93=9C=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc/study/domain/user/exception/code/UserSuccessCode.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserSuccessCode.java b/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserSuccessCode.java index 9f5044c9..b01f8c48 100644 --- a/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserSuccessCode.java +++ b/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserSuccessCode.java @@ -16,7 +16,8 @@ public enum UserSuccessCode implements BaseResponseCode { ), USER_LOGIN_OK( HttpStatus.OK, - "USER_200_1" + "USER_200_1", + "로그인에 성공했습니다." ), USER_PREF_FOOD_CREATED( HttpStatus.CREATED, From e2c9f41f2e20c183eb1ecbf21121cfb54efaf20f Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 18:27:17 +0900 Subject: [PATCH 18/25] =?UTF-8?q?fix:=20ObjectMapper=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EC=B5=9C=EC=8B=A0=ED=99=94=EB=90=9C=20Jso?= =?UTF-8?q?nMapper=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc/study/global/config/SecurityConfig.java | 16 ++++------------ .../umc/study/global/config/SwaggerConfig.java | 2 +- .../com/umc/study/global/jwt/JwtTokenFilter.java | 12 ++++++------ .../study/global/security/CustomEntryPoint.java | 6 +++--- .../security/exception/CustomAccessDenied.java | 6 +++--- 5 files changed, 17 insertions(+), 25 deletions(-) diff --git a/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java b/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java index 90b80e3d..6c8d0d94 100644 --- a/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java +++ b/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java @@ -1,10 +1,8 @@ package com.umc.study.global.config; -import com.fasterxml.jackson.databind.ObjectMapper; import com.umc.study.global.jwt.JwtTokenFilter; import com.umc.study.global.security.CustomEntryPoint; import com.umc.study.global.security.exception.CustomAccessDenied; -import com.umc.study.global.security.service.CustomUserDetailsService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -21,19 +19,13 @@ @RequiredArgsConstructor public class SecurityConfig { - @Bean - public ObjectMapper objectMapper() { - return new ObjectMapper(); - } - private final JwtTokenFilter jwtTokenFilter; - private final CustomUserDetailsService customUserDetailsService; private final String[] allowUris = { - "/api/swagger-ui/**", - "/api/swagger-resources/**", - "/api/v3/api-docs/**", - "/api/auth/**" // sign-up, login request allow + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + "/auth/**" // sign-up, login request allow }; @Bean diff --git a/Joonseok/src/main/java/com/umc/study/global/config/SwaggerConfig.java b/Joonseok/src/main/java/com/umc/study/global/config/SwaggerConfig.java index 4dd03962..86499c43 100644 --- a/Joonseok/src/main/java/com/umc/study/global/config/SwaggerConfig.java +++ b/Joonseok/src/main/java/com/umc/study/global/config/SwaggerConfig.java @@ -49,7 +49,7 @@ public OpenAPI swagger() { // OpenAPI 객체 조립: 기본 정보 + 서버 URL + 보안 설정 return new OpenAPI() .info(info) - .addServersItem(new Server().url("/")) + .addServersItem(new Server().url("/api")) .addSecurityItem(securityRequirement) .components(components); } diff --git a/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenFilter.java b/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenFilter.java index 89128c4a..69c7b15a 100644 --- a/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenFilter.java +++ b/Joonseok/src/main/java/com/umc/study/global/jwt/JwtTokenFilter.java @@ -1,9 +1,7 @@ package com.umc.study.global.jwt; -import com.fasterxml.jackson.databind.ObjectMapper; import com.umc.study.global.apiPayload.ApiResponse; import com.umc.study.global.apiPayload.code.GeneralErrorCode; -import com.umc.study.global.security.service.CustomUserDetailsService; import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -15,6 +13,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; +import tools.jackson.databind.json.JsonMapper; import java.io.IOException; @@ -23,8 +22,7 @@ public class JwtTokenFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; - private final ObjectMapper objectMapper; - private final CustomUserDetailsService customUserDetailsService; + private final JsonMapper jsonMapper; @Override protected void doFilterInternal( @@ -37,8 +35,10 @@ protected void doFilterInternal( String accessToken = extractToken(request); // token이 없으면 다음 필터 체인 진행 - if(accessToken == null || accessToken.isBlank()) + if(accessToken == null || accessToken.isBlank()) { filterChain.doFilter(request,response); + return; + } try { @@ -60,7 +60,7 @@ private void set401Response(@org.jspecify.annotations.NonNull HttpServletRespons response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); ApiResponse errorResponse = ApiResponse.onFailure(GeneralErrorCode.UNAUTHORIZED, null); - response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + response.getWriter().write(jsonMapper.writeValueAsString(errorResponse)); } private String extractToken(HttpServletRequest request) { diff --git a/Joonseok/src/main/java/com/umc/study/global/security/CustomEntryPoint.java b/Joonseok/src/main/java/com/umc/study/global/security/CustomEntryPoint.java index 8922a968..0c9ab5fb 100644 --- a/Joonseok/src/main/java/com/umc/study/global/security/CustomEntryPoint.java +++ b/Joonseok/src/main/java/com/umc/study/global/security/CustomEntryPoint.java @@ -1,6 +1,5 @@ package com.umc.study.global.security; -import com.fasterxml.jackson.databind.ObjectMapper; import com.umc.study.global.apiPayload.ApiResponse; import com.umc.study.global.apiPayload.code.BaseResponseCode; import com.umc.study.global.apiPayload.code.GeneralErrorCode; @@ -11,6 +10,7 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; +import tools.jackson.databind.json.JsonMapper; import java.io.IOException; @@ -18,7 +18,7 @@ @RequiredArgsConstructor public class CustomEntryPoint implements AuthenticationEntryPoint { - private final ObjectMapper objectMapper; + private final JsonMapper jsonMApper; @Override public void commence( @@ -34,6 +34,6 @@ public void commence( ApiResponse errorResponse = ApiResponse.onFailure(errorCode, null); - response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + response.getWriter().write(jsonMApper.writeValueAsString(errorResponse)); } } diff --git a/Joonseok/src/main/java/com/umc/study/global/security/exception/CustomAccessDenied.java b/Joonseok/src/main/java/com/umc/study/global/security/exception/CustomAccessDenied.java index e8fb2807..37f802c3 100644 --- a/Joonseok/src/main/java/com/umc/study/global/security/exception/CustomAccessDenied.java +++ b/Joonseok/src/main/java/com/umc/study/global/security/exception/CustomAccessDenied.java @@ -1,6 +1,5 @@ package com.umc.study.global.security.exception; -import com.fasterxml.jackson.databind.ObjectMapper; import com.umc.study.global.apiPayload.ApiResponse; import com.umc.study.global.apiPayload.code.BaseResponseCode; import com.umc.study.global.apiPayload.code.GeneralErrorCode; @@ -11,6 +10,7 @@ import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; +import tools.jackson.databind.json.JsonMapper; import java.io.IOException; @@ -18,7 +18,7 @@ @RequiredArgsConstructor public class CustomAccessDenied implements AccessDeniedHandler { - private final ObjectMapper objectMapper; + private final JsonMapper jsonMapper; @Override public void handle( @@ -34,6 +34,6 @@ public void handle( ApiResponse errorResponse = ApiResponse.onFailure(errorCode, null); - response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + response.getWriter().write(jsonMapper.writeValueAsString(errorResponse)); } } From cf6b977bf047ca1cc13b7c19a1734a407a770182 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 18:27:57 +0900 Subject: [PATCH 19/25] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EB=B0=94=EB=94=94=EC=97=90=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=EB=90=9C=20=EA=B2=80=EC=A6=9D=20=EC=A1=B0=EA=B1=B4?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/umc/study/domain/user/web/dto/LoginReq.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/web/dto/LoginReq.java b/Joonseok/src/main/java/com/umc/study/domain/user/web/dto/LoginReq.java index cfdcc44a..e267bae1 100644 --- a/Joonseok/src/main/java/com/umc/study/domain/user/web/dto/LoginReq.java +++ b/Joonseok/src/main/java/com/umc/study/domain/user/web/dto/LoginReq.java @@ -1,5 +1,7 @@ package com.umc.study.domain.user.web.dto; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -7,6 +9,9 @@ @RequiredArgsConstructor public class LoginReq { + @Email(message = "이메일 형식이 아닙니다.") private final String loginId; + + @NotBlank(message = "비밀번호 필드는 비어있을 수 없습니다.") private final String password; } From 5848e6df2f6476b3051fce6df7faf942661706df Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 18:37:17 +0900 Subject: [PATCH 20/25] =?UTF-8?q?fix:=20userId=EB=A5=BC=20PathVariable?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=9B=EC=95=84=EC=84=9C=20=ED=99=88=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80,=20=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8D=98=20?= =?UTF-8?q?=EA=B2=83=EC=9D=84=20SecurityContextHolder=EC=97=90=EC=84=9C=20?= =?UTF-8?q?UserDetails=EC=9D=98=20=EA=B5=AC=ED=98=84=EC=B2=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=A3=BC=EC=9E=85=EB=B0=9B=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/web/controller/UserController.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java b/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java index ad9a603d..9a6b2f04 100644 --- a/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java +++ b/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java @@ -4,12 +4,14 @@ import com.umc.study.domain.user.service.UserService; import com.umc.study.domain.user.web.dto.*; import com.umc.study.global.apiPayload.ApiResponse; +import com.umc.study.global.security.entity.CustomUserDetails; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -55,14 +57,15 @@ public ResponseEntity> createMyPrefFood( .body(ApiResponse.onComplete(UserSuccessCode.USER_PREF_FOOD_CREATED, null)); } - @GetMapping("/home/{userId}") + @GetMapping("/home") public ResponseEntity> getMain( - // TODO JWT에서 유저를 추출해서 주입 - @PathVariable Long userId, + @AuthenticationPrincipal CustomUserDetails user, @RequestParam @Min(message = "page값은 1 이상이어야 합니다.", value = 1) Integer page, @RequestParam @Min(message = "size값은 1 이상이어야 합니다.", value = 1) @Max(message = "size값은 10을 넘길 수 없습니다.", value = 10) Integer size ) { + Long userId = user.getUser().getId(); + GetHomeRes response = userService.getHomepage(userId, page, size); return ResponseEntity @@ -73,11 +76,12 @@ public ResponseEntity> getMain( )); } - @GetMapping("/my/{userId}") + @GetMapping("/my") public ResponseEntity> getMyPage( - // JWT Security Holder에서 추출 - @PathVariable Long userId + @AuthenticationPrincipal CustomUserDetails userDetails ) { + Long userId = userDetails.getUser().getId(); + // 서비스 메소드 호출 GetMyPageRes response = userService.getMyPage(userId); From 56690cf2ed93f590c250127ede75971b03572c34 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 25 May 2026 18:50:07 +0900 Subject: [PATCH 21/25] =?UTF-8?q?docs:=20spring=20oauth=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Joonseok/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Joonseok/build.gradle b/Joonseok/build.gradle index 6b6dd270..85a105bf 100644 --- a/Joonseok/build.gradle +++ b/Joonseok/build.gradle @@ -34,6 +34,9 @@ dependencies { // JWT implementation 'io.jsonwebtoken:jjwt:0.13.0' + // kakao oauth + implementation 'org.springframework.boot:spring-boot-starter-security-oauth2-client' + // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.3' From 6f7866c17993dcbb3fa03e5b4a2eec43f615745b Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Tue, 26 May 2026 09:45:38 +0900 Subject: [PATCH 22/25] =?UTF-8?q?feat:=20FIDO=EC=9A=A9=20WebAuthn=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Joonseok/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Joonseok/build.gradle b/Joonseok/build.gradle index 85a105bf..7708aa28 100644 --- a/Joonseok/build.gradle +++ b/Joonseok/build.gradle @@ -31,6 +31,9 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testAnnotationProcessor 'org.projectlombok:lombok' + // FIDO + implementation 'org.springframework.security:spring-security-webauthn' + // JWT implementation 'io.jsonwebtoken:jjwt:0.13.0' From 5f36342fe76055bb1a6cbb5e8314a6d51c383f92 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Tue, 26 May 2026 09:51:15 +0900 Subject: [PATCH 23/25] =?UTF-8?q?feat:=20WebAuthn=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EC=9D=84=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/umc/study/global/config/SecurityConfig.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java b/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java index 6c8d0d94..bc43701b 100644 --- a/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java +++ b/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java @@ -9,6 +9,7 @@ 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.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @@ -38,7 +39,16 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomAccessDe .anyRequest().authenticated() ) .formLogin(AbstractHttpConfigurer::disable) - .sessionManagement(AbstractHttpConfigurer::disable) + .sessionManagement( + session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + + .webAuthn(webAuthn -> + webAuthn + .rpId("localhost") + .allowedOrigins("http://localhost:8080") + .disableDefaultRegistrationPage(true)) .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class) From 45ff0bcd202a1b9bf9bb2d1ced8974eaa5884d94 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Tue, 26 May 2026 13:10:23 +0900 Subject: [PATCH 24/25] =?UTF-8?q?feat:=20FIDO=EC=97=90=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=A0=20User=20=EA=B4=80=EB=A0=A8=20=EC=97=94?= =?UTF-8?q?=ED=84=B0=ED=8B=B0=20=EC=9E=91=EC=84=B1,=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/TestController.java | 15 ++++++ .../study/global/entity/UserCredential.java | 53 +++++++++++++++++++ .../umc/study/global/entity/UserEntity.java | 20 +++++++ 3 files changed, 88 insertions(+) create mode 100644 Joonseok/src/main/java/com/umc/study/domain/webauthn/web/controller/TestController.java create mode 100644 Joonseok/src/main/java/com/umc/study/global/entity/UserCredential.java create mode 100644 Joonseok/src/main/java/com/umc/study/global/entity/UserEntity.java diff --git a/Joonseok/src/main/java/com/umc/study/domain/webauthn/web/controller/TestController.java b/Joonseok/src/main/java/com/umc/study/domain/webauthn/web/controller/TestController.java new file mode 100644 index 00000000..4cc5df9f --- /dev/null +++ b/Joonseok/src/main/java/com/umc/study/domain/webauthn/web/controller/TestController.java @@ -0,0 +1,15 @@ +package com.umc.study.domain.webauthn.web.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/passkey") +public class TestController { + + @GetMapping("/test") + public String test() { + return "/auth/index.html"; + } +} diff --git a/Joonseok/src/main/java/com/umc/study/global/entity/UserCredential.java b/Joonseok/src/main/java/com/umc/study/global/entity/UserCredential.java new file mode 100644 index 00000000..72dc8d77 --- /dev/null +++ b/Joonseok/src/main/java/com/umc/study/global/entity/UserCredential.java @@ -0,0 +1,53 @@ +package com.umc.study.global.entity; + +import com.umc.study.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserCredential { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "credential_id") + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private UserEntity userEntity; + + @Lob + @Column(nullable = false) + private byte[] publicKey; + + private Long signatureCount; + + private boolean uvInitialized; + + @Column(nullable = false) + private boolean backupEligible; + + private String authenticatorTransports; + + private String publicKeyCredentialType; + + @Column(nullable = false) + private boolean backupState; + + private byte[] attestationObject; + + private byte[] attestationClientDataJson; + + @Column(updatable =false) + private LocalDateTime created; + + private LocalDateTime lastUsed; + + @Column(nullable = false) + private String label; +} diff --git a/Joonseok/src/main/java/com/umc/study/global/entity/UserEntity.java b/Joonseok/src/main/java/com/umc/study/global/entity/UserEntity.java new file mode 100644 index 00000000..0688f4d2 --- /dev/null +++ b/Joonseok/src/main/java/com/umc/study/global/entity/UserEntity.java @@ -0,0 +1,20 @@ +package com.umc.study.global.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserEntity { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String username; + + private String displayName; +} From ededd4569254ec3a7a6d4a8745914dd5c8e8e05c Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Tue, 26 May 2026 14:31:23 +0900 Subject: [PATCH 25/25] =?UTF-8?q?docs:=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=ED=95=B5=EC=8B=AC=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EB=A7=88?= =?UTF-8?q?=ED=81=AC=EB=8B=A4=EC=9A=B4=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Joonseok/keyword_summary/ch09.md | 144 +++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 Joonseok/keyword_summary/ch09.md diff --git a/Joonseok/keyword_summary/ch09.md b/Joonseok/keyword_summary/ch09.md new file mode 100644 index 00000000..77d43e04 --- /dev/null +++ b/Joonseok/keyword_summary/ch09.md @@ -0,0 +1,144 @@ +- 세션과 토큰의 차이는? + + + + 인증을 처리할 때, 서버가 서비스 플로우를 따라서 사용자의 정보를 기억하는 방식이 `stateful` , 요청이 멱등하게, 사용자가 누군지 모르는 상태에서 시작하는 것이 `stateless` 이라 했습니다. 세션과 토큰은 두 방식의 대표 구현 방법입니다. + + ### Session + + 서버가 사용자의 정보를 여러 요청에 걸쳐 기억하는 방식으로, `SessionManager` 를 이용하여 인-메모리에서 사용자의 정보를 기억하는 간이 저장소를 만듭니다. 혹은, DB를 사용할 수 있습니다. + + 인증 여부를 확인하기 위해 클라이언트는 Cookie 등을 이용하여 Session의 고유값을 서버에 전송합니다. 그리고, 서버는 인-메모리 / DB에서 해당 세션이 유효한지 검색한 후에, 유효하다면 인가 처리를 진행하고, 유효하지 않다면 재인증을 유도하게끔 처리합니다. + + - `loginId` , `password` 입력 후 서버로 전송 + - 서버는 자격 증명을 검증하고 세션을 생성. 그리고 쿠키 형태로 사용자 브라우저로 전달 + - 사용자가 요청할 때마다 브라우저는 세션 ID를 서버로 다시 전송 + - 서버는 세션 ID를 저장소에서 조회하고, 유효 여부를 체크한 후 후속 절차를 실행 + + **장점** + + - 악의적인 공격에 의해, 인증 수단이 탈취된 경우에 DB, 저장소에서 해당 세션을 만료 처리하면 방어할 수 있으므로, 탈취 공격에 대처 가능 + - 세션은 클라이언트 측에서 저장하지 않음 → XSS 공격 예방 + - 세션 그 자체로는 전혀 데이터를 가지지 않으므로, 서버 외에 세션을 참조하는 경우, 아무런 정보를 얻지 못함 + + **단점** + + - 기능이 확장되는 경우(예. 카카오톡, 카카오맵, 카카오택시, …)에 세션 관리 저장소를 단일화해야 하는 문제 발생. 장애 발생 시 대처 어려움 → 애플리케이션 확장성 저하 + - 세션을 조회할 때 컴퓨팅 리소스를 사용 → 사용자가 많아지는 경우, 인증-인가 오버헤드 발생 + + ### Token + + 토큰은 매 요청에 대해 사용자를 모르는 채로 진행하므로 stateless 서버를 구현할 때 사용되며, 대표적으로는 `JWT(JSON Web Token)`을 사용합니다. JWT는 `header.payload.signature`로 구성되어 있습니다. + + - `header` : 토큰에 사용된 해싱 알고리즘을 작성합니다. + - `payload` : 사용자 정보와 토큰 정보를 작성합니다. + - `subject`: 사용자를 식별할 수 있는 값을 넣습니다. (예. DB_USER 중 `UNIQUE` 속성 필드) + - `claim` : 토큰에 대한 간단한 정보와 사용자의 정보를 저장합니다. username, email, role, issuedAt, expiredAt, … + - `signature` : 서버에서 발행하는 해싱값이 변조되지 않았음을 검증하는 값입니다. 공격자가 쉽게 탈취할 수 없어야 하므로 이것 역시 해싱값을 사용합니다. 이상적으로는 `HSXXX` 알고리즘 등으로 256bit 이상 생성합니다. + + JWT의 작동 방식은 다음과 같습니다. + + - 클라이언트에서 자격 증명을 생성하여 인증 요청 + - 서버에서 자격 증명을 검증하고, `사용자의 정보가 포함된` JWT를 생성하고, 비밀 키(signature)로 해싱 + - 생성된 JWT를 클라이언트 측에 전송. RFC 6749 문서에 따르면, ResponseBody에 추가하는 것이 옳다고 서술되어 있다.(별다른 보안 문제가 없다면, 추후로도 응답 바디로 반환해줄 것 같다.) + - 이후 클라이언트는 인증-인가가 필요한 모든 API 요청에 대해 Header - Authorization에 해당 JWT를 추가하여 전송한다. + + **장점** + + - stateless : 애플리케이션 확장을 진행할 때, 인증-인가 절차가 종속된 컴포넌트(인-메모리 세션 매니저 / DB)가 존재하지 않아 확장성이 세션에 비해 높다. + - 여러 도메인에 걸쳐 인증-인가를 통합하기 편리하다 + + **단점** + + - 토큰 만료를 서버 측에서 처리하기 어렵다. + - JWT는 클라이언트 측의 로컬 스토리지에 저장된 경우, XSS 취약점이 될 수 있다. + - HTTPS 통신이 강제된다.(표준이 HTTPS이므로, 큰 단점은 아니다.) + + 여러 단점이 있지만, JWT의 가장 큰 단점은 발행된 토큰의 인증을 서버 측에서 임의 만료시킬 수 없다는게 문제입니다. 따라서, DB에 1회 조회하는 오버헤드가 발생하더라도, 해당 요청을 처리하는 동안은 사용자의 정보를 조회할 수 있게 하이브리드 방식을 채택하는 경우도 많습니다. + +- 엑세스 토큰과 리프레시 토큰이란? + + + + 참고 자료 : [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) + + ### AccessToken + + 액세스 토큰은 보호된 리소스에 접근할 때 사용되는 자격 증명입니다. 액세스 토큰은 클라이언트 측에서 이해할 수 없게 암호화 처리해야 합니다. 토큰은 `접근 권한` , `유효한 기간` 등의 정보를 담습니다. + + 토큰은 인증을 검색하는 데 사용되는 식별자를 나타낼 수도 있습니다.(JWT의 `subject`) + + 권한 부여는 단일 사용자 이름 및 비밀번호를 사용하여 구성합니다.(`loginId`, `password` ) + 서버는 토큰을 해독하고, 인가 처리를 진행합니다. + + ### RefreshToken + + 리프레시 토큰은 액세스 토큰을 얻는데 데 사용되는 자격 증명입니다. 리프레시 토큰은 인증 서버에서 클라이언트에게 발급됩니다. + + 현재 액세스 토큰이 만료되었을 때, 리프레시 토큰이 유효하다면 인증 서버로 전달하여 새로운 액세스 토큰을 얻을 수 있습니다. + + 대개 리프레시 토큰보다 액세스 토큰의 유효 기간이 더 짧으며, 액세스 토큰을 발급한다고 하더라도, 리프레시 토큰의 발급 여부는 필수 사항이 아닙니다. + + --- + + ### Spring과 리프레시 토큰 구현 + + 리프레시 토큰은 마치 세션처럼, 저장소에 일정 시간동안 저장되어 있습니다. 그리고, 정해진 시간이 지나면 만료됩니다. 편리하게 구현하기 위해서는 Redis를 사용하는 경우도 왕왕 있습니다. + +- OAuth 1.0과 OAuth 2.0의 차이는? + + ## OAuth + + OAuth는 클라이언트가 서버 리소스에 접근할 수 있는 방법을 제공하는 프로토콜의 일종입니다. 일반적으로는 ID와 Password를 사용하여 접근 권한을 획득합니다. + + 기존의 클라이언트-서버 인증 모델에서 클라이언트는 모놀리식 아키텍처로, 다른 서버의 리소스에 접근할 수 없었습니다. + + 하지만, 현대에 들어 자주 사용되는 애플리케이션 속 나의 리소스에 접근하려는 다른 애플리케이션이 생길 수 있게 되었습니다. OAuth는 이 두 서비스 사이에서 권한을 조정합니다. 이로써 카카오톡의 특정 리소스에만 제한적으로 접근할 수 있는 토큰을 갖고 thrid-party service에서 카카오톡 리소스에 접근할 수 있게 되는 것입니다. + + ### OAuth 1.0 + + **프로토콜에서 사용되는 매개체** + + - 클라이언트 : OAuth 인증 요청을 보내는 HTTP 클라이언트 + - 서버 - OAuth 인증 요청을 받는 HTTP 서버 + - 리소스 보유자 - 자격 증명으로 서버에 인증하여 보호된 리소스를 통제하는 주체 + + **요청 인증 방식** + + OAuth 1.0은 모든 요청에 서명합니다. 매 요청마다 서명된 문자열을 이용하여 다음 요소를 정규화한 뒤, 서명을 계산합니다. + + - HTTP method + - base string URI (Scheme, host) + - 요청 파라미터들 + + 서명 자체가 무결성과 진위성을 보장하므로, TLS(HTTPS 프로토콜용 보안 통신 중간 전달자?)가 없어도 위변조를 막을 수 있습니다. 하지만, 기밀성을 보장할 수 없고 도청자가 요청 내용에 완전히 접근할 수 있습니다. + + ### OAuth 2.0 + + **프로토콜에서 사용되는 매개체** + + - 리소스 보유자 - 보호된 리소스에 대해 접근을 허락하는 주체 + - 리소스 제공자 - 보호된 리소스를 호스팅하며, 액세스 토큰으로 요청에 응답하는 서버 + - 클라이언 - 리소스 보유자를 대신해 보호된 리소스를 요청하는 애플리케이션 + - 인가 서버 - 리소스 보유자를 인증하고, 권한을 얻은 뒤 클라이언트에게 액세스 토큰을 발행하는 서버 + + **요청 인증 방식** + + 요청별 서명을 완전히 제거한 대신, 다음을 추가했습니다. + + - 토큰은 암호화된 문자열 + - 토큰을 가진 자는 누구나 사용할 수 있음 + - TLS가 필수(HTTPS 통신) + + > 가장 큰 차이점은 HTTPS 프로토콜을 강제하는 여부 +> \ No newline at end of file