From a11171b8a36dd2625eb1ffc98a1ab838891bfb3e Mon Sep 17 00:00:00 2001 From: wooki Date: Wed, 10 Sep 2025 22:37:34 +0900 Subject: [PATCH 01/37] =?UTF-8?q?feat(#20):=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=EC=97=90=EC=84=9C=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=ED=95=98=EB=8A=94=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=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 --- .../global/jwt/JwtTokenResolver.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java new file mode 100644 index 0000000..794b301 --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java @@ -0,0 +1,21 @@ +package kr.ac.ks.cs_web_back.global.jwt; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +public class JwtTokenResolver { + public static final String AUTHORIZATION_HEADER = "Authorization"; + public static final String HEADER_PREFIX = "Bearer "; + + public String resolveToken(HttpServletRequest request) { + String token = request.getHeader(AUTHORIZATION_HEADER); + if (StringUtils.hasText(token) && token.startsWith(HEADER_PREFIX)) { + return token.substring(HEADER_PREFIX.length()); + } + + return null; + } + +} From 1e90f64f52f0484748eee9b12a7e0ce5695a99a4 Mon Sep 17 00:00:00 2001 From: wooki Date: Wed, 10 Sep 2025 22:38:28 +0900 Subject: [PATCH 02/37] =?UTF-8?q?feat(#20):=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C,=20=EA=B2=80=EC=A6=9D=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=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 --- .../ac/ks/cs_web_back/global/jwt/JwtUtil.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java index adb5c8d..fd279b2 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java @@ -1,8 +1,12 @@ package kr.ac.ks.cs_web_back.global.jwt; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import jakarta.annotation.PostConstruct; +import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; +import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -46,4 +50,23 @@ public String generateToken(String email, Long expireTime) { .signWith(hmacKey) .compact(); } + + public String getEmailFromToken(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(hmacKey) + .build() + .parseClaimsJws(token) + .getBody(); + + return claims.getSubject(); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(hmacKey).build().parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + throw new InvalidTokenException(AuthExceptionCode.NOT_FOUND_USER); + } + } } \ No newline at end of file From 90df8e92fa3b3e6a14bb7ca8d30852c47a1bb1cc Mon Sep 17 00:00:00 2001 From: wooki Date: Wed, 10 Sep 2025 22:40:17 +0900 Subject: [PATCH 03/37] =?UTF-8?q?feat(#20):=20JWT=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/jwt/JwtAuthenticationFilter.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..00c801f --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,43 @@ +package kr.ac.ks.cs_web_back.global.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final JwtTokenResolver jwtTokenResolver; + private final UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + String token = jwtTokenResolver.resolveToken(request); + + if (token != null && jwtUtil.validateToken(token)) { + String email = jwtUtil.getEmailFromToken(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } + +} From 9f289b1e9139838540cd04e71c503ba496c96272 Mon Sep 17 00:00:00 2001 From: wooki Date: Wed, 10 Sep 2025 22:41:25 +0900 Subject: [PATCH 04/37] =?UTF-8?q?feat(#20):=20=EC=9D=B8=ED=84=B0=EC=85=89?= =?UTF-8?q?=ED=84=B0=20Spring=20Security=EC=97=90=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ks/cs_web_back/global/config/SecurityConfig.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java b/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java index f0e2c3a..8da6122 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java @@ -1,5 +1,7 @@ package kr.ac.ks.cs_web_back.global.config; +import kr.ac.ks.cs_web_back.global.jwt.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -9,17 +11,20 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.boot.autoconfigure.security.servlet.PathRequest; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { + private final JwtAuthenticationFilter jwtAuthenticationFilter; + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http @@ -31,7 +36,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(PathRequest.toH2Console()).permitAll() // H2 콘솔 경로 허용 .requestMatchers("/member/register", "/auth/login", "/swagger-ui/**", "/v3/api-docs/**").permitAll() .anyRequest().authenticated() - ); + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } From 8c0cb508ae619d4acd9ec0f41580faf1adda8ddf Mon Sep 17 00:00:00 2001 From: wooki Date: Wed, 10 Sep 2025 22:42:12 +0900 Subject: [PATCH 05/37] =?UTF-8?q?feat(#20):=20=EC=9C=A0=ED=9A=A8=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=ED=86=A0=ED=81=B0=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/exeption/domain/InvalidTokenException.java | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/exeption/domain/InvalidTokenException.java diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/exeption/domain/InvalidTokenException.java b/src/main/java/kr/ac/ks/cs_web_back/global/exeption/domain/InvalidTokenException.java new file mode 100644 index 0000000..9e4bde0 --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/global/exeption/domain/InvalidTokenException.java @@ -0,0 +1,9 @@ +package kr.ac.ks.cs_web_back.global.exeption.domain; + +import kr.ac.ks.cs_web_back.global.exeption.dto.ExceptionCode; + +public class InvalidTokenException extends UnauthorizedException { + public InvalidTokenException(final ExceptionCode exception) { + super(exception); + } +} From 6696c892a57634366e3469a1eaefb920904beadd Mon Sep 17 00:00:00 2001 From: wooki Date: Wed, 10 Sep 2025 23:46:05 +0900 Subject: [PATCH 06/37] =?UTF-8?q?feat(#20):=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=EC=97=90=20UserDetailService=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ac/ks/cs_web_back/domain/member/model/Member.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/member/model/Member.java b/src/main/java/kr/ac/ks/cs_web_back/domain/member/model/Member.java index ce27c0a..e31be79 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/member/model/Member.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/member/model/Member.java @@ -6,13 +6,17 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; import java.time.LocalDate; +import java.util.Collection; +import java.util.Collections; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Member extends BaseEntity { +public class Member extends BaseEntity implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -40,4 +44,9 @@ private Member(String email, String password, String username, LocalDate birthd this.birthdate = birthdate; this.number = number; } + + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } } From fa6bca70ea6df0b4971fa11a97faf9b1f026ea1c Mon Sep 17 00:00:00 2001 From: wooki Date: Wed, 10 Sep 2025 23:46:35 +0900 Subject: [PATCH 07/37] =?UTF-8?q?feat(#20):=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/code/AuthExceptionCode.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java index f538830..92bccdc 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java @@ -10,8 +10,9 @@ public enum AuthExceptionCode implements ExceptionCode { BAD_REQUEST_NO_EMAIL(9010, "이메일이 입력되지 않았습니다."), BAD_REQUEST_NO_PASSWORD(9010, "비밀번호가 입력되지 않았습니다."), + UNAUTHORIZED_INVALID_TOKEN(8002, "유효하지 않은 토큰 형식입니다."), + UNAUTHORIZED_FAILED_VALIDATION(8003, "인증에 실패했습니다.."), UNAUTHORIZED_PASSWORD(8011, "비밀번호가 일치하지 않습니다."), - NOT_FOUND_USER(6001, "일치하는 사용자가 존재하지 않습니다."), ; private final int code; From da242c788b92e8f57e3919c8edae69b0af196de8 Mon Sep 17 00:00:00 2001 From: wooki Date: Wed, 10 Sep 2025 23:47:44 +0900 Subject: [PATCH 08/37] =?UTF-8?q?feat(#20):=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/code/AuthExceptionCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java index 92bccdc..16e893c 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java @@ -11,7 +11,7 @@ public enum AuthExceptionCode implements ExceptionCode { BAD_REQUEST_NO_EMAIL(9010, "이메일이 입력되지 않았습니다."), BAD_REQUEST_NO_PASSWORD(9010, "비밀번호가 입력되지 않았습니다."), UNAUTHORIZED_INVALID_TOKEN(8002, "유효하지 않은 토큰 형식입니다."), - UNAUTHORIZED_FAILED_VALIDATION(8003, "인증에 실패했습니다.."), + UNAUTHORIZED_FAILED_VALIDATION(8003, "인증에 실패했습니다."), UNAUTHORIZED_PASSWORD(8011, "비밀번호가 일치하지 않습니다."), ; From 28c8e1a6dc7480d95d71a16c28b3363a2cb027b0 Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 11 Sep 2025 00:27:20 +0900 Subject: [PATCH 09/37] =?UTF-8?q?feat(#20):=20JWT=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=B2=B4=EC=9D=B8=20=EC=97=94=ED=8A=B8=EB=A6=AC=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jwt/CustomAuthenticationEntryPoint.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPoint.java diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPoint.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..affef88 --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPoint.java @@ -0,0 +1,33 @@ +package kr.ac.ks.cs_web_back.global.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; +import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; +import kr.ac.ks.cs_web_back.global.exeption.dto.ExceptionResponse; +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; +import java.nio.charset.StandardCharsets; + +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + UnauthorizedException exceptionResponse = new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); + String jsonResponse = objectMapper.writeValueAsString(exceptionResponse); + + response.getWriter().write(jsonResponse); + } +} From 9b0aebc1a6b630c726c38be04dde67dad6f5216b Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 11 Sep 2025 00:27:50 +0900 Subject: [PATCH 10/37] =?UTF-8?q?feat(#20):=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=B2=B4=EC=9D=B8=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/ac/ks/cs_web_back/global/config/SecurityConfig.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java b/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java index 8da6122..2bdadf5 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java @@ -1,5 +1,6 @@ package kr.ac.ks.cs_web_back.global.config; +import kr.ac.ks.cs_web_back.global.jwt.CustomAuthenticationEntryPoint; import kr.ac.ks.cs_web_back.global.jwt.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -19,6 +20,7 @@ public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; @Bean public PasswordEncoder passwordEncoder() { @@ -37,7 +39,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/member/register", "/auth/login", "/swagger-ui/**", "/v3/api-docs/**").permitAll() .anyRequest().authenticated() ) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(exceptions -> exceptions + .authenticationEntryPoint(customAuthenticationEntryPoint)); return http.build(); } From 6cbd53a40025a892dc0e91f1b2ba0a3f3fb83a4e Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 11 Sep 2025 00:28:00 +0900 Subject: [PATCH 11/37] =?UTF-8?q?feat(#20):=20UserDetailsService=20?= =?UTF-8?q?=EC=83=81=EC=86=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/service/MemberService.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java index 21bef06..5247b8d 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java @@ -1,15 +1,20 @@ package kr.ac.ks.cs_web_back.domain.member.service; +import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; import kr.ac.ks.cs_web_back.domain.member.controller.code.MemberExceptionCode; import kr.ac.ks.cs_web_back.domain.member.dto.request.MemberCreateRequest; import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest; import kr.ac.ks.cs_web_back.domain.member.model.Member; import kr.ac.ks.cs_web_back.domain.member.repository.MemberRepository; import kr.ac.ks.cs_web_back.global.exeption.domain.ConflictException; +import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; import kr.ac.ks.cs_web_back.global.exeption.domain.NotFoundException; import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; import kr.ac.ks.cs_web_back.global.jwt.JwtUtil; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,7 +22,7 @@ @Service @RequiredArgsConstructor @Transactional -public class MemberService { +public class MemberService implements UserDetailsService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; @@ -39,4 +44,11 @@ public Long createMember(MemberCreateRequest request) { return memberRepository.save(member).getId(); } + + @Override + @Transactional(readOnly = true) + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + return memberRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email)); + } } From 6169019d6a034a0a3d31650fc9063e39959394e5 Mon Sep 17 00:00:00 2001 From: wooki Date: Tue, 16 Sep 2025 18:14:53 +0900 Subject: [PATCH 12/37] =?UTF-8?q?refactor(#20):=20import=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/controller/MemberController.java | 1 - .../ks/cs_web_back/domain/member/service/MemberService.java | 6 ------ 2 files changed, 7 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberController.java b/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberController.java index 68e1023..ce1a57e 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberController.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberController.java @@ -3,7 +3,6 @@ import jakarta.validation.Valid; import kr.ac.ks.cs_web_back.domain.member.controller.code.MemberSuccessCode; import kr.ac.ks.cs_web_back.domain.member.dto.request.MemberCreateRequest; -import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest; import kr.ac.ks.cs_web_back.domain.member.service.MemberService; import kr.ac.ks.cs_web_back.global.response.CsResponse; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java index 5247b8d..3852ca2 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java @@ -1,16 +1,10 @@ package kr.ac.ks.cs_web_back.domain.member.service; -import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; import kr.ac.ks.cs_web_back.domain.member.controller.code.MemberExceptionCode; import kr.ac.ks.cs_web_back.domain.member.dto.request.MemberCreateRequest; -import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest; import kr.ac.ks.cs_web_back.domain.member.model.Member; import kr.ac.ks.cs_web_back.domain.member.repository.MemberRepository; import kr.ac.ks.cs_web_back.global.exeption.domain.ConflictException; -import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; -import kr.ac.ks.cs_web_back.global.exeption.domain.NotFoundException; -import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; -import kr.ac.ks.cs_web_back.global.jwt.JwtUtil; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; From 407f85bb1ea51d2ea2c2f71273c7745a8e7794ba Mon Sep 17 00:00:00 2001 From: wooki Date: Tue, 16 Sep 2025 18:51:28 +0900 Subject: [PATCH 13/37] =?UTF-8?q?fix(#20):=20PasswordEncoder=20=EC=88=9C?= =?UTF-8?q?=ED=99=98=20=EC=B0=B8=EC=A1=B0=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ac/ks/cs_web_back/global/config/AppConfig.java | 14 ++++++++++++++ .../cs_web_back/global/config/SecurityConfig.java | 5 +---- 2 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/config/AppConfig.java diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/config/AppConfig.java b/src/main/java/kr/ac/ks/cs_web_back/global/config/AppConfig.java new file mode 100644 index 0000000..ddddd78 --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/global/config/AppConfig.java @@ -0,0 +1,14 @@ +package kr.ac.ks.cs_web_back.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class AppConfig { + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java b/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java index 2bdadf5..f2040f0 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java @@ -22,10 +22,7 @@ public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { From bd7cf205343eec65f96f7d96920d55b8622225a7 Mon Sep 17 00:00:00 2001 From: wooki Date: Tue, 16 Sep 2025 19:24:45 +0900 Subject: [PATCH 14/37] =?UTF-8?q?feat(#20):=20jwt=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EC=B2=B4=EC=9D=B8=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=EC=98=88=EC=99=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/exeption/GlobalExceptionCode.java | 18 ++++++++++ .../jwt/CustomAuthenticationEntryPoint.java | 28 ++++++++------- .../global/jwt/JwtAuthenticationFilter.java | 34 +++++++++++++++---- .../global/jwt/JwtTokenResolver.java | 2 ++ .../ac/ks/cs_web_back/global/jwt/JwtUtil.java | 3 +- 5 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionCode.java diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionCode.java b/src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionCode.java new file mode 100644 index 0000000..9fdabe8 --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionCode.java @@ -0,0 +1,18 @@ +package kr.ac.ks.cs_web_back.global.exeption; + +import kr.ac.ks.cs_web_back.global.exeption.dto.ExceptionCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum GlobalExceptionCode implements ExceptionCode { + UNAUTHORIZED_NO_TOKEN(8001, "토큰이 없습니다."), + UNAUTHORIZED_INVALID_TOKEN(8002, "유효하지 않은 토큰 형식입니다."), + UNAUTHORIZED_EXPIRED_TOKEN(8003, "만료된 토큰입니다."), + UNAUTHORIZED_FAILED_VALIDATION(8004, "인증에 실패했습니다."), + ; + + private final int code; + private final String message; +} diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPoint.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPoint.java index affef88..e119347 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPoint.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPoint.java @@ -3,31 +3,35 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; -import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; +import kr.ac.ks.cs_web_back.global.exeption.GlobalExceptionCode; import kr.ac.ks.cs_web_back.global.exeption.dto.ExceptionResponse; -import org.springframework.http.MediaType; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import java.io.IOException; -import java.nio.charset.StandardCharsets; @Component +@RequiredArgsConstructor public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper; @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + final GlobalExceptionCode responseCode = GlobalExceptionCode.UNAUTHORIZED_NO_TOKEN; - UnauthorizedException exceptionResponse = new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); - String jsonResponse = objectMapper.writeValueAsString(exceptionResponse); + response.setCharacterEncoding("UTF-8"); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write(jsonResponse); + ExceptionResponse exceptionResponse = new ExceptionResponse(responseCode.getCode(), responseCode.getMessage()); + response.getWriter().write(objectMapper.writeValueAsString(exceptionResponse)); } } diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java index 00c801f..970bc91 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java @@ -1,10 +1,15 @@ package kr.ac.ks.cs_web_back.global.jwt; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import kr.ac.ks.cs_web_back.global.exeption.GlobalExceptionCode; +import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; +import kr.ac.ks.cs_web_back.global.exeption.dto.ExceptionResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; @@ -21,6 +26,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final JwtTokenResolver jwtTokenResolver; private final UserDetailsService userDetailsService; + private final ObjectMapper objectMapper; @Override protected void doFilterInternal( @@ -28,13 +34,29 @@ protected void doFilterInternal( HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { - String token = jwtTokenResolver.resolveToken(request); + try { + String token = jwtTokenResolver.resolveToken(request); - if (token != null && jwtUtil.validateToken(token)) { - String email = jwtUtil.getEmailFromToken(token); - UserDetails userDetails = userDetailsService.loadUserByUsername(email); - UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - SecurityContextHolder.getContext().setAuthentication(authentication); + if (token != null) { + jwtUtil.validateToken(token); + + String email = jwtUtil.getEmailFromToken(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (InvalidTokenException e) { + response.setCharacterEncoding("UTF-8"); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType("application/json;charset=UTF-8"); + + ExceptionResponse exceptionResponse = new ExceptionResponse( + GlobalExceptionCode.UNAUTHORIZED_FAILED_VALIDATION.getCode(), + GlobalExceptionCode.UNAUTHORIZED_FAILED_VALIDATION.getMessage() + ); + + response.getWriter().write(objectMapper.writeValueAsString(exceptionResponse)); + return; } filterChain.doFilter(request, response); diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java index 794b301..702d0a0 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java @@ -1,6 +1,8 @@ package kr.ac.ks.cs_web_back.global.jwt; import jakarta.servlet.http.HttpServletRequest; +import kr.ac.ks.cs_web_back.global.exeption.GlobalExceptionCode; +import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java index fd279b2..95fe5e4 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java @@ -6,6 +6,7 @@ import io.jsonwebtoken.SignatureAlgorithm; import jakarta.annotation.PostConstruct; import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; +import kr.ac.ks.cs_web_back.global.exeption.GlobalExceptionCode; import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -66,7 +67,7 @@ public boolean validateToken(String token) { Jwts.parserBuilder().setSigningKey(hmacKey).build().parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { - throw new InvalidTokenException(AuthExceptionCode.NOT_FOUND_USER); + throw new InvalidTokenException(GlobalExceptionCode.UNAUTHORIZED_INVALID_TOKEN); } } } \ No newline at end of file From 613cd5a0d005d7bf631a28c8dba784431dcfb655 Mon Sep 17 00:00:00 2001 From: wooki Date: Tue, 16 Sep 2025 19:25:50 +0900 Subject: [PATCH 15/37] =?UTF-8?q?refactor(#20):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=98=88=EC=99=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/code/AuthExceptionCode.java | 3 +-- .../kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java index 16e893c..d236100 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java @@ -10,8 +10,7 @@ public enum AuthExceptionCode implements ExceptionCode { BAD_REQUEST_NO_EMAIL(9010, "이메일이 입력되지 않았습니다."), BAD_REQUEST_NO_PASSWORD(9010, "비밀번호가 입력되지 않았습니다."), - UNAUTHORIZED_INVALID_TOKEN(8002, "유효하지 않은 토큰 형식입니다."), - UNAUTHORIZED_FAILED_VALIDATION(8003, "인증에 실패했습니다."), + UNAUTHORIZED_FAILED_VALIDATION(8004, "인증에 실패했습니다."), UNAUTHORIZED_PASSWORD(8011, "비밀번호가 일치하지 않습니다."), ; diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java index ee4dd47..64d8652 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java @@ -24,7 +24,7 @@ public class AuthService { public AuthLoginResponse loginMember(AuthLoginRequest request) { Member member = memberRepository.findByEmail(request.email()) - .orElseThrow(() -> new NotFoundException(AuthExceptionCode.NOT_FOUND_USER)); + .orElseThrow(() -> new NotFoundException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION)); if(!passwordEncoder.matches(request.password(), member.getPassword())) throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_PASSWORD); From 36695dd69fa5c8b2c71460d9237dd97a329c5632 Mon Sep 17 00:00:00 2001 From: wooki Date: Wed, 17 Sep 2025 15:07:55 +0900 Subject: [PATCH 16/37] =?UTF-8?q?style(#20):=20import=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java index 702d0a0..b715d3f 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java @@ -1,8 +1,6 @@ package kr.ac.ks.cs_web_back.global.jwt; import jakarta.servlet.http.HttpServletRequest; -import kr.ac.ks.cs_web_back.global.exeption.GlobalExceptionCode; -import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -13,9 +11,8 @@ public class JwtTokenResolver { public String resolveToken(HttpServletRequest request) { String token = request.getHeader(AUTHORIZATION_HEADER); - if (StringUtils.hasText(token) && token.startsWith(HEADER_PREFIX)) { + if (StringUtils.hasText(token) && token.startsWith(HEADER_PREFIX)) return token.substring(HEADER_PREFIX.length()); - } return null; } From 0b6d6c85cf58a82145e71fc2e03ca9047c617332 Mon Sep 17 00:00:00 2001 From: wooki Date: Wed, 17 Sep 2025 18:20:05 +0900 Subject: [PATCH 17/37] =?UTF-8?q?refactor(#20):=20import=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/ac/ks/cs_web_back/global/config/SecurityConfig.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java b/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java index f2040f0..5cd31f1 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java @@ -8,8 +8,6 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 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; import org.springframework.boot.autoconfigure.security.servlet.PathRequest; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -28,7 +26,6 @@ public class SecurityConfig { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) - // H2 콘솔을 위한 설정 .headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin())) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authz -> authz From 392d590527ace5c5c7a22053956e80d7b6bee2df Mon Sep 17 00:00:00 2001 From: wooki Date: Wed, 17 Sep 2025 18:20:42 +0900 Subject: [PATCH 18/37] =?UTF-8?q?refactor(#20):=20import=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cs_web_back/global/exeption/GlobalExceptionHandler.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionHandler.java b/src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionHandler.java index d86aa75..e415c4a 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionHandler.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionHandler.java @@ -10,10 +10,6 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import kr.ac.ks.cs_web_back.global.exeption.dto.ValidationExceptionCode; -import org.springframework.validation.FieldError; - -import java.util.List; @RestControllerAdvice public class GlobalExceptionHandler { From c54dd82d851a00ec8764fdff124521903d53b089 Mon Sep 17 00:00:00 2001 From: wooki Date: Wed, 17 Sep 2025 18:24:02 +0900 Subject: [PATCH 19/37] =?UTF-8?q?feat(#20):=20=EB=A7=8C=EB=A3=8C=EB=90=9C?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=EA=B3=BC=20=EB=82=98=EB=A8=B8=EC=A7=80=20?= =?UTF-8?q?=EC=83=81=ED=99=A9=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/jwt/JwtAuthenticationFilter.java | 5 ++--- .../ac/ks/cs_web_back/global/jwt/JwtUtil.java | 18 +++++++++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java index 970bc91..0e44c28 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java @@ -5,7 +5,6 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import kr.ac.ks.cs_web_back.global.exeption.GlobalExceptionCode; import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; import kr.ac.ks.cs_web_back.global.exeption.dto.ExceptionResponse; import lombok.RequiredArgsConstructor; @@ -51,8 +50,8 @@ protected void doFilterInternal( response.setContentType("application/json;charset=UTF-8"); ExceptionResponse exceptionResponse = new ExceptionResponse( - GlobalExceptionCode.UNAUTHORIZED_FAILED_VALIDATION.getCode(), - GlobalExceptionCode.UNAUTHORIZED_FAILED_VALIDATION.getMessage() + e.getExceptionCode().getCode(), + e.getExceptionCode().getMessage() ); response.getWriter().write(objectMapper.writeValueAsString(exceptionResponse)); diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java index 95fe5e4..1676f3b 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java @@ -1,12 +1,8 @@ package kr.ac.ks.cs_web_back.global.jwt; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.*; import jakarta.annotation.PostConstruct; import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; -import kr.ac.ks.cs_web_back.global.exeption.GlobalExceptionCode; import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -62,12 +58,16 @@ public String getEmailFromToken(String token) { return claims.getSubject(); } - public boolean validateToken(String token) { + public void validateToken(String token) { try { - Jwts.parserBuilder().setSigningKey(hmacKey).build().parseClaimsJws(token); - return true; + Jwts.parserBuilder() + .setSigningKey(hmacKey) + .build() + .parseClaimsJws(token); + } catch (ExpiredJwtException e) { + throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); } catch (JwtException | IllegalArgumentException e) { - throw new InvalidTokenException(GlobalExceptionCode.UNAUTHORIZED_INVALID_TOKEN); + throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN); } } } \ No newline at end of file From 64534bda926334cf9151ec95b5913d1aab752f7e Mon Sep 17 00:00:00 2001 From: wooki Date: Wed, 17 Sep 2025 18:24:52 +0900 Subject: [PATCH 20/37] =?UTF-8?q?refactor(#20):=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=EC=BD=94=EB=93=9C=20Aut?= =?UTF-8?q?h=20=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/code/AuthExceptionCode.java | 3 ++- .../global/exeption/GlobalExceptionCode.java | 18 ------------------ .../jwt/CustomAuthenticationEntryPoint.java | 4 ++-- 3 files changed, 4 insertions(+), 21 deletions(-) delete mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionCode.java diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java index d236100..2536293 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java @@ -10,7 +10,8 @@ public enum AuthExceptionCode implements ExceptionCode { BAD_REQUEST_NO_EMAIL(9010, "이메일이 입력되지 않았습니다."), BAD_REQUEST_NO_PASSWORD(9010, "비밀번호가 입력되지 않았습니다."), - UNAUTHORIZED_FAILED_VALIDATION(8004, "인증에 실패했습니다."), + UNAUTHORIZED_INVALID_TOKEN(8001, "유효하지 않은 토큰입니다."), + UNAUTHORIZED_FAILED_VALIDATION(8002, "인증에 실패했습니다."), UNAUTHORIZED_PASSWORD(8011, "비밀번호가 일치하지 않습니다."), ; diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionCode.java b/src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionCode.java deleted file mode 100644 index 9fdabe8..0000000 --- a/src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionCode.java +++ /dev/null @@ -1,18 +0,0 @@ -package kr.ac.ks.cs_web_back.global.exeption; - -import kr.ac.ks.cs_web_back.global.exeption.dto.ExceptionCode; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public enum GlobalExceptionCode implements ExceptionCode { - UNAUTHORIZED_NO_TOKEN(8001, "토큰이 없습니다."), - UNAUTHORIZED_INVALID_TOKEN(8002, "유효하지 않은 토큰 형식입니다."), - UNAUTHORIZED_EXPIRED_TOKEN(8003, "만료된 토큰입니다."), - UNAUTHORIZED_FAILED_VALIDATION(8004, "인증에 실패했습니다."), - ; - - private final int code; - private final String message; -} diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPoint.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPoint.java index e119347..c14a581 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPoint.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPoint.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import kr.ac.ks.cs_web_back.global.exeption.GlobalExceptionCode; +import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; import kr.ac.ks.cs_web_back.global.exeption.dto.ExceptionResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -25,7 +25,7 @@ public void commence( HttpServletResponse response, AuthenticationException authException ) throws IOException { - final GlobalExceptionCode responseCode = GlobalExceptionCode.UNAUTHORIZED_NO_TOKEN; + final AuthExceptionCode responseCode = AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN; response.setCharacterEncoding("UTF-8"); response.setStatus(HttpStatus.UNAUTHORIZED.value()); From b6db6536789735077096a32ef1ee0a04ae9b2259 Mon Sep 17 00:00:00 2001 From: wooki Date: Wed, 17 Sep 2025 20:58:52 +0900 Subject: [PATCH 21/37] =?UTF-8?q?feat(#20):=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=97=AC=EB=B6=80=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EC=9D=B8=EC=A6=9D=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Jwt 인증받은 멤버 객체: IdentifiedUser - Jwt 인증받지 않아도 되는 객체: ResolvedUser --- .../global/annotation/IdentifiedUser.java | 11 ++++++ .../global/annotation/ResolvedUser.java | 11 ++++++ .../cs_web_back/global/config/WebConfig.java | 24 ++++++++++++ .../IdentifiedUserArgumentResolver.java | 34 +++++++++++++++++ .../ResolvedUserArgumentResolver.java | 38 +++++++++++++++++++ 5 files changed, 118 insertions(+) create mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/annotation/IdentifiedUser.java create mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/annotation/ResolvedUser.java create mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/config/WebConfig.java create mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/resolver/IdentifiedUserArgumentResolver.java create mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/resolver/ResolvedUserArgumentResolver.java diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/annotation/IdentifiedUser.java b/src/main/java/kr/ac/ks/cs_web_back/global/annotation/IdentifiedUser.java new file mode 100644 index 0000000..34bbec5 --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/global/annotation/IdentifiedUser.java @@ -0,0 +1,11 @@ +package kr.ac.ks.cs_web_back.global.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface IdentifiedUser { +} diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/annotation/ResolvedUser.java b/src/main/java/kr/ac/ks/cs_web_back/global/annotation/ResolvedUser.java new file mode 100644 index 0000000..4608dc8 --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/global/annotation/ResolvedUser.java @@ -0,0 +1,11 @@ +package kr.ac.ks.cs_web_back.global.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface ResolvedUser { +} diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/config/WebConfig.java b/src/main/java/kr/ac/ks/cs_web_back/global/config/WebConfig.java new file mode 100644 index 0000000..655ad1e --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/global/config/WebConfig.java @@ -0,0 +1,24 @@ +package kr.ac.ks.cs_web_back.global.config; + +import kr.ac.ks.cs_web_back.global.resolver.IdentifiedUserArgumentResolver; +import kr.ac.ks.cs_web_back.global.resolver.ResolvedUserArgumentResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private final IdentifiedUserArgumentResolver identifiedUserArgumentResolver; + private final ResolvedUserArgumentResolver resolvedUserArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(identifiedUserArgumentResolver); + resolvers.add(resolvedUserArgumentResolver); + } +} diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/resolver/IdentifiedUserArgumentResolver.java b/src/main/java/kr/ac/ks/cs_web_back/global/resolver/IdentifiedUserArgumentResolver.java new file mode 100644 index 0000000..92e6ccb --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/global/resolver/IdentifiedUserArgumentResolver.java @@ -0,0 +1,34 @@ +package kr.ac.ks.cs_web_back.global.resolver; + +import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; +import kr.ac.ks.cs_web_back.domain.member.model.Member; +import kr.ac.ks.cs_web_back.global.annotation.IdentifiedUser; +import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +public class IdentifiedUserArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(IdentifiedUser.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + final Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + if (!(principal instanceof Member)) { + throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN); + } + + return principal; + } +} diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/resolver/ResolvedUserArgumentResolver.java b/src/main/java/kr/ac/ks/cs_web_back/global/resolver/ResolvedUserArgumentResolver.java new file mode 100644 index 0000000..adaf4b5 --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/global/resolver/ResolvedUserArgumentResolver.java @@ -0,0 +1,38 @@ +package kr.ac.ks.cs_web_back.global.resolver; + +import kr.ac.ks.cs_web_back.domain.member.model.Member; +import kr.ac.ks.cs_web_back.global.annotation.ResolvedUser; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +public class ResolvedUserArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(ResolvedUser.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || "anonymousUser".equals(authentication.getPrincipal())) + return null; + + final Object principal = authentication.getPrincipal(); + + if (!(principal instanceof Member)) { + return null; + } + + return principal; + } +} From 33c307a671cb4ab11fe8a9f60e9ec95a94436f6c Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 01:24:35 +0900 Subject: [PATCH 22/37] =?UTF-8?q?fix(#20):=20=EB=AA=85=EC=84=B8=EC=84=9C?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/dto/request/MemberCreateRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/member/dto/request/MemberCreateRequest.java b/src/main/java/kr/ac/ks/cs_web_back/domain/member/dto/request/MemberCreateRequest.java index f7e35fa..daaa0b0 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/member/dto/request/MemberCreateRequest.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/member/dto/request/MemberCreateRequest.java @@ -12,12 +12,12 @@ public record MemberCreateRequest( @Schema(description = "비밀번호", example = "examplePassword123") @NotBlank(message = "비밀번호가 입력되지 않았습니다.") - @Size(max = 24, message = "비밀번호는 24자 내로 입력해 주세요.") + @Size(max = 48, message = "비밀번호는 48자 이상일 수 없습니다.") String password, @Schema(description = "사용자명", example = "userNickName") @NotBlank(message = "사용자명이 입력되지 않았습니다.") - @Size(max = 12, message = "유저명은 12자 내로 입력해 주세요.") + @Size(max = 12, message = "사용자명은 12자 이상일 수 없습니다.") String username ) { } From 5361cdf196f63cb2bea191ddf3316a19a7ac6563 Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 01:24:51 +0900 Subject: [PATCH 23/37] =?UTF-8?q?refactor(#20):=20=EB=B9=88=20=EC=A4=84=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java b/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java index 5cd31f1..2b93c99 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java @@ -20,8 +20,6 @@ public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; - - @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http From 5cee264ea0758bba4130e9aeb5fd1ee668585a3f Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 01:40:45 +0900 Subject: [PATCH 24/37] =?UTF-8?q?fix(#20):=20Spring=20Security=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9D=B4=EB=A9=94=EC=9D=BC=EC=9D=84=20=EC=8B=9D?= =?UTF-8?q?=EB=B3=84=EC=9E=90=EB=A1=9C=20=EB=B0=9B=EC=95=84=EA=B0=88=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 실제 username 필드의 Getter로 getUsername 대신 getNickname 할당 --- .../kr/ac/ks/cs_web_back/domain/member/model/Member.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/member/model/Member.java b/src/main/java/kr/ac/ks/cs_web_back/domain/member/model/Member.java index e31be79..ac4c508 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/member/model/Member.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/member/model/Member.java @@ -45,8 +45,17 @@ private Member(String email, String password, String username, LocalDate birthd this.number = number; } + public String getNickname() { + return username; + } + @Override public Collection getAuthorities() { return Collections.emptyList(); } + + @Override + public String getUsername() { + return email; + } } From 66cb90b6d3ae9f751bf24e212af639e774236839 Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 01:41:45 +0900 Subject: [PATCH 25/37] =?UTF-8?q?test(#20):=20getUsername=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EB=B3=80=EA=B2=BD=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ks/cs_web_back/domain/member/service/MemberServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java b/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java index 226e7a2..989f7e6 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java @@ -48,7 +48,7 @@ void shouldCreateMemberAndReturnsCreatedId() { assertThat(createdId).isEqualTo(1L); assertThat(savedMember.getEmail()).isEqualTo(request.email()); - assertThat(savedMember.getUsername()).isEqualTo(request.username()); + assertThat(savedMember.getNickName()).isEqualTo(request.username()); assertThat(passwordEncoder.matches(request.password(), savedMember.getPassword())).isTrue(); } From 9b869a75cb5a26e039ea9a55377447f529794b45 Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 01:43:00 +0900 Subject: [PATCH 26/37] =?UTF-8?q?test(#20):=20=ED=86=A0=ED=81=B0=ED=95=84?= =?UTF-8?q?=ED=84=B0=EC=B2=B4=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=83=9D=EC=84=B1=EC=9E=90=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cs_web_back/global/jwt/TokenFixture.java | 31 +++++++++++++++++++ .../resources/application-test.properties | 4 ++- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/test/java/kr/ac/ks/cs_web_back/global/jwt/TokenFixture.java diff --git a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/TokenFixture.java b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/TokenFixture.java new file mode 100644 index 0000000..5dc8283 --- /dev/null +++ b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/TokenFixture.java @@ -0,0 +1,31 @@ +package kr.ac.ks.cs_web_back.global.jwt; + +public class TokenFixture { + private static final String TEST_EMAIL = "example@ks.ac.kr"; + + /** + * 테스트용 Access Token 생성 + * @param jwtUtil JwtUtil 인스턴스 + * @return 생성된 Access Token + */ + public static String createAccessToken(JwtUtil jwtUtil) { + return jwtUtil.generateAccessToken(TEST_EMAIL); + } + + public static String createAccessToken(JwtUtil jwtUtil, String email) { + return jwtUtil.generateAccessToken(email); + } + + /** + * 테스트용 Refresh Token 생성 + * @param jwtUtil JwtUtil 인스턴스 + * @return 생성된 Refresh Token + */ + public static String createRefreshToken(JwtUtil jwtUtil) { + return jwtUtil.generateRefreshToken(TEST_EMAIL); + } + + public static String createRefreshToken(JwtUtil jwtUtil, String email) { + return jwtUtil.generateRefreshToken(email); + } +} diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index f075f76..cebffda 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -3,4 +3,6 @@ spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.jpa.hibernate.ddl-auto=update -spring.datasource.generate-unique-name=false \ No newline at end of file +spring.datasource.generate-unique-name=false + +jwt.secret=very-long-and-random-secret-key-for-testing-purpose-only-DO-NOT-USE-IN-PRODUCTION \ No newline at end of file From 753d65b81c1e6c6a4922fb360706416fd509599c Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 01:44:24 +0900 Subject: [PATCH 27/37] =?UTF-8?q?test(#20):=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ks/cs_web_back/domain/member/service/MemberServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java b/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java index 989f7e6..c7a9a68 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java @@ -48,7 +48,7 @@ void shouldCreateMemberAndReturnsCreatedId() { assertThat(createdId).isEqualTo(1L); assertThat(savedMember.getEmail()).isEqualTo(request.email()); - assertThat(savedMember.getNickName()).isEqualTo(request.username()); + assertThat(savedMember.getNickname()).isEqualTo(request.username()); assertThat(passwordEncoder.matches(request.password(), savedMember.getPassword())).isTrue(); } From 613d1dc7df895cfa2b5ae34bc63c9d1d6189b98f Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 03:09:51 +0900 Subject: [PATCH 28/37] =?UTF-8?q?refactor(#20):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EA=B9=94=EB=81=94=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ac/ks/cs_web_back/domain/member/service/MemberService.java | 2 +- src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java index 3852ca2..bacb9a8 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java @@ -43,6 +43,6 @@ public Long createMember(MemberCreateRequest request) { @Transactional(readOnly = true) public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return memberRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email)); + .orElseThrow(() -> new UsernameNotFoundException("User not found")); } } diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java index 1676f3b..1c1d0df 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java @@ -38,7 +38,7 @@ public String generateRefreshToken(String email) { return generateToken(email, refreshTokenExpireTime); } - public String generateToken(String email, Long expireTime) { + private String generateToken(String email, Long expireTime) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + expireTime); return Jwts.builder().setSubject(email) From bbb6fb9eabff124d1ccf7101759f78604567fcfd Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 03:10:38 +0900 Subject: [PATCH 29/37] =?UTF-8?q?test(#20):=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EC=B2=B4=EC=9D=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BB=A4=EB=B2=84=EB=A6=AC=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jwt/JwtAuthenticationFilterTest.java | 164 ++++++++++++++++++ .../cs_web_back/global/jwt/TokenFixture.java | 24 ++- 2 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilterTest.java diff --git a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilterTest.java b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilterTest.java new file mode 100644 index 0000000..7c1fd4e --- /dev/null +++ b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilterTest.java @@ -0,0 +1,164 @@ +package kr.ac.ks.cs_web_back.global.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.transaction.Transactional; +import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; +import kr.ac.ks.cs_web_back.domain.member.dto.request.MemberCreateRequest; +import kr.ac.ks.cs_web_back.domain.member.fixture.MemberFixture; +import kr.ac.ks.cs_web_back.domain.member.model.Member; +import kr.ac.ks.cs_web_back.domain.member.service.MemberService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.io.PrintWriter; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@Transactional +@AutoConfigureMockMvc +public class JwtAuthenticationFilterTest { + + @Value("${jwt.secret}") + private String secret; + + @Autowired + JwtAuthenticationFilter jwtAuthenticationFilter; + + @Autowired + MemberService memberService; + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private JwtTokenResolver jwtTokenResolver; + + @MockitoBean + private HttpServletRequest request; + + @MockitoBean + private HttpServletResponse response; + + @MockitoBean + private FilterChain filterChain; + + @Autowired + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + Member member = MemberFixture.memberFixture(); + + memberService.createMember(new MemberCreateRequest( + member.getEmail(), member.getPassword(), member.getUsername() + )); + + SecurityContextHolder.clearContext(); + } + + @Test + @DisplayName("토큰이 유효한 경우 요청이 필터 체인을 통과하고 SecurityContext에 인증 정보가 저장된다.") + void shouldProceedWithFilterChainWhenTokenIsValid() throws Exception { + // Given + String accessToken = TokenFixture.createAccessToken(jwtUtil); + when(request.getHeader("Authorization")).thenReturn("Bearer " + accessToken); + + // When + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // Then + verify(filterChain, times(1)).doFilter(request, response); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + assertNotNull(authentication); + assertTrue(authentication.isAuthenticated()); + assertEquals(TokenFixture.TEST_EMAIL, ((UserDetails) authentication.getPrincipal()).getUsername()); + } + + @Test + @DisplayName("만료된 토큰인 경우 필터 체인 과정에서 401 Unauthorized 예외가 발생한다.") + void shouldBe401UnauthorizedWithFilterChainWhenTokenIsExpired() throws Exception { + // Given + String expiredAccessToken = TokenFixture.createExpiredAccessToken(secret); + when(request.getHeader("Authorization")).thenReturn("Bearer " + expiredAccessToken); + when(response.getWriter()).thenReturn(mock(PrintWriter.class)); + + // When + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // Then + verify(filterChain, never()).doFilter(request, response); + verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + verify(response).setContentType("application/json;charset=UTF-8"); + verify(response, times(1)).getWriter(); + } + + @Test + @DisplayName("토큰 형식이 잘못된 경우 필터 체인 과정에서 401 Unauthorized 예외가 발생한다.") + void shouldBe401UnauthorizedWithFilterChainWhenTokenIsInvalid() throws Exception { + // Given + String malformedAccessToken = "Bearer MalformedAccessToken"; + when(request.getHeader("Authorization")).thenReturn(malformedAccessToken); + when(response.getWriter()).thenReturn(mock(PrintWriter.class)); + + // When + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // Then + verify(filterChain, never()).doFilter(request, response); + verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + verify(response).setContentType("application/json;charset=UTF-8"); + verify(response, times(1)).getWriter(); + } + + @Test + @DisplayName("Authorization 헤더가 없거나 'Bearer '로 시작하지 않는 경우 필터 체인이 진행되고 인증 정보가 설정되지 않는다.") + void shouldProceedWithFilterChainWhenTokenIsNotPresentOrInvalidFormat() throws Exception { + when(request.getHeader("Authorization")).thenReturn(null); + + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + verify(filterChain, times(1)).doFilter(request, response); + assertNull(SecurityContextHolder.getContext().getAuthentication()); + + reset(request, filterChain); + SecurityContextHolder.clearContext(); + + when(request.getHeader("Authorization")).thenReturn("Basic somecredentials"); + + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + verify(filterChain, times(1)).doFilter(request, response); + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + @DisplayName("인증되지 않은 사용자가 보호된 리소스에 접근 시 401 Unauthorized 응답을 받는다.") + void shouldReturn401UnauthorizedWhenUnauthenticatedAccessToSecuredResource() throws Exception { + mockMvc.perform(get("/protected/securedResource")) + .andExpect(status().isUnauthorized()) + .andExpect(content().contentType("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.code").value(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN.getCode())) + .andExpect(jsonPath("$.message").value(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN.getMessage())); + } +} diff --git a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/TokenFixture.java b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/TokenFixture.java index 5dc8283..b6b32ec 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/TokenFixture.java +++ b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/TokenFixture.java @@ -1,7 +1,16 @@ package kr.ac.ks.cs_web_back.global.jwt; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; +import java.util.Base64; +import java.util.Date; + public class TokenFixture { - private static final String TEST_EMAIL = "example@ks.ac.kr"; + + public static final String TEST_EMAIL = "example@ks.ac.kr"; /** * 테스트용 Access Token 생성 @@ -28,4 +37,17 @@ public static String createRefreshToken(JwtUtil jwtUtil) { public static String createRefreshToken(JwtUtil jwtUtil, String email) { return jwtUtil.generateRefreshToken(email); } + + public static String createExpiredAccessToken(String secret) { + byte[] decodedKey = Base64.getDecoder().decode(secret); + Key hmacKey = new SecretKeySpec(decodedKey, SignatureAlgorithm.HS256.getJcaName()); + + Date now = new Date(); + Date expiryDate = new Date(now.getTime() - 1); + return Jwts.builder().setSubject(TEST_EMAIL) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(hmacKey) + .compact(); + } } From 666802152284399855a54fc78d570e9504ae2f7e Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 03:11:39 +0900 Subject: [PATCH 30/37] =?UTF-8?q?feat(#20):=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=EC=B2=B4=EC=9D=B8=EC=97=90=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20Mock=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthControllerTest.java | 18 +++++++++++++++--- .../domain/auth/service/AuthServiceTest.java | 2 +- .../controller/MemberControllerTest.java | 12 ++++++++++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/test/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthControllerTest.java b/src/test/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthControllerTest.java index 815661a..f245399 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthControllerTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthControllerTest.java @@ -9,12 +9,15 @@ import kr.ac.ks.cs_web_back.domain.auth.service.AuthService; import kr.ac.ks.cs_web_back.global.exeption.domain.NotFoundException; import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; +import kr.ac.ks.cs_web_back.global.jwt.JwtTokenResolver; +import kr.ac.ks.cs_web_back.global.jwt.JwtUtil; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.MediaType; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -35,6 +38,15 @@ public class AuthControllerTest { @MockitoBean private AuthService authService; + @MockitoBean + private JwtUtil jwtUtil; + + @MockitoBean + private JwtTokenResolver jwtTokenResolver; + + @MockitoBean + private UserDetailsService userDetailsService; + @Test @DisplayName("로그인 성공: 올바른 이메일과 비밀번호로 로그인 시 200 OK와 토큰을 반환한다.") void loginSuccessReturns200OkWithToken() throws Exception { @@ -110,14 +122,14 @@ void loginWithNonExistentEmailReturns404NotFound() throws Exception { AuthLoginRequest request = AuthFixture.NonExistentEmailLoginRequest(); when(authService.loginMember(any(AuthLoginRequest.class))) - .thenThrow(new NotFoundException(AuthExceptionCode.NOT_FOUND_USER)); + .thenThrow(new NotFoundException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION)); // when & then mockMvc.perform(post("/auth/login") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.code").value(AuthExceptionCode.NOT_FOUND_USER.getCode())) - .andExpect(jsonPath("$.message").value(AuthExceptionCode.NOT_FOUND_USER.getMessage())); + .andExpect(jsonPath("$.code").value(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION.getCode())) + .andExpect(jsonPath("$.message").value(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION.getMessage())); } } diff --git a/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java b/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java index 1828974..a83b10a 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java @@ -86,7 +86,7 @@ void shouldThrowExceptionWhenLoginWithNonExistentEmail() { .isInstanceOf(NotFoundException.class) .satisfies(e -> { NotFoundException exception = (NotFoundException) e; - assertThat(exception.getExceptionCode()).isEqualTo(AuthExceptionCode.NOT_FOUND_USER); + assertThat(exception.getExceptionCode()).isEqualTo(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); }); } } diff --git a/src/test/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberControllerTest.java b/src/test/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberControllerTest.java index 6220cfe..6e1ab09 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberControllerTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberControllerTest.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import kr.ac.ks.cs_web_back.domain.member.dto.request.MemberCreateRequest; import kr.ac.ks.cs_web_back.domain.member.service.MemberService; +import kr.ac.ks.cs_web_back.global.jwt.JwtTokenResolver; +import kr.ac.ks.cs_web_back.global.jwt.JwtUtil; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -22,6 +24,12 @@ public class MemberControllerTest { @MockitoBean private MemberService memberService; + @MockitoBean + private JwtUtil jwtUtil; + + @MockitoBean + private JwtTokenResolver jwtTokenResolver; + @Autowired private MockMvc mockMvc; @@ -114,12 +122,12 @@ public void registeredWithInvalidEmailReturns400BadRequest() throws Exception { } @Test - @DisplayName("회원가입 실패: 비밀번호가 24글자를 초과하면 400 BadRequest를 반환한다.") + @DisplayName("회원가입 실패: 비밀번호가 48글자 이상이면 400 BadRequest를 반환한다.") public void tooLongPasswordReturns400BadRequest() throws Exception { // given MemberCreateRequest request = new MemberCreateRequest( "invalidEmail", - "password".repeat(10), + "a".repeat(48), "exampleUser" ); From f474ea877dcc6d5026609cddaefa774d028e3a8c Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 03:14:54 +0900 Subject: [PATCH 31/37] =?UTF-8?q?test(#20):=20loadUserByUserName=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=BB=A4=EB=B2=84=EB=A6=AC?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/service/MemberServiceTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java b/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java index c7a9a68..926fdc0 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java @@ -102,4 +102,16 @@ void registeredFailedByConflictUsername() { assertThat(e.getExceptionCode()).isEqualTo(MemberExceptionCode.CONFLICT_USERNAME); }); } + + @Test + @DisplayName("loadUserByUsername 실패: 존재하지 않는 이메일로 사용자 로드 시 UsernameNotFoundException이 발생한다.") + void shouldThrowUsernameNotFoundExceptionWhenLoadingNonExistentUser() { + // given + String nonExistentEmail = "nonexistent@ks.ac.kr"; + + // when & then + assertThatThrownBy(() -> memberService.loadUserByUsername(nonExistentEmail)) + .isInstanceOf(org.springframework.security.core.userdetails.UsernameNotFoundException.class) + .hasMessage("User not found"); + } } From ddaa534c4dc14827468852fd85dd744feea15c2c Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 03:26:02 +0900 Subject: [PATCH 32/37] =?UTF-8?q?fix(#20):=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EA=B0=80=20=EC=97=86=EB=8A=94=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/resolver/IdentifiedUserArgumentResolver.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/resolver/IdentifiedUserArgumentResolver.java b/src/main/java/kr/ac/ks/cs_web_back/global/resolver/IdentifiedUserArgumentResolver.java index 92e6ccb..af42990 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/resolver/IdentifiedUserArgumentResolver.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/resolver/IdentifiedUserArgumentResolver.java @@ -5,6 +5,7 @@ import kr.ac.ks.cs_web_back.global.annotation.IdentifiedUser; import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -23,7 +24,13 @@ public boolean supportsParameter(MethodParameter parameter) { @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { - final Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null) { + throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN); + } + + final Object principal = authentication.getPrincipal(); if (!(principal instanceof Member)) { throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN); From b26aa2c64315564a8e1b551b7d63ff983511afe2 Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 03:26:37 +0900 Subject: [PATCH 33/37] =?UTF-8?q?test(#20):=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=A6=AC=EC=A1=B8=EB=B2=84=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resolver/UserArgumentResolverTest.java | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 src/test/java/kr/ac/ks/cs_web_back/global/resolver/UserArgumentResolverTest.java diff --git a/src/test/java/kr/ac/ks/cs_web_back/global/resolver/UserArgumentResolverTest.java b/src/test/java/kr/ac/ks/cs_web_back/global/resolver/UserArgumentResolverTest.java new file mode 100644 index 0000000..2626720 --- /dev/null +++ b/src/test/java/kr/ac/ks/cs_web_back/global/resolver/UserArgumentResolverTest.java @@ -0,0 +1,163 @@ +package kr.ac.ks.cs_web_back.global.resolver; + +import kr.ac.ks.cs_web_back.domain.member.fixture.MemberFixture; +import kr.ac.ks.cs_web_back.domain.member.model.Member; +import kr.ac.ks.cs_web_back.global.annotation.IdentifiedUser; +import kr.ac.ks.cs_web_back.global.annotation.ResolvedUser; +import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserArgumentResolverTest { + + @InjectMocks + private IdentifiedUserArgumentResolver identifiedUserArgumentResolver; + + @InjectMocks + private ResolvedUserArgumentResolver resolvedUserArgumentResolver; + + @Mock + private MethodParameter methodParameter; + + @Mock + private NativeWebRequest nativeWebRequest; + + @Mock + private WebDataBinderFactory webDataBinderFactory; + + @Mock + private ModelAndViewContainer modelAndViewContainer; + + @Mock + private SecurityContext securityContext; + + @Mock + private Authentication authentication; + + private Member testMember; + + @BeforeEach + void setUp() { + testMember = MemberFixture.memberFixture(); + SecurityContextHolder.setContext(securityContext); + } + + // IdentifiedUserArgumentResolver Tests + @Test + @DisplayName("IdentifiedUserArgumentResolver: @IdentifiedUser 어노테이션이 있는 파라미터를 지원한다.") + void identifiedUserSupportsParameterWithAnnotation() { + when(methodParameter.hasParameterAnnotation(IdentifiedUser.class)).thenReturn(true); + assertThat(identifiedUserArgumentResolver.supportsParameter(methodParameter)).isTrue(); + } + + @Test + @DisplayName("IdentifiedUserArgumentResolver: @IdentifiedUser 어노테이션이 없는 파라미터를 지원하지 않는다.") + void identifiedUserSupportsParameterWithoutAnnotation() { + when(methodParameter.hasParameterAnnotation(IdentifiedUser.class)).thenReturn(false); + assertThat(identifiedUserArgumentResolver.supportsParameter(methodParameter)).isFalse(); + } + + @Test + @DisplayName("IdentifiedUserArgumentResolver: 인증된 Member 객체를 반환한다.") + void identifiedUserResolveArgumentReturnsMember() { + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(testMember); + + Object result = identifiedUserArgumentResolver.resolveArgument(methodParameter, modelAndViewContainer, nativeWebRequest, webDataBinderFactory); + + assertThat(result).isEqualTo(testMember); + } + + @Test + @DisplayName("IdentifiedUserArgumentResolver: 인증 정보가 없으면 InvalidTokenException을 발생시킨다.") + void identifiedUserResolveArgumentThrowsExceptionWhenNoAuthentication() { + when(securityContext.getAuthentication()).thenReturn(null); + + assertThatThrownBy(() -> identifiedUserArgumentResolver.resolveArgument(methodParameter, modelAndViewContainer, nativeWebRequest, webDataBinderFactory)) + .isInstanceOf(InvalidTokenException.class); + } + + @Test + @DisplayName("IdentifiedUserArgumentResolver: Principal이 Member 타입이 아니면 InvalidTokenException을 발생시킨다.") + void identifiedUserResolveArgumentThrowsExceptionWhenPrincipalIsNotMember() { + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn("notAMember"); + + assertThatThrownBy(() -> identifiedUserArgumentResolver.resolveArgument(methodParameter, modelAndViewContainer, nativeWebRequest, webDataBinderFactory)) + .isInstanceOf(InvalidTokenException.class); + } + + @Test + @DisplayName("ResolvedUserArgumentResolver: @ResolvedUser 어노테이션이 있는 파라미터를 지원한다.") + void resolvedUserSupportsParameterWithAnnotation() { + when(methodParameter.hasParameterAnnotation(ResolvedUser.class)).thenReturn(true); + assertThat(resolvedUserArgumentResolver.supportsParameter(methodParameter)).isTrue(); + } + + @Test + @DisplayName("ResolvedUserArgumentResolver: @ResolvedUser 어노테이션이 없는 파라미터를 지원하지 않는다.") + void resolvedUserSupportsParameterWithoutAnnotation() { + when(methodParameter.hasParameterAnnotation(ResolvedUser.class)).thenReturn(false); + assertThat(resolvedUserArgumentResolver.supportsParameter(methodParameter)).isFalse(); + } + + @Test + @DisplayName("ResolvedUserArgumentResolver: 인증된 Member 객체를 반환한다.") + void resolvedUserResolveArgumentReturnsMember() { + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(testMember); + + Object result = resolvedUserArgumentResolver.resolveArgument(methodParameter, modelAndViewContainer, nativeWebRequest, webDataBinderFactory); + + assertThat(result).isEqualTo(testMember); + } + + @Test + @DisplayName("ResolvedUserArgumentResolver: 인증 정보가 없으면 null을 반환한다.") + void resolvedUserResolveArgumentReturnsNullWhenNoAuthentication() { + when(securityContext.getAuthentication()).thenReturn(null); + + Object result = resolvedUserArgumentResolver.resolveArgument(methodParameter, modelAndViewContainer, nativeWebRequest, webDataBinderFactory); + + assertThat(result).isNull(); + } + + @Test + @DisplayName("ResolvedUserArgumentResolver: Principal이 'anonymousUser'이면 null을 반환한다.") + void resolvedUserResolveArgumentReturnsNullWhenPrincipalIsAnonymousUser() { + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + + Object result = resolvedUserArgumentResolver.resolveArgument(methodParameter, modelAndViewContainer, nativeWebRequest, webDataBinderFactory); + + assertThat(result).isNull(); + } + + @Test + @DisplayName("ResolvedUserArgumentResolver: Principal이 Member 타입이 아니면 null을 반환한다.") + void resolvedUserResolveArgumentReturnsNullWhenPrincipalIsNotMember() { + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn("notAMember"); + + Object result = resolvedUserArgumentResolver.resolveArgument(methodParameter, modelAndViewContainer, nativeWebRequest, webDataBinderFactory); + + assertThat(result).isNull(); + } +} From 6114a876133c7b86ff82c886b597c8cae49ccb60 Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 04:05:04 +0900 Subject: [PATCH 34/37] =?UTF-8?q?test(#20):=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=86=B5=EA=B3=BC=20=EA=B8=B0=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomAuthenticationEntryPointTest.java | 32 +++++++++++++++++++ .../jwt/JwtAuthenticationFilterTest.java | 10 ------ 2 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java diff --git a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java new file mode 100644 index 0000000..d6252fe --- /dev/null +++ b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java @@ -0,0 +1,32 @@ +package kr.ac.ks.cs_web_back.global.jwt; + +import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +public class CustomAuthenticationEntryPointTest { + + @Autowired + private MockMvc mockMvc; + + @Test + @DisplayName("인증되지 않은 사용자가 보호된 리소스에 접근 시 401 Unauthorized 응답을 받는다.") + void shouldReturn401UnauthorizedWhenUnauthenticatedAccessToSecuredResource() throws Exception { + mockMvc.perform(get("/protected/securedResource")) + .andExpect(status().isUnauthorized()) + .andExpect(content().contentType("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.code").value(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN.getCode())) + .andExpect(jsonPath("$.message").value(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN.getMessage())); + } +} diff --git a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilterTest.java b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilterTest.java index 7c1fd4e..3e3e364 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilterTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilterTest.java @@ -151,14 +151,4 @@ void shouldProceedWithFilterChainWhenTokenIsNotPresentOrInvalidFormat() throws E verify(filterChain, times(1)).doFilter(request, response); assertNull(SecurityContextHolder.getContext().getAuthentication()); } - - @Test - @DisplayName("인증되지 않은 사용자가 보호된 리소스에 접근 시 401 Unauthorized 응답을 받는다.") - void shouldReturn401UnauthorizedWhenUnauthenticatedAccessToSecuredResource() throws Exception { - mockMvc.perform(get("/protected/securedResource")) - .andExpect(status().isUnauthorized()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.code").value(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN.getCode())) - .andExpect(jsonPath("$.message").value(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN.getMessage())); - } } From 735f3cba473868fe946b4cc3b2028db9172dbe73 Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 04:08:24 +0900 Subject: [PATCH 35/37] =?UTF-8?q?test(#20):=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=84=B1=EA=B3=B5=20=EA=B8=B0=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/jwt/CustomAuthenticationEntryPointTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java index d6252fe..7d91505 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java @@ -13,7 +13,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc public class CustomAuthenticationEntryPointTest { From e8d5d4f34973e84e5ff9dba66efe07a358dc7b7a Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 04:13:32 +0900 Subject: [PATCH 36/37] =?UTF-8?q?test(#20):=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=86=B5=EA=B3=BC=20=EA=B8=B0=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/jwt/CustomAuthenticationEntryPointTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java index 7d91505..de7c092 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java @@ -1,11 +1,13 @@ package kr.ac.ks.cs_web_back.global.jwt; import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; +import kr.ac.ks.cs_web_back.global.config.SecurityConfig; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -15,6 +17,7 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc +@Import({SecurityConfig.class, CustomAuthenticationEntryPoint.class}) public class CustomAuthenticationEntryPointTest { @Autowired From b5d1b0b2b2e5a78df45dca1a371dd9dadddd6a08 Mon Sep 17 00:00:00 2001 From: wooki Date: Thu, 18 Sep 2025 04:38:33 +0900 Subject: [PATCH 37/37] =?UTF-8?q?test(#20):=20=EA=B8=B8=EA=B3=A0=20?= =?UTF-8?q?=EA=B8=B4=20=EC=8B=B8=EC=9B=80=20=EB=81=9D=EC=97=90=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=86=B5=EA=B3=BC=20=EA=B8=B0=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomAuthenticationEntryPointTest.java | 84 ++++++++++++++----- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java index de7c092..27baae8 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/CustomAuthenticationEntryPointTest.java @@ -1,35 +1,73 @@ package kr.ac.ks.cs_web_back.global.jwt; +import com.fasterxml.jackson.databind.ObjectMapper; import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; -import kr.ac.ks.cs_web_back.global.config.SecurityConfig; +import kr.ac.ks.cs_web_back.global.exeption.dto.ExceptionResponse; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Import; -import org.springframework.test.web.servlet.MockMvc; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@AutoConfigureMockMvc -@Import({SecurityConfig.class, CustomAuthenticationEntryPoint.class}) -public class CustomAuthenticationEntryPointTest { +import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; - @Autowired - private MockMvc mockMvc; +@ExtendWith(MockitoExtension.class) +class CustomAuthenticationEntryPointTest { + + @InjectMocks + private CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private AuthenticationException authException; + + @Spy + private ObjectMapper objectMapper = new ObjectMapper(); + private StringWriter stringWriter; + private PrintWriter printWriter; + + @BeforeEach + void setUp() throws IOException { + stringWriter = new StringWriter(); + printWriter = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(printWriter); + + } @Test - @DisplayName("인증되지 않은 사용자가 보호된 리소스에 접근 시 401 Unauthorized 응답을 받는다.") - void shouldReturn401UnauthorizedWhenUnauthenticatedAccessToSecuredResource() throws Exception { - mockMvc.perform(get("/protected/securedResource")) - .andExpect(status().isUnauthorized()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.code").value(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN.getCode())) - .andExpect(jsonPath("$.message").value(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN.getMessage())); + @DisplayName("commence 메서드 호출 시 401 Unauthorized 응답과 JSON 에러 메시지를 반환한다.") + void commenceReturns401UnauthorizedAndJsonError() throws IOException { + // Given + AuthExceptionCode expectedCode = AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN; + ExceptionResponse expectedResponse = new ExceptionResponse(expectedCode.getCode(), expectedCode.getMessage()); + String expectedJson = objectMapper.writeValueAsString(expectedResponse); + + // When + customAuthenticationEntryPoint.commence(request, response, authException); + + // Then + verify(response).setCharacterEncoding("UTF-8"); + verify(response).setStatus(HttpStatus.UNAUTHORIZED.value()); + verify(response).setContentType("application/json;charset=UTF-8"); + verify(response).getWriter(); + printWriter.flush(); + assertThat(stringWriter.toString()).isEqualTo(expectedJson); } }