From ee829b00e663c062a07f79626ac7bef3af6a46dc Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Tue, 23 Sep 2025 16:44:29 +0900 Subject: [PATCH 01/36] [Feat] #3 UserPrincipal --- .../domain/auth/model/UserPrincipal.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/model/UserPrincipal.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/model/UserPrincipal.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/model/UserPrincipal.java new file mode 100644 index 0000000..59e32ca --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/model/UserPrincipal.java @@ -0,0 +1,38 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.model; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +@RequiredArgsConstructor +public class UserPrincipal implements UserDetails{ + + private final Long userId; + private final String username; + private final String password; + private final Collection authorities; + + /** UserDetails 구현 */ + @Override + public String getPassword() { return password; } + + @Override + public String getUsername() { return username; } + + @Override + public Collection getAuthorities() { return authorities; } + + @Override + public boolean isAccountNonExpired() { return true; } + + @Override + public boolean isAccountNonLocked() { return true; } + + @Override + public boolean isCredentialsNonExpired() { return true; } + + @Override + public boolean isEnabled() { return true; } +} From cc8a4a99ffa5a82f9ce69d3b9cbf320613c20eba Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Tue, 23 Sep 2025 16:52:02 +0900 Subject: [PATCH 02/36] [Feat] #3 CustomUserDetailsService --- .../service/CustomUserDetailsService.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java new file mode 100644 index 0000000..5f27e74 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java @@ -0,0 +1,27 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.service; + +import com.WhoIsRoom.WhoIs_Server.domain.auth.model.UserPrincipal; +import com.WhoIsRoom.WhoIs_Server.domain.user.model.User; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import java.util.Collections; + +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + + @Override + public UserPrincipal loadUserByUsername(String email) throws UsernameNotFoundException { + User user = userRepository.findByEmail(email); + if (user == null) { + throw new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다."); + } + + return new UserPrincipal( + user.getEmail(), + user.getPassword(), + Collections.singleton(new SimpleGrantedAuthority(Role.USER.name())) + ); + } +} From 43ffcbd3bb6eebaef7f8a9cfa032bc43cb02e40e Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Tue, 23 Sep 2025 16:55:51 +0900 Subject: [PATCH 03/36] [Feat] #3 UserRepository --- .../auth/service/CustomUserDetailsService.java | 14 ++++++++++---- .../domain/user/repository/UserRepository.java | 10 ++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/repository/UserRepository.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java index 5f27e74..ff27047 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java @@ -2,21 +2,27 @@ import com.WhoIsRoom.WhoIs_Server.domain.auth.model.UserPrincipal; import com.WhoIsRoom.WhoIs_Server.domain.user.model.User; +import com.WhoIsRoom.WhoIs_Server.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; import java.util.Collections; +@Slf4j +@Service +@RequiredArgsConstructor public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; @Override public UserPrincipal loadUserByUsername(String email) throws UsernameNotFoundException { - User user = userRepository.findByEmail(email); - if (user == null) { - throw new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다."); - } + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException(email + "해당 이메일의 사용자를 찾을 수 없습니다.")); return new UserPrincipal( user.getEmail(), diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/repository/UserRepository.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..20cdfec --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.WhoIsRoom.WhoIs_Server.domain.user.repository; + +import com.WhoIsRoom.WhoIs_Server.domain.user.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); +} From 50e54d0c0a6f10c3edc7c3fa971c74af73f47d0b Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Tue, 23 Sep 2025 17:06:24 +0900 Subject: [PATCH 04/36] [Feat] #3 Role enum --- .../service/CustomUserDetailsService.java | 4 +- .../WhoIs_Server/domain/user/model/Role.java | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/model/Role.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java index ff27047..23c6712 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java @@ -9,6 +9,7 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import com.WhoIsRoom.WhoIs_Server.domain.user.model.Role; import java.util.Collections; @@ -25,9 +26,10 @@ public UserPrincipal loadUserByUsername(String email) throws UsernameNotFoundExc .orElseThrow(() -> new UsernameNotFoundException(email + "해당 이메일의 사용자를 찾을 수 없습니다.")); return new UserPrincipal( + user.getId(), user.getEmail(), user.getPassword(), - Collections.singleton(new SimpleGrantedAuthority(Role.USER.name())) + Collections.singleton(new SimpleGrantedAuthority(Role.MEMBER.getValue())) ); } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/model/Role.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/model/Role.java new file mode 100644 index 0000000..05a3fc8 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/model/Role.java @@ -0,0 +1,39 @@ +package com.WhoIsRoom.WhoIs_Server.domain.user.model; + +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Optional; + +@Getter +public enum Role { + + MEMBER("MEMBER"), + ADMIN("ADMIN"); + + Role(String value) { + this.value = value; + this.role = PREFIX + value; + } + + private static final String PREFIX = "ROLE_"; + private final String value; + private final String role; + + // 파싱된 값에 맞는 Role을 반환하는 메서드 + public static Optional fromRole(String roleString) { + if (roleString != null && roleString.startsWith(PREFIX)) { + String roleValue = roleString.substring(PREFIX.length()); + for (Role r : values()) { + if (r.value.equalsIgnoreCase(roleValue)) return Optional.of(r); + } + } + return Optional.empty(); + } + + // Role을 권한으로 변환하는 메서드 + public GrantedAuthority toAuthority() { + return new SimpleGrantedAuthority(PREFIX + this.value); + } +} From b33f419c9bdb3097b6320642bfffc06da45cfd81 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Tue, 23 Sep 2025 17:23:23 +0900 Subject: [PATCH 05/36] [Feat] #3 JwtService --- .../service/CustomUserDetailsService.java | 4 +- .../domain/auth/service/JwtService.java | 91 +++++++++++++++++++ .../WhoIs_Server/domain/user/model/User.java | 7 +- src/main/resources/application.yml | 12 +-- 4 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java index 23c6712..a59ff9c 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java @@ -23,13 +23,13 @@ public class CustomUserDetailsService implements UserDetailsService { @Override public UserPrincipal loadUserByUsername(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException(email + "해당 이메일의 사용자를 찾을 수 없습니다.")); + .orElseThrow(() -> new UsernameNotFoundException(email + ": 해당 이메일의 사용자를 찾을 수 없습니다.")); return new UserPrincipal( user.getId(), user.getEmail(), user.getPassword(), - Collections.singleton(new SimpleGrantedAuthority(Role.MEMBER.getValue())) + Collections.singleton(user.getRole().toAuthority()) ); } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java new file mode 100644 index 0000000..51e2a44 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java @@ -0,0 +1,91 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.service; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.time.Duration; + +@Slf4j +@Service +@RequiredArgsConstructor +public class JwtService { + + @Getter + @Value("${jwt.access.expiration}") + private Long accessTokenExpirationPeriod; + + @Getter + @Value("${jwt.refresh.expiration}") + private Long refreshTokenExpirationPeriod; + + private static final String LOGOUT_VALUE = "logout"; + private static final String REFRESH_TOKEN_KEY_PREFIX = "auth:refresh:"; + + private final RedisService redisService; + private final JwtUtil jwtUtil; + + public void logout(HttpServletRequest request) { + String accessToken = jwtUtil.resolveAccessToken(request); + String refreshToken = jwtUtil.resolveRefreshToken(request); + + deleteRefreshToken(refreshToken); + //access token blacklist 처리 -> 로그아웃한 사용자가 요청 시 access token이 redis에 존재하면 jwtAuthenticationFilter에서 인증처리 거부 + invalidAccessToken(accessToken); + } + + public void reissueToken(HttpServletRequest request, HttpServletResponse response) { + String refreshToken = jwtUtil.resolveRefreshToken(request); + jwtUtil.validateToken(refreshToken); + reissueAndSendTokens(response, refreshToken); + } + + public void checkLogout(HttpServletRequest request) { + String accessToken = jwtUtil.resolveAccessToken(request); + String value = redisService.getValues(accessToken); + if (value.equals(LOGOUT_VALUE)) { + throw new LogoutException(BaseResponseStatus.UNAUTHORIZED_ACCESS); + } + } + + public void storeRefreshToken(String refreshToken) { + redisService.setValues(REFRESH_TOKEN_KEY_PREFIX, refreshToken, Duration.ofMillis(refreshTokenExpirationPeriod)); + } + + private void deleteRefreshToken(String refreshToken){ + if(refreshToken == null){ + throw new JwtException(BaseResponseStatus.EMPTY_REFRESH_HEADER); + } + redisService.delete(refreshToken); + } + + private void invalidAccessToken(String accessToken) { + redisService.setValues(accessToken, LOGOUT_VALUE, + Duration.ofMillis(accessTokenExpirationPeriod)); + } + + private void reissueAndSendTokens(HttpServletResponse response, String refreshToken) { + + // 새로운 Refresh Token 발급 + String reissuedAccessToken = jwtUtil.createAccessToken(jwtUtil.getMemberId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken), jwtUtil.getName(refreshToken)); + String reissuedRefreshToken = jwtUtil.createRefreshToken(jwtUtil.getMemberId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken)); + + // 새로운 Refresh Token을 DB나 Redis에 저장 + storeRefreshToken(reissuedRefreshToken); + + // 기존 Refresh Token 폐기 (DB나 Redis에서 삭제) + deleteRefreshToken(refreshToken); + + sendTokens(response, reissuedAccessToken, reissuedRefreshToken); + } + + private void sendTokens(HttpServletResponse response, String reissuedAccessToken, + String reissuedRefreshToken) { + response.addCookie(cookieUtil.createCookie(accessHeader, reissuedAccessToken)); + response.addCookie(cookieUtil.createCookie(refreshHeader, reissuedRefreshToken)); + } +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/model/User.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/model/User.java index a72e44a..e57ef5b 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/model/User.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/model/User.java @@ -24,10 +24,15 @@ public class User extends BaseEntity { @Column(name = "nickname", length = 30, nullable = false) private String nickName; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role; + @Builder - public User(String email, String password, String nickName) { + public User(String email, String password, String nickName, Role role) { this.email = email; this.password = password; this.nickName = nickName; + this.role = role; } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e24f928..75cc4ec 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -72,12 +72,12 @@ spring: # mail.smtp.timeout: 5000 # mail.smtp.starttls.enable: true # -#jwt: -# secret: ${jwt.secret} -# access: -# expiration: ${jwt.access.expiration} -# refresh: -# expiration: ${jwt.refresh.expiration} +jwt: + secret: "DB2643999CDE219D555A492B4767A4517ACD96AB6C846111E9FE8C79E3" + access: + expiration: 3600000 + refresh: + expiration: 1209600000 --- # 개발용 포트 From 6b0dcae77a508041200bef811a987fb44778623e Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Tue, 23 Sep 2025 17:25:35 +0900 Subject: [PATCH 06/36] [Feat] #3 RedisService --- .../domain/auth/service/JwtService.java | 1 + .../global/common/redis/RedisService.java | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/redis/RedisService.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java index 51e2a44..f78d32c 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java @@ -1,5 +1,6 @@ package com.WhoIsRoom.WhoIs_Server.domain.auth.service; +import com.WhoIsRoom.WhoIs_Server.global.common.redis.RedisService; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.Getter; diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/redis/RedisService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/redis/RedisService.java new file mode 100644 index 0000000..4db9683 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/redis/RedisService.java @@ -0,0 +1,39 @@ +package com.WhoIsRoom.WhoIs_Server.global.common.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; + +@Service +@RequiredArgsConstructor +public class RedisService { + + private final RedisTemplate redisTemplate; + + public void setValues(String key, String data, Duration duration) { + ValueOperations values = redisTemplate.opsForValue(); + values.set(key, data, duration); + } + + @Transactional(readOnly = true) + public String getValues(String key) { + ValueOperations values = redisTemplate.opsForValue(); + if (values.get(key) == null) { + return "false"; + } + return (String) values.get(key); + } + + public void delete(String key) { + redisTemplate.delete(key); + } + + protected boolean checkExistsValue(String value) { + return !value.equals("false"); + } + +} From 6ef8264a251df6063ff0b0115d3bc5abdec0cc1b Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Tue, 23 Sep 2025 17:28:07 +0900 Subject: [PATCH 07/36] [Feat] #3 RedisConfig --- .../global/config/RedisConfig.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/RedisConfig.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/RedisConfig.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/RedisConfig.java new file mode 100644 index 0000000..d677aad --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/RedisConfig.java @@ -0,0 +1,44 @@ +package com.WhoIsRoom.WhoIs_Server.global.config; + +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Slf4j +@Configuration +public class RedisConfig { + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @PostConstruct + public void init() { + log.info("Redis Host: {}", host); + log.info("Redis Port: {}", port); + } + + // RedisProperties로 yaml에 저장한 host, post를 연결 + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + // serializer 설정으로 redis-cli를 통해 직접 데이터를 조회할 수 있도록 설정 + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + + return redisTemplate; + } +} From 3c6e9e506bf5ab897ef8c47463b9ce59937ad920 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Tue, 23 Sep 2025 18:15:43 +0900 Subject: [PATCH 08/36] [Feat] #3 JwtUtil --- build.gradle | 6 +- .../domain/auth/service/JwtService.java | 11 +- .../domain/auth/util/JwtUtil.java | 128 ++++++++++++++++++ src/main/resources/application.yml | 6 +- 4 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java diff --git a/build.gradle b/build.gradle index ddfac73..3ac0063 100644 --- a/build.gradle +++ b/build.gradle @@ -48,9 +48,9 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' // JWT -> 원하는 버전 사용 - implementation 'io.jsonwebtoken:jjwt-api:0.11.5' - implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' - implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java index f78d32c..46088d1 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java @@ -1,5 +1,6 @@ package com.WhoIsRoom.WhoIs_Server.domain.auth.service; +import com.WhoIsRoom.WhoIs_Server.domain.auth.util.JwtUtil; import com.WhoIsRoom.WhoIs_Server.global.common.redis.RedisService; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -16,13 +17,11 @@ @RequiredArgsConstructor public class JwtService { - @Getter @Value("${jwt.access.expiration}") - private Long accessTokenExpirationPeriod; + private Long ACCESS_TOKEN_EXPIRED_IN; - @Getter @Value("${jwt.refresh.expiration}") - private Long refreshTokenExpirationPeriod; + private Long REFRESH_TOKEN_EXPIRED_IN; private static final String LOGOUT_VALUE = "logout"; private static final String REFRESH_TOKEN_KEY_PREFIX = "auth:refresh:"; @@ -54,7 +53,7 @@ public void checkLogout(HttpServletRequest request) { } public void storeRefreshToken(String refreshToken) { - redisService.setValues(REFRESH_TOKEN_KEY_PREFIX, refreshToken, Duration.ofMillis(refreshTokenExpirationPeriod)); + redisService.setValues(REFRESH_TOKEN_KEY_PREFIX, refreshToken, Duration.ofMillis(REFRESH_TOKEN_EXPIRED_IN)); } private void deleteRefreshToken(String refreshToken){ @@ -66,7 +65,7 @@ private void deleteRefreshToken(String refreshToken){ private void invalidAccessToken(String accessToken) { redisService.setValues(accessToken, LOGOUT_VALUE, - Duration.ofMillis(accessTokenExpirationPeriod)); + Duration.ofMillis(ACCESS_TOKEN_EXPIRED_IN)); } private void reissueAndSendTokens(HttpServletResponse response, String refreshToken) { diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java new file mode 100644 index 0000000..28aa210 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java @@ -0,0 +1,128 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.util; + +import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import io.jsonwebtoken.*; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Optional; + +@Slf4j +@Component +public class JwtUtil { + + @Value("${jwt.secret}") + private SecretKey secretKey; + + @Value("${jwt.access.expiration}") + private Long ACCESS_TOKEN_EXPIRED_IN; + + @Value("${jwt.refresh.expiration}") + private Long REFRESH_TOKEN_EXPIRED_IN; + + public final String BEARER = "Bearer "; + + public JwtUtil(@Value("${secret.jwt-secret-key}") String secret) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + } + + public Long getMemberId(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("memberId", Long.class); + } + + public String getProviderId(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("providerId", String.class); + } + + public String getRole(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class); + } + + public String getTokenType(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("tokenType", String.class); + } + + public String getEmail(String token){ + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("email", String.class); + } + + public String getName(String token){ + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("name", String.class); + } + + public Boolean isTokenExpired(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date()); + } + + public String createAccessToken(Long memberId, String providerId, String role, String name) { + + return Jwts.builder() + .claim("tokenType", "access") + .claim("memberId", memberId) + .claim("providerId", providerId) + .claim("role", role) + .claim("name", name) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRED_IN)) + .signWith(secretKey) + .compact(); + } + + public String createRefreshToken(Long memberId, String providerId, String role) { + + return Jwts.builder() + .claim("tokenType", "refresh") + .claim("memberId", memberId) + .claim("providerId", providerId) + .claim("role", role) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRED_IN)) + .signWith(secretKey) + .compact(); + } + + public void validateToken(String token) { + try { + Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload(); + } catch (ExpiredJwtException e) { // 토큰 만료 + throw new JwtException(ErrorCode.EXPIRED_ACCESS_TOKEN); + } catch (UnsupportedJwtException e) { // 지원되지 않는 형식 + throw new JwtException(ErrorCode.UNSUPPORTED_TOKEN_TYPE); + } catch (MalformedJwtException e) { // 구조가 잘못된 토큰 + throw new JwtException(ErrorCode.MALFORMED_TOKEN_TYPE); + } catch (SignatureException e) { // 서명 위조 (곧 지원 중단) + throw new JwtException(ErrorCode.INVALID_SIGNATURE_JWT); + } catch (IllegalArgumentException e) { // 토큰이 비어 있거나 Null + throw new JwtException(ErrorCode.EMPTY_AUTHORIZATION_HEADER); + } catch (Exception e) { // 기타 예외 상황 + throw new JwtException(ErrorCode.INVALID_ACCESS_TOKEN); + } + } + + public String getUserNameFromToken(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("name", String.class); + } + + public Optional extractAccessToken(HttpServletRequest request, String accessHeader) { + return Optional.ofNullable(request.getHeader(accessHeader)) + .filter(accessToken -> accessToken.startsWith(BEARER)) + .map(accessToken -> accessToken.replace(BEARER, "")); + } + + public Optional extractRefreshToken(HttpServletRequest request, String refreshHeader) { + return Optional.ofNullable(request.getHeader(refreshHeader)) + .filter(refreshToken -> refreshToken.startsWith(BEARER)) + .map(refreshToken -> refreshToken.replace(BEARER, "")); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 75cc4ec..8d1d18b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -73,11 +73,11 @@ spring: # mail.smtp.starttls.enable: true # jwt: - secret: "DB2643999CDE219D555A492B4767A4517ACD96AB6C846111E9FE8C79E3" + secret: ${JWT_SECRET_KEY} access: - expiration: 3600000 + expiration: ${JWT_ACCESS_EXPIRED_IN} refresh: - expiration: 1209600000 + expiration: ${JWT_REFRESH_EXPIRED_IN} --- # 개발용 포트 From 492c37f92bef2e52f4cb09a5c3a6f3265ac6de25 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Tue, 23 Sep 2025 18:53:18 +0900 Subject: [PATCH 09/36] [Feat] #3 JwtAuthenticationFilter --- .../auth/filter/JwtAuthenticationFilter.java | 103 ++++++++++++++++++ .../domain/auth/util/JwtUtil.java | 5 +- 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..f311e08 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java @@ -0,0 +1,103 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.filter; + +import com.WhoIsRoom.WhoIs_Server.domain.auth.model.UserPrincipal; +import com.WhoIsRoom.WhoIs_Server.domain.auth.service.JwtService; +import com.WhoIsRoom.WhoIs_Server.domain.auth.util.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.GenericFilterBean; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtUtil jwtUtil; + private final JwtService jwtService; + + // 인증을 안해도 되니 토큰이 필요없는 URL들 (에러: 로그인이 필요합니다) + public final static List PASS_URIS = Arrays.asList( + "/api/login", + "/api/logout", + "/api/signup" + ); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + if(isPassUris(request.getRequestURI())) { + log.info("JWT Filter Passed (pass uri) : {}", request.getRequestURI()); + filterChain.doFilter(request, response); + return; + } + + log.info("Request URI: {}", request.getRequestURI()); // 요청 URI 로깅 + String accessToken = jwtUtil.resolveAccessToken(request); + + // 엑세스 토큰이 없으면 Authentication도 없음 -> EntryPoint (401) + if(accessToken == null) { + log.info("JWT Filter Pass (accessToken is null) : {}", request.getRequestURI()); + SecurityContextHolder.clearContext(); + filterChain.doFilter(request, response); + return; + } + + // 토큰 유효성 검사 + jwtUtil.validateToken(accessToken); + + // 토큰 타입 검사 + if(!"access".equals(jwtUtil.getTokenType(accessToken))) { + throw new JwtException(BaseResponseStatus.INVALID_TOKEN_TYPE); + } + + jwtService.checkLogout(request); + + // 권한 리스트 생성 + List authorities = Arrays.asList(new SimpleGrantedAuthority(jwtUtil.getRole(accessToken))); + log.info("Granted Authorities : {}", authorities); + UserPrincipal principal = new UserPrincipal( + jwtUtil.getMemberId(accessToken), + jwtUtil.getEmail(accessToken), + null, // 패스워드는 필요 없음 + authorities + ); + log.info("UserPrincipal created: {}", principal); // 생성된 사용자 정보 로깅 + log.info("UserPrincipal.providerId: {}", principal.getProviderId()); + log.info("UserPrincipal.role: {}", principal.getAuthorities().stream().findFirst().get().toString()); + log.info("UserPrincipal.memberId: {}", principal.getUserId()); + + Authentication authToken = null; + if ("localhost".equals(loginProvider)) { + // 폼 로그인(자체 회원) + authToken = new UsernamePasswordAuthenticationToken(principal, null, authorities); + } +// else { +// // 소셜 로그인 +// authToken = new OAuth2AuthenticationToken(principal, authorities, loginProvider); +// } + log.info("Authentication set in SecurityContext: {}", SecurityContextHolder.getContext().getAuthentication()); // SecurityContext 설정 확인 로깅 + log.info("Authorities in SecurityContext: {}", authToken.getAuthorities()); + + log.info("JWT Filter Success : {}", request.getRequestURI()); + SecurityContextHolder.getContext().setAuthentication(authToken); + filterChain.doFilter(request, response); + } + + private boolean isPassUris(String uri) { + return PASS_URIS.contains(uri); + } +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java index 28aa210..20a0f5e 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java @@ -18,8 +18,7 @@ @Component public class JwtUtil { - @Value("${jwt.secret}") - private SecretKey secretKey; + private final SecretKey secretKey; @Value("${jwt.access.expiration}") private Long ACCESS_TOKEN_EXPIRED_IN; @@ -29,7 +28,7 @@ public class JwtUtil { public final String BEARER = "Bearer "; - public JwtUtil(@Value("${secret.jwt-secret-key}") String secret) { + public JwtUtil(@Value("${jwt.secret}") String secret) { this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } From 1788d85a6dfcb15009c00be341e9558749011dc6 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Tue, 23 Sep 2025 23:22:38 +0900 Subject: [PATCH 10/36] =?UTF-8?q?[Feat]=20#3=20UserPrincipal=EC=97=90=20em?= =?UTF-8?q?ail=EA=B3=BC=20providerId=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/filter/JwtAuthenticationFilter.java | 22 +++++--- .../domain/auth/model/UserPrincipal.java | 4 ++ .../service/CustomUserDetailsService.java | 2 + .../domain/auth/util/JwtUtil.java | 26 +++++---- .../WhoIs_Server/domain/user/model/User.java | 2 +- .../global/config/SecurityConfig.java | 54 ++++++++++++++++++- src/main/resources/application.yml | 2 + 7 files changed, 92 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java index f311e08..7506cea 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java @@ -3,6 +3,8 @@ import com.WhoIsRoom.WhoIs_Server.domain.auth.model.UserPrincipal; import com.WhoIsRoom.WhoIs_Server.domain.auth.service.JwtService; import com.WhoIsRoom.WhoIs_Server.domain.auth.util.JwtUtil; +import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; +import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -11,6 +13,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; @@ -21,6 +24,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Optional; @Slf4j @Component @@ -46,10 +50,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } log.info("Request URI: {}", request.getRequestURI()); // 요청 URI 로깅 - String accessToken = jwtUtil.resolveAccessToken(request); + String accessToken = jwtUtil.extractAccessToken(request) + .orElseThrow(new AuthenticationException(ErrorCode.)) // 엑세스 토큰이 없으면 Authentication도 없음 -> EntryPoint (401) - if(accessToken == null) { + if(accessToken.isEmpty()) { log.info("JWT Filter Pass (accessToken is null) : {}", request.getRequestURI()); SecurityContextHolder.clearContext(); filterChain.doFilter(request, response); @@ -61,7 +66,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse // 토큰 타입 검사 if(!"access".equals(jwtUtil.getTokenType(accessToken))) { - throw new JwtException(BaseResponseStatus.INVALID_TOKEN_TYPE); + throw new JwtException(ErrorCode.INVALID_TOKEN_TYPE); } jwtService.checkLogout(request); @@ -70,18 +75,21 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse List authorities = Arrays.asList(new SimpleGrantedAuthority(jwtUtil.getRole(accessToken))); log.info("Granted Authorities : {}", authorities); UserPrincipal principal = new UserPrincipal( - jwtUtil.getMemberId(accessToken), + jwtUtil.getUserId(accessToken), + jwtUtil.getName(accessToken), jwtUtil.getEmail(accessToken), null, // 패스워드는 필요 없음 + jwtUtil.getProviderId(accessToken), authorities ); - log.info("UserPrincipal created: {}", principal); // 생성된 사용자 정보 로깅 + log.info("UserPrincipal.userId: {}", principal.getUserId()); + log.info("UserPrincipal.name: {}", principal.getUsername()); + log.info("UserPrincipal.email: {}", principal.getEmail()); log.info("UserPrincipal.providerId: {}", principal.getProviderId()); log.info("UserPrincipal.role: {}", principal.getAuthorities().stream().findFirst().get().toString()); - log.info("UserPrincipal.memberId: {}", principal.getUserId()); Authentication authToken = null; - if ("localhost".equals(loginProvider)) { + if ("localhost".equals(principal.getProviderId())) { // 폼 로그인(자체 회원) authToken = new UsernamePasswordAuthenticationToken(principal, null, authorities); } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/model/UserPrincipal.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/model/UserPrincipal.java index 59e32ca..e3f6849 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/model/UserPrincipal.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/model/UserPrincipal.java @@ -1,17 +1,21 @@ package com.WhoIsRoom.WhoIs_Server.domain.auth.model; +import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; +@Getter @RequiredArgsConstructor public class UserPrincipal implements UserDetails{ private final Long userId; private final String username; + private final String email; private final String password; + private final String providerId; private final Collection authorities; /** UserDetails 구현 */ diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java index a59ff9c..7595055 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java @@ -27,8 +27,10 @@ public UserPrincipal loadUserByUsername(String email) throws UsernameNotFoundExc return new UserPrincipal( user.getId(), + user.getNickName(), user.getEmail(), user.getPassword(), + "localhost", Collections.singleton(user.getRole().toAuthority()) ); } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java index 20a0f5e..5f9d4c1 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java @@ -26,14 +26,20 @@ public class JwtUtil { @Value("${jwt.refresh.expiration}") private Long REFRESH_TOKEN_EXPIRED_IN; + @Value("$jwt.access.header") + private String ACCESS_HEADER; + + @Value("$jwt.refresh.header") + private String REFRESH_HEADER; + public final String BEARER = "Bearer "; public JwtUtil(@Value("${jwt.secret}") String secret) { this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } - public Long getMemberId(String token) { - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("memberId", Long.class); + public Long getUserId(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("userId", Long.class); } public String getProviderId(String token) { @@ -60,11 +66,11 @@ public Boolean isTokenExpired(String token) { return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date()); } - public String createAccessToken(Long memberId, String providerId, String role, String name) { + public String createAccessToken(Long userId, String providerId, String role, String name) { return Jwts.builder() .claim("tokenType", "access") - .claim("memberId", memberId) + .claim("userId", userId) .claim("providerId", providerId) .claim("role", role) .claim("name", name) @@ -74,11 +80,11 @@ public String createAccessToken(Long memberId, String providerId, String role, S .compact(); } - public String createRefreshToken(Long memberId, String providerId, String role) { + public String createRefreshToken(Long userId, String providerId, String role) { return Jwts.builder() .claim("tokenType", "refresh") - .claim("memberId", memberId) + .claim("userId", userId) .claim("providerId", providerId) .claim("role", role) .issuedAt(new Date(System.currentTimeMillis())) @@ -113,14 +119,14 @@ public String getUserNameFromToken(String token) { return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("name", String.class); } - public Optional extractAccessToken(HttpServletRequest request, String accessHeader) { - return Optional.ofNullable(request.getHeader(accessHeader)) + public Optional extractAccessToken(HttpServletRequest request) { + return Optional.ofNullable(request.getHeader(ACCESS_HEADER)) .filter(accessToken -> accessToken.startsWith(BEARER)) .map(accessToken -> accessToken.replace(BEARER, "")); } - public Optional extractRefreshToken(HttpServletRequest request, String refreshHeader) { - return Optional.ofNullable(request.getHeader(refreshHeader)) + public Optional extractRefreshToken(HttpServletRequest request) { + return Optional.ofNullable(request.getHeader(REFRESH_HEADER)) .filter(refreshToken -> refreshToken.startsWith(BEARER)) .map(refreshToken -> refreshToken.replace(BEARER, "")); } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/model/User.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/model/User.java index e57ef5b..f9cb714 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/model/User.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/model/User.java @@ -21,7 +21,7 @@ public class User extends BaseEntity { @Column(name = "password", length = 200, nullable = false) private String password; - @Column(name = "nickname", length = 30, nullable = false) + @Column(name = "nickname", length = 50, nullable = false, unique = true) private String nickName; @Enumerated(EnumType.STRING) diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java index d306c60..ee3dea7 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java @@ -1,21 +1,71 @@ package com.WhoIsRoom.WhoIs_Server.global.config; +import com.WhoIsRoom.WhoIs_Server.domain.auth.filter.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; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration +@EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { + // Custom한 것들은 Component로 주입하기 (싱글턴 Bean) + // private final CustomOAuth2UserService customOuth2UserService; + private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler; + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + private final FilterExceptionHandler filterExceptionHandler; + private final JwtAuthenticationFilter jwtAuthenticationFilter; + @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception { http + .cors(cors -> cors.disable()) .csrf(csrf -> csrf.disable()) + .httpBasic(httpBasic -> httpBasic.disable()); + + http + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(filterExceptionHandler, JwtAuthenticationFilter.class); + + http + .exceptionHandling(exception -> exception + .authenticationEntryPoint(customAuthenticationEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler) + ); + + http .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/**").permitAll() // 모든 인증 없이 접근 허용 + .requestMatchers("/**").permitAll() + // 나머지 모든 요청은 인증 필요 .anyRequest().authenticated() ); + + // 일반 ID/비밀번호 로그인 설정 + http + .formLogin(formLogin -> formLogin + .loginPage("/login") + .loginProcessingUrl("/login-process") // 로그인 폼 제출 처리 URL + .usernameParameter("email") + .passwordParameter("password") + .successHandler(customAuthenticationSuccessHandler) // 일반 로그인도 동일한 성공 핸들러 사용 + .permitAll() // 로그인 페이지는 모두 접근 가능 + ); + + // OAuth2 소셜 로그인 설정 +// http +// .oauth2Login(oauth2Login -> oauth2Login +// .loginPage("/login") +// .userInfoEndpoint(userInfo -> userInfo +// .userService(customOAuth2UserService) // OAuth2 사용자 정보 처리 +// ) +// .successHandler(customAuthenticationSuccessHandler) // 소셜 로그인도 동일한 성공 핸들러 사용 +// ); return http.build(); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8d1d18b..20fd30f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -76,8 +76,10 @@ jwt: secret: ${JWT_SECRET_KEY} access: expiration: ${JWT_ACCESS_EXPIRED_IN} + header: Authorization refresh: expiration: ${JWT_REFRESH_EXPIRED_IN} + header: Authorization-refresh --- # 개발용 포트 From c447bb80fb2413cc5ddfc1ac451090d991cf4485 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Tue, 23 Sep 2025 23:47:24 +0900 Subject: [PATCH 11/36] [Feat] #3 CustomAuthenticationException --- .../CustomAuthenticationException.java | 15 +++++++++++++ .../auth/filter/JwtAuthenticationFilter.java | 21 ++++++++----------- .../domain/auth/service/JwtService.java | 6 ++++-- .../global/common/response/ErrorCode.java | 17 ++++++++++++++- 4 files changed, 44 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/exception/CustomAuthenticationException.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/exception/CustomAuthenticationException.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/exception/CustomAuthenticationException.java new file mode 100644 index 0000000..f878254 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/exception/CustomAuthenticationException.java @@ -0,0 +1,15 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.exception; + +import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; +import lombok.Getter; +import org.springframework.security.core.AuthenticationException; + +@Getter +public class CustomAuthenticationException extends AuthenticationException { + private final ErrorCode errorCode; + + public CustomAuthenticationException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java index 7506cea..8deda45 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java @@ -1,5 +1,6 @@ package com.WhoIsRoom.WhoIs_Server.domain.auth.filter; +import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomAuthenticationException; import com.WhoIsRoom.WhoIs_Server.domain.auth.model.UserPrincipal; import com.WhoIsRoom.WhoIs_Server.domain.auth.service.JwtService; import com.WhoIsRoom.WhoIs_Server.domain.auth.util.JwtUtil; @@ -11,9 +12,10 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; @@ -21,6 +23,7 @@ import org.springframework.web.filter.GenericFilterBean; import org.springframework.web.filter.OncePerRequestFilter; +import javax.security.sasl.AuthenticationException; import java.io.IOException; import java.util.Arrays; import java.util.List; @@ -49,27 +52,21 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse return; } + // 엑세스 토큰이 없으면 Authentication도 없음 -> EntryPoint (401) log.info("Request URI: {}", request.getRequestURI()); // 요청 URI 로깅 String accessToken = jwtUtil.extractAccessToken(request) - .orElseThrow(new AuthenticationException(ErrorCode.)) - - // 엑세스 토큰이 없으면 Authentication도 없음 -> EntryPoint (401) - if(accessToken.isEmpty()) { - log.info("JWT Filter Pass (accessToken is null) : {}", request.getRequestURI()); - SecurityContextHolder.clearContext(); - filterChain.doFilter(request, response); - return; - } + .orElseThrow(() -> new CustomAuthenticationException(ErrorCode.SECURITY_UNAUTHORIZED)); // 토큰 유효성 검사 jwtUtil.validateToken(accessToken); // 토큰 타입 검사 if(!"access".equals(jwtUtil.getTokenType(accessToken))) { - throw new JwtException(ErrorCode.INVALID_TOKEN_TYPE); + throw new BadCredentialsException(ErrorCode.INVALID_TOKEN_TYPE.getMessage()); } - jwtService.checkLogout(request); + // 로그아웃 체크 + jwtService.checkLogout(accessToken); // 권한 리스트 생성 List authorities = Arrays.asList(new SimpleGrantedAuthority(jwtUtil.getRole(accessToken))); diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java index 46088d1..861e278 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java @@ -1,13 +1,16 @@ package com.WhoIsRoom.WhoIs_Server.domain.auth.service; +import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomAuthenticationException; import com.WhoIsRoom.WhoIs_Server.domain.auth.util.JwtUtil; import com.WhoIsRoom.WhoIs_Server.global.common.redis.RedisService; +import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.stereotype.Service; import java.time.Duration; @@ -44,8 +47,7 @@ public void reissueToken(HttpServletRequest request, HttpServletResponse respons reissueAndSendTokens(response, refreshToken); } - public void checkLogout(HttpServletRequest request) { - String accessToken = jwtUtil.resolveAccessToken(request); + public void checkLogout(String accessToken) { String value = redisService.getValues(accessToken); if (value.equals(LOGOUT_VALUE)) { throw new LogoutException(BaseResponseStatus.UNAUTHORIZED_ACCESS); diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java index 63a5261..1b9d7db 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java @@ -16,7 +16,22 @@ public enum ErrorCode{ ILLEGAL_ARGUMENT(100, BAD_REQUEST.value(), "잘못된 요청값입니다."), NOT_FOUND(101, HttpStatus.NOT_FOUND.value(), "존재하지 않는 API 입니다."), METHOD_NOT_ALLOWED(102, HttpStatus.METHOD_NOT_ALLOWED.value(), "유효하지 않은 Http 메서드입니다."), - SERVER_ERROR(103, INTERNAL_SERVER_ERROR.value(), "서버에 오류가 발생했습니다."); + SERVER_ERROR(103, INTERNAL_SERVER_ERROR.value(), "서버에 오류가 발생했습니다."), + + // Auth + SECURITY_UNAUTHORIZED(600,HttpStatus.UNAUTHORIZED.value(), "인증 정보가 유효하지 않습니다"), + INVALID_TOKEN_TYPE(601, HttpStatus.UNAUTHORIZED.value(), "토큰 타입이 유효하지 않습니다."), + SECURITY_INVALID_REFRESH_TOKEN(602, HttpStatus.UNAUTHORIZED.value(), "refresh token이 유효하지 않습니다."), + SECURITY_INVALID_ACCESS_TOKEN(603, HttpStatus.UNAUTHORIZED.value(), "access token이 유효하지 않습니다."), + SECURITY_ACCESS_DENIED(604, HttpStatus.UNAUTHORIZED.value(), "접근 권한이 없습니다."), + REFRESH_TOKEN_REQUIRED(605, BAD_REQUEST.value(), "refresh token이 필요합니다."), + MAIL_SEND_FAILED(606, BAD_REQUEST.value(), "메일 전송에 실패했습니다."), + INVALID_EMAIL_CODE(607, BAD_REQUEST.value(), "인증 번호가 다릅니다."), + EXPIRED_EMAIL_CODE(608, BAD_REQUEST.value(), "인증 번호가 만료되었거나 없습니다."), + AUTHCODE_ALREADY_AUTHENTICATED(609, BAD_REQUEST.value(), "이미 인증이 된 번호입니다."), + AUTHCODE_UNAUTHORIZED(610, HttpStatus.UNAUTHORIZED.value(), "이메일 인증을 하지 않았습니다."), + LOGIN_FAILED(611, BAD_REQUEST.value(), "이메일 혹은 비밀번호가 올바르지 않습니다."); + private final int code; private final int httpStatus; From a3c47cdcac3db6be3a0c5c249dac8ea45cb2b4d3 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Tue, 23 Sep 2025 23:51:07 +0900 Subject: [PATCH 12/36] [Feat] #3 CustomJwtException --- .../domain/auth/exception/CustomJwtException.java | 15 +++++++++++++++ .../auth/filter/JwtAuthenticationFilter.java | 5 +++-- 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/exception/CustomJwtException.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/exception/CustomJwtException.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/exception/CustomJwtException.java new file mode 100644 index 0000000..e0173a6 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/exception/CustomJwtException.java @@ -0,0 +1,15 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.exception; + +import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; +import io.jsonwebtoken.JwtException; +import lombok.Getter; + +@Getter +public class CustomJwtException extends JwtException { + private final ErrorCode errorCode; + + public CustomJwtException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java index 8deda45..81a6068 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java @@ -1,6 +1,7 @@ package com.WhoIsRoom.WhoIs_Server.domain.auth.filter; import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomAuthenticationException; +import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomJwtException; import com.WhoIsRoom.WhoIs_Server.domain.auth.model.UserPrincipal; import com.WhoIsRoom.WhoIs_Server.domain.auth.service.JwtService; import com.WhoIsRoom.WhoIs_Server.domain.auth.util.JwtUtil; @@ -62,7 +63,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse // 토큰 타입 검사 if(!"access".equals(jwtUtil.getTokenType(accessToken))) { - throw new BadCredentialsException(ErrorCode.INVALID_TOKEN_TYPE.getMessage()); + throw new CustomJwtException(ErrorCode.INVALID_TOKEN_TYPE); } // 로그아웃 체크 @@ -94,7 +95,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse // // 소셜 로그인 // authToken = new OAuth2AuthenticationToken(principal, authorities, loginProvider); // } - log.info("Authentication set in SecurityContext: {}", SecurityContextHolder.getContext().getAuthentication()); // SecurityContext 설정 확인 로깅 + log.info("Authentication set in SecurityContext: {}", SecurityContextHolder.getContext().getAuthentication()); log.info("Authorities in SecurityContext: {}", authToken.getAuthorities()); log.info("JWT Filter Success : {}", request.getRequestURI()); From cced5c9578961ae61bc2ba87812c78aad5d4657a Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Tue, 23 Sep 2025 23:58:26 +0900 Subject: [PATCH 13/36] [Feat] #3 CustomAuthenticationEntryPoint --- .../CustomAuthenticationEntryPoint.java | 46 +++++++++++++++++++ .../global/config/SecurityConfig.java | 1 + 2 files changed, 47 insertions(+) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..e0d2f5a --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java @@ -0,0 +1,46 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception; + +import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomAuthenticationException; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + log.info("=== AuthenticationEntryPoint 진입 ==="); + BaseResponseStatus status = BaseResponseStatus.UNAUTHORIZED_ACCESS; + + if (authException instanceof CustomAuthenticationException e) { + code = e.getErrorCode(); // 커스텀 코드 사용 + } + setErrorResponse(response, status); + } +} +/** + * [CustomAuthenticationEntryPoint] + * + * 📌 Spring Security에서 인증(Authentication)에 실패했을 때 호출되는 진입점 클래스입니다. + * + * ✅ 주요 처리 대상: + * - Spring Security 내부에서 발생한 AuthenticationException + * (ex. UsernameNotFoundException, BadCredentialsException, 인증 객체 없음 등) + * + * ✅ 동작 방식: + * - 인증되지 않은 사용자가 보호된 리소스에 접근 시 + * - Spring Security의 ExceptionTranslationFilter가 감지 + * - 이 EntryPoint의 commence() 메서드가 호출됨 + * - 401 Unauthorized 상태 코드와 공통 JSON 에러 응답 반환 + * + * ✅ 처리하지 않는 예외: + * - 필터 단계에서 발생한 JwtException 은 ExceptionHandlerFilter에서 처리됨 + **/ diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java index ee3dea7..e1e60fd 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java @@ -1,6 +1,7 @@ package com.WhoIsRoom.WhoIs_Server.global.config; import com.WhoIsRoom.WhoIs_Server.domain.auth.filter.JwtAuthenticationFilter; +import com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception.CustomAuthenticationEntryPoint; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; From b313a02b6021881e7b6ca8520d47d2403bd77e51 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 00:04:54 +0900 Subject: [PATCH 14/36] [Feat] #3 SecurityErrorResponseUtil --- .../CustomAuthenticationEntryPoint.java | 7 ++++-- .../auth/util/SecurityErrorResponseUtil.java | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/SecurityErrorResponseUtil.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java index e0d2f5a..65291fc 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java @@ -1,6 +1,7 @@ package com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception; import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomAuthenticationException; +import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -11,6 +12,8 @@ import java.io.IOException; +import static com.WhoIsRoom.WhoIs_Server.domain.auth.util.SecurityErrorResponseUtil.setErrorResponse; + @Slf4j @Component public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @@ -18,12 +21,12 @@ public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { log.info("=== AuthenticationEntryPoint 진입 ==="); - BaseResponseStatus status = BaseResponseStatus.UNAUTHORIZED_ACCESS; + ErrorCode code = ErrorCode.SECURITY_UNAUTHORIZED; if (authException instanceof CustomAuthenticationException e) { code = e.getErrorCode(); // 커스텀 코드 사용 } - setErrorResponse(response, status); + setErrorResponse(response, code); } } /** diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/SecurityErrorResponseUtil.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/SecurityErrorResponseUtil.java new file mode 100644 index 0000000..d95fc7d --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/SecurityErrorResponseUtil.java @@ -0,0 +1,25 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.util; + +import com.WhoIsRoom.WhoIs_Server.global.common.response.BaseErrorResponse; +import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class SecurityErrorResponseUtil { + private static final ObjectMapper objectMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + + public static void setErrorResponse(HttpServletResponse response, ErrorCode errorCode) throws IOException { + response.setStatus(errorCode.getHttpStatus()); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + BaseErrorResponse errorResponse = new BaseErrorResponse(errorCode); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } +} From efa657754ed0038aab22282e2d02192a69fac19e Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 00:14:22 +0900 Subject: [PATCH 15/36] [Feat] #3 JwtExceptionHandlerFilter --- .../CustomAuthenticationEntryPoint.java | 1 + .../exception/JwtExceptionHandlerFilter.java | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/JwtExceptionHandlerFilter.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java index 65291fc..e3b8145 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java @@ -21,6 +21,7 @@ public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { log.info("=== AuthenticationEntryPoint 진입 ==="); + ErrorCode code = ErrorCode.SECURITY_UNAUTHORIZED; if (authException instanceof CustomAuthenticationException e) { diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/JwtExceptionHandlerFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/JwtExceptionHandlerFilter.java new file mode 100644 index 0000000..eda49f0 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/JwtExceptionHandlerFilter.java @@ -0,0 +1,33 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception; + +import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomJwtException; +import com.WhoIsRoom.WhoIs_Server.global.common.exception.BusinessException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static com.WhoIsRoom.WhoIs_Server.domain.auth.util.SecurityErrorResponseUtil.setErrorResponse; + +@Slf4j +@Component +public class JwtExceptionHandlerFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + log.info("=== JwtExceptionHandlerFilter 진입 ==="); + + try { + filterChain.doFilter(request, response); + } catch (CustomJwtException e) { + setErrorResponse(response, e.getErrorCode()); + } catch (BusinessException e) { + setErrorResponse(response, e.getErrorCode()); + } + } +} From de562bf5a07210074e792bf675f7ceab94adeb3b Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 00:25:20 +0900 Subject: [PATCH 16/36] [Feat] #3 CustomAccessDeniedHandler --- .../exception/CustomAccessDeniedHandler.java | 31 +++++++++++++++++++ .../CustomAuthenticationEntryPoint.java | 2 +- .../domain/auth/service/JwtService.java | 7 +++-- .../domain/auth/util/JwtUtil.java | 13 ++++---- .../global/common/response/ErrorCode.java | 2 +- .../global/config/SecurityConfig.java | 4 ++- 6 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAccessDeniedHandler.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAccessDeniedHandler.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..74cf0bf --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAccessDeniedHandler.java @@ -0,0 +1,31 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception; + +import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomAuthenticationException; +import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Map; + +import static com.WhoIsRoom.WhoIs_Server.domain.auth.util.SecurityErrorResponseUtil.setErrorResponse; + +@Slf4j +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException{ + + log.info("=== AccessDeniedHandler 진입 ==="); + + ErrorCode code = ErrorCode.SECURITY_ACCESS_DENIED; + setErrorResponse(response, code); + } +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java index e3b8145..488663b 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomAuthenticationEntryPoint.java @@ -19,7 +19,7 @@ public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException{ log.info("=== AuthenticationEntryPoint 진입 ==="); ErrorCode code = ErrorCode.SECURITY_UNAUTHORIZED; diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java index 861e278..c5e55d7 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java @@ -1,6 +1,7 @@ package com.WhoIsRoom.WhoIs_Server.domain.auth.service; import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomAuthenticationException; +import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomJwtException; import com.WhoIsRoom.WhoIs_Server.domain.auth.util.JwtUtil; import com.WhoIsRoom.WhoIs_Server.global.common.redis.RedisService; import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; @@ -60,7 +61,7 @@ public void storeRefreshToken(String refreshToken) { private void deleteRefreshToken(String refreshToken){ if(refreshToken == null){ - throw new JwtException(BaseResponseStatus.EMPTY_REFRESH_HEADER); + throw new CustomJwtException(BaseResponseStatus.EMPTY_REFRESH_HEADER); } redisService.delete(refreshToken); } @@ -73,8 +74,8 @@ private void invalidAccessToken(String accessToken) { private void reissueAndSendTokens(HttpServletResponse response, String refreshToken) { // 새로운 Refresh Token 발급 - String reissuedAccessToken = jwtUtil.createAccessToken(jwtUtil.getMemberId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken), jwtUtil.getName(refreshToken)); - String reissuedRefreshToken = jwtUtil.createRefreshToken(jwtUtil.getMemberId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken)); + String reissuedAccessToken = jwtUtil.createAccessToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken), jwtUtil.getName(refreshToken)); + String reissuedRefreshToken = jwtUtil.createRefreshToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken)); // 새로운 Refresh Token을 DB나 Redis에 저장 storeRefreshToken(reissuedRefreshToken); diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java index 5f9d4c1..e110c73 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java @@ -1,5 +1,6 @@ package com.WhoIsRoom.WhoIs_Server.domain.auth.util; +import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomJwtException; import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; @@ -101,17 +102,17 @@ public void validateToken(String token) { .parseSignedClaims(token) .getPayload(); } catch (ExpiredJwtException e) { // 토큰 만료 - throw new JwtException(ErrorCode.EXPIRED_ACCESS_TOKEN); + throw new CustomJwtException(ErrorCode.EXPIRED_ACCESS_TOKEN); } catch (UnsupportedJwtException e) { // 지원되지 않는 형식 - throw new JwtException(ErrorCode.UNSUPPORTED_TOKEN_TYPE); + throw new CustomJwtException(ErrorCode.UNSUPPORTED_TOKEN_TYPE); } catch (MalformedJwtException e) { // 구조가 잘못된 토큰 - throw new JwtException(ErrorCode.MALFORMED_TOKEN_TYPE); + throw new CustomJwtException(ErrorCode.MALFORMED_TOKEN_TYPE); } catch (SignatureException e) { // 서명 위조 (곧 지원 중단) - throw new JwtException(ErrorCode.INVALID_SIGNATURE_JWT); + throw new CustomJwtException(ErrorCode.INVALID_SIGNATURE_JWT); } catch (IllegalArgumentException e) { // 토큰이 비어 있거나 Null - throw new JwtException(ErrorCode.EMPTY_AUTHORIZATION_HEADER); + throw new CustomJwtException(ErrorCode.EMPTY_AUTHORIZATION_HEADER); } catch (Exception e) { // 기타 예외 상황 - throw new JwtException(ErrorCode.INVALID_ACCESS_TOKEN); + throw new CustomJwtException(ErrorCode.INVALID_ACCESS_TOKEN); } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java index 1b9d7db..59573d1 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java @@ -23,7 +23,7 @@ public enum ErrorCode{ INVALID_TOKEN_TYPE(601, HttpStatus.UNAUTHORIZED.value(), "토큰 타입이 유효하지 않습니다."), SECURITY_INVALID_REFRESH_TOKEN(602, HttpStatus.UNAUTHORIZED.value(), "refresh token이 유효하지 않습니다."), SECURITY_INVALID_ACCESS_TOKEN(603, HttpStatus.UNAUTHORIZED.value(), "access token이 유효하지 않습니다."), - SECURITY_ACCESS_DENIED(604, HttpStatus.UNAUTHORIZED.value(), "접근 권한이 없습니다."), + SECURITY_ACCESS_DENIED(604, HttpStatus.FORBIDDEN.value(), "접근 권한이 없습니다."), REFRESH_TOKEN_REQUIRED(605, BAD_REQUEST.value(), "refresh token이 필요합니다."), MAIL_SEND_FAILED(606, BAD_REQUEST.value(), "메일 전송에 실패했습니다."), INVALID_EMAIL_CODE(607, BAD_REQUEST.value(), "인증 번호가 다릅니다."), diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java index e1e60fd..b3c01c4 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java @@ -1,7 +1,9 @@ package com.WhoIsRoom.WhoIs_Server.global.config; import com.WhoIsRoom.WhoIs_Server.domain.auth.filter.JwtAuthenticationFilter; +import com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception.CustomAccessDeniedHandler; import com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception.CustomAuthenticationEntryPoint; +import com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception.JwtExceptionHandlerFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -20,7 +22,7 @@ public class SecurityConfig { private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler; private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; - private final FilterExceptionHandler filterExceptionHandler; + private final JwtExceptionHandlerFilter filterExceptionHandler; private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean From 4438f947decf838d6b041a7723fee55c2455dc94 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 00:33:34 +0900 Subject: [PATCH 17/36] [Feat] #3 CustomAuthenticationSuccessHandler --- .../CustomAuthenticationSuccessHandler.java | 47 +++++++++++++++++++ .../global/config/SecurityConfig.java | 5 +- 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java new file mode 100644 index 0000000..619e9fa --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java @@ -0,0 +1,47 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.handler.success; + +import com.WhoIsRoom.WhoIs_Server.domain.auth.util.JwtUtil; +import com.WhoIsRoom.WhoIs_Server.domain.auth.service.JwtService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final AuthenticationUtil authenticationUtil; + private final JwtUtil jwtUtil; + private final JwtService jwtService; + + @Override + @Transactional + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + + String providerId = authenticationUtil.getProviderId(); + String role = authenticationUtil.getRole(); + Long memberId = authenticationUtil.getMemberId(); + String userName = authenticationUtil.getUsername(); + String email = authenticationUtil.getEmail(); + log.info("[CustomAuthenticationSuccessHandler] providerId={}, role={}, memberId={}, email={}", providerId, role, memberId, email); + + // 토큰 생성 + String accessToken = jwtUtil.createAccessToken(memberId, providerId, role, userName); + String refreshToken = jwtUtil.createRefreshToken(memberId, providerId, role); + + // refresh token 저장 + jwtService.storeRefreshToken(refreshToken); + log.info("[CustomAuthenticationSuccessHandler], refreshToken={}", refreshToken); + + // 리다이렉션 + response.sendRedirect(LOGIN_SUCCESS_URI); + } +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java index b3c01c4..5c14c99 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java @@ -4,6 +4,7 @@ import com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception.CustomAccessDeniedHandler; import com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception.CustomAuthenticationEntryPoint; import com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception.JwtExceptionHandlerFilter; +import com.WhoIsRoom.WhoIs_Server.domain.auth.handler.success.CustomAuthenticationSuccessHandler; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -22,7 +23,7 @@ public class SecurityConfig { private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler; private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; - private final JwtExceptionHandlerFilter filterExceptionHandler; + private final JwtExceptionHandlerFilter jwtExceptionHandlerFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean @@ -34,7 +35,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticat http .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(filterExceptionHandler, JwtAuthenticationFilter.class); + .addFilterBefore(jwtExceptionHandlerFilter, JwtAuthenticationFilter.class); http .exceptionHandling(exception -> exception From ad68dd9fae97f1bee3ef9e9c5b7d9fb6488e63c5 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 00:37:12 +0900 Subject: [PATCH 18/36] [Feat] #3 AuthenticationUtil --- .../CustomAuthenticationSuccessHandler.java | 1 + .../domain/auth/util/AuthenticationUtil.java | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/AuthenticationUtil.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java index 619e9fa..6ad5316 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java @@ -1,5 +1,6 @@ package com.WhoIsRoom.WhoIs_Server.domain.auth.handler.success; +import com.WhoIsRoom.WhoIs_Server.domain.auth.util.AuthenticationUtil; import com.WhoIsRoom.WhoIs_Server.domain.auth.util.JwtUtil; import com.WhoIsRoom.WhoIs_Server.domain.auth.service.JwtService; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/AuthenticationUtil.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/AuthenticationUtil.java new file mode 100644 index 0000000..0207000 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/AuthenticationUtil.java @@ -0,0 +1,46 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.util; + +import com.WhoIsRoom.WhoIs_Server.domain.auth.model.UserPrincipal; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Iterator; + +@Component +public class AuthenticationUtil { + + public String getProviderId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); + return principal.getProviderId(); + } + + public String getRole() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Collection authorities = authentication.getAuthorities(); + Iterator iterator = authorities.iterator(); + GrantedAuthority grantedAuthority = iterator.next(); + return grantedAuthority.getAuthority(); + } + + public Long getMemberId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); + return principal.getUserId(); + } + + public String getUsername() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); + return principal.getUsername(); + } + + public String getEmail(){ + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); + return principal.getEmail(); + } +} From bb97c2490d5d80607334bdfc0187c96a19d82b96 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 00:55:18 +0900 Subject: [PATCH 19/36] =?UTF-8?q?[Fix]=20#3=20SuccessHandler=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=EC=9D=84=20=ED=97=A4=EB=8D=94=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomAuthenticationSuccessHandler.java | 7 +++---- .../domain/auth/service/JwtService.java | 21 ++++++++++++------- .../domain/auth/util/JwtUtil.java | 8 ++++--- .../global/common/response/ErrorCode.java | 17 +++++++++------ 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java index 6ad5316..218521d 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java @@ -35,14 +35,13 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo log.info("[CustomAuthenticationSuccessHandler] providerId={}, role={}, memberId={}, email={}", providerId, role, memberId, email); // 토큰 생성 - String accessToken = jwtUtil.createAccessToken(memberId, providerId, role, userName); - String refreshToken = jwtUtil.createRefreshToken(memberId, providerId, role); + String accessToken = jwtUtil.createAccessToken(memberId, providerId, role, userName, email); + String refreshToken = jwtUtil.createRefreshToken(memberId, providerId, role, email); // refresh token 저장 jwtService.storeRefreshToken(refreshToken); log.info("[CustomAuthenticationSuccessHandler], refreshToken={}", refreshToken); - // 리다이렉션 - response.sendRedirect(LOGIN_SUCCESS_URI); + jwtService.sendTokens(response, accessToken, refreshToken); } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java index c5e55d7..e5326bb 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java @@ -27,8 +27,15 @@ public class JwtService { @Value("${jwt.refresh.expiration}") private Long REFRESH_TOKEN_EXPIRED_IN; + @Value("$jwt.access.header") + private String ACCESS_HEADER; + + @Value("$jwt.refresh.header") + private String REFRESH_HEADER; + private static final String LOGOUT_VALUE = "logout"; private static final String REFRESH_TOKEN_KEY_PREFIX = "auth:refresh:"; + private final String BEARER = "Bearer "; private final RedisService redisService; private final JwtUtil jwtUtil; @@ -61,7 +68,7 @@ public void storeRefreshToken(String refreshToken) { private void deleteRefreshToken(String refreshToken){ if(refreshToken == null){ - throw new CustomJwtException(BaseResponseStatus.EMPTY_REFRESH_HEADER); + throw new CustomJwtException(ErrorCode.EMPTY_REFRESH_HEADER); } redisService.delete(refreshToken); } @@ -74,8 +81,8 @@ private void invalidAccessToken(String accessToken) { private void reissueAndSendTokens(HttpServletResponse response, String refreshToken) { // 새로운 Refresh Token 발급 - String reissuedAccessToken = jwtUtil.createAccessToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken), jwtUtil.getName(refreshToken)); - String reissuedRefreshToken = jwtUtil.createRefreshToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken)); + String reissuedAccessToken = jwtUtil.createAccessToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken), jwtUtil.getName(refreshToken), jwtUtil.getEmail(refreshToken)); + String reissuedRefreshToken = jwtUtil.createRefreshToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken), jwtUtil.getEmail(refreshToken)); // 새로운 Refresh Token을 DB나 Redis에 저장 storeRefreshToken(reissuedRefreshToken); @@ -86,9 +93,9 @@ private void reissueAndSendTokens(HttpServletResponse response, String refreshTo sendTokens(response, reissuedAccessToken, reissuedRefreshToken); } - private void sendTokens(HttpServletResponse response, String reissuedAccessToken, - String reissuedRefreshToken) { - response.addCookie(cookieUtil.createCookie(accessHeader, reissuedAccessToken)); - response.addCookie(cookieUtil.createCookie(refreshHeader, reissuedRefreshToken)); + public void sendTokens(HttpServletResponse response, String accessToken, + String refreshToken) { + response.setHeader(ACCESS_HEADER, BEARER + accessToken); + response.setHeader(REFRESH_HEADER, BEARER + refreshToken); } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java index e110c73..85e9e74 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java @@ -67,7 +67,7 @@ public Boolean isTokenExpired(String token) { return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date()); } - public String createAccessToken(Long userId, String providerId, String role, String name) { + public String createAccessToken(Long userId, String providerId, String role, String name, String email) { return Jwts.builder() .claim("tokenType", "access") @@ -75,19 +75,21 @@ public String createAccessToken(Long userId, String providerId, String role, Str .claim("providerId", providerId) .claim("role", role) .claim("name", name) + .claim("email", email) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRED_IN)) .signWith(secretKey) .compact(); } - public String createRefreshToken(Long userId, String providerId, String role) { + public String createRefreshToken(Long userId, String providerId, String role, String email) { return Jwts.builder() .claim("tokenType", "refresh") .claim("userId", userId) .claim("providerId", providerId) .claim("role", role) + .claim("email", email) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRED_IN)) .signWith(secretKey) @@ -112,7 +114,7 @@ public void validateToken(String token) { } catch (IllegalArgumentException e) { // 토큰이 비어 있거나 Null throw new CustomJwtException(ErrorCode.EMPTY_AUTHORIZATION_HEADER); } catch (Exception e) { // 기타 예외 상황 - throw new CustomJwtException(ErrorCode.INVALID_ACCESS_TOKEN); + throw new CustomJwtException(ErrorCode.SECURITY_INVALID_ACCESS_TOKEN); } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java index 59573d1..bee281b 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java @@ -24,13 +24,18 @@ public enum ErrorCode{ SECURITY_INVALID_REFRESH_TOKEN(602, HttpStatus.UNAUTHORIZED.value(), "refresh token이 유효하지 않습니다."), SECURITY_INVALID_ACCESS_TOKEN(603, HttpStatus.UNAUTHORIZED.value(), "access token이 유효하지 않습니다."), SECURITY_ACCESS_DENIED(604, HttpStatus.FORBIDDEN.value(), "접근 권한이 없습니다."), - REFRESH_TOKEN_REQUIRED(605, BAD_REQUEST.value(), "refresh token이 필요합니다."), - MAIL_SEND_FAILED(606, BAD_REQUEST.value(), "메일 전송에 실패했습니다."), - INVALID_EMAIL_CODE(607, BAD_REQUEST.value(), "인증 번호가 다릅니다."), - EXPIRED_EMAIL_CODE(608, BAD_REQUEST.value(), "인증 번호가 만료되었거나 없습니다."), - AUTHCODE_ALREADY_AUTHENTICATED(609, BAD_REQUEST.value(), "이미 인증이 된 번호입니다."), + EMPTY_REFRESH_HEADER(605, HttpStatus.BAD_REQUEST.value(), "refresh token이 필요합니다."), + MAIL_SEND_FAILED(606, HttpStatus.BAD_REQUEST.value(), "메일 전송에 실패했습니다."), + INVALID_EMAIL_CODE(607, HttpStatus.BAD_REQUEST.value(), "인증 번호가 다릅니다."), + EXPIRED_EMAIL_CODE(608, HttpStatus.BAD_REQUEST.value(), "인증 번호가 만료되었거나 없습니다."), + AUTHCODE_ALREADY_AUTHENTICATED(609, HttpStatus.BAD_REQUEST.value(), "이미 인증이 된 번호입니다."), AUTHCODE_UNAUTHORIZED(610, HttpStatus.UNAUTHORIZED.value(), "이메일 인증을 하지 않았습니다."), - LOGIN_FAILED(611, BAD_REQUEST.value(), "이메일 혹은 비밀번호가 올바르지 않습니다."); + LOGIN_FAILED(611, HttpStatus.BAD_REQUEST.value(), "이메일 혹은 비밀번호가 올바르지 않습니다."), + EMPTY_AUTHORIZATION_HEADER(612, HttpStatus.BAD_REQUEST.value(),"Authorization 헤더가 존재하지 않습니다."), + EXPIRED_ACCESS_TOKEN(613, HttpStatus.BAD_REQUEST.value(), "이미 만료된 Access 토큰입니다."), + UNSUPPORTED_TOKEN_TYPE(614, HttpStatus.BAD_REQUEST.value(),"지원되지 않는 토큰 형식입니다."), + MALFORMED_TOKEN_TYPE(615, HttpStatus.BAD_REQUEST.value(),"인증 토큰이 올바르게 구성되지 않았습니다."), + INVALID_SIGNATURE_JWT(616, HttpStatus.BAD_REQUEST.value(), "인증 시그니처가 올바르지 않습니다"); private final int code; From 89260c88e0a7b3b689fce5befeff608ad85ebd72 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 17:40:06 +0900 Subject: [PATCH 20/36] [Feat] #3 CustomLoginFilter --- .../domain/auth/filter/CustomLoginFilter.java | 58 +++++++++++++++++++ ...ustomJsonAuthenticationFailureHandler.java | 41 +++++++++++++ .../domain/auth/service/JwtService.java | 17 +++--- .../domain/auth/util/JwtUtil.java | 10 ++-- .../global/config/SecurityConfig.java | 49 +++++++++++----- 5 files changed, 148 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomJsonAuthenticationFailureHandler.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java new file mode 100644 index 0000000..e3db277 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java @@ -0,0 +1,58 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter { + + private final ObjectMapper objectMapper; + + public CustomLoginFilter(AuthenticationManager authenticationManager, + ObjectMapper objectMapper, + AuthenticationSuccessHandler successHandler, + AuthenticationFailureHandler failureHandler) { + this.objectMapper = objectMapper; + + // 기본 설정들 캡슐화 + super.setFilterProcessesUrl("/api/login"); // 로그인 엔드포인트 고정 + super.setUsernameParameter("email"); // username 대신 email + super.setPasswordParameter("password"); + + // AuthenticationManager + 핸들러 세팅 + super.setAuthenticationManager(authenticationManager); + super.setAuthenticationSuccessHandler(successHandler); + super.setAuthenticationFailureHandler(failureHandler); + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + + //클라이언트 요청에서 username, password 추출 + String email = obtainUsername(request); + String password = obtainPassword(request); + + //스프링 시큐리티에서 username과 password를 검증하기 위해서는 token에 담아야 함 + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, password, null); + + //token에 담은 검증을 위한 AuthenticationManager로 전달 + return this.getAuthenticationManager().authenticate(authToken); + } + + @Override + protected String obtainUsername(HttpServletRequest request) { + return request.getParameter("email"); // username 대신 email + } +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomJsonAuthenticationFailureHandler.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomJsonAuthenticationFailureHandler.java new file mode 100644 index 0000000..3857da4 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomJsonAuthenticationFailureHandler.java @@ -0,0 +1,41 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomJsonAuthenticationFailureHandler implements AuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse res, + AuthenticationException ex) throws IOException { + res.setContentType("application/json;charset=UTF-8"); + + int http = HttpServletResponse.SC_UNAUTHORIZED; // 401 기본 + String code = "INVALID_CREDENTIALS"; + String message = "이메일 또는 비밀번호가 올바르지 않습니다."; + + if (ex instanceof UsernameNotFoundException) { + code = "USER_NOT_FOUND"; + message = ex.getMessage(); + } else if (ex instanceof LockedException) { + http = 423; code = "ACCOUNT_LOCKED"; message = "잠긴 계정입니다."; + } else if (ex instanceof DisabledException) { + http = 403; code = "ACCOUNT_DISABLED"; message = "비활성화된 계정입니다."; + } else if (ex instanceof AccountExpiredException) { + http = 403; code = "ACCOUNT_EXPIRED"; message = "만료된 계정입니다."; + } else if (ex instanceof CredentialsExpiredException) { + code = "CREDENTIALS_EXPIRED"; message = "비밀번호가 만료되었습니다."; + } + + res.setStatus(http); + res.getWriter().write(""" + {"success":false,"code":"%s","message":"%s"} + """.formatted(code, message)); + } +} \ No newline at end of file diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java index e5326bb..75e4331 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java @@ -35,14 +35,16 @@ public class JwtService { private static final String LOGOUT_VALUE = "logout"; private static final String REFRESH_TOKEN_KEY_PREFIX = "auth:refresh:"; - private final String BEARER = "Bearer "; + private final String BEARER_PREFIX = "Bearer "; private final RedisService redisService; private final JwtUtil jwtUtil; public void logout(HttpServletRequest request) { - String accessToken = jwtUtil.resolveAccessToken(request); - String refreshToken = jwtUtil.resolveRefreshToken(request); + String accessToken = jwtUtil.extractAccessToken(request) + .orElseThrow(() -> new CustomAuthenticationException(ErrorCode.SECURITY_INVALID_ACCESS_TOKEN)); + String refreshToken = jwtUtil.extractRefreshToken(request) + .orElseThrow(() -> new CustomAuthenticationException(ErrorCode.SECURITY_INVALID_REFRESH_TOKEN)); deleteRefreshToken(refreshToken); //access token blacklist 처리 -> 로그아웃한 사용자가 요청 시 access token이 redis에 존재하면 jwtAuthenticationFilter에서 인증처리 거부 @@ -50,7 +52,8 @@ public void logout(HttpServletRequest request) { } public void reissueToken(HttpServletRequest request, HttpServletResponse response) { - String refreshToken = jwtUtil.resolveRefreshToken(request); + String refreshToken = jwtUtil.extractRefreshToken(request) + .orElseThrow(() -> new CustomAuthenticationException(ErrorCode.SECURITY_INVALID_REFRESH_TOKEN)); jwtUtil.validateToken(refreshToken); reissueAndSendTokens(response, refreshToken); } @@ -58,7 +61,7 @@ public void reissueToken(HttpServletRequest request, HttpServletResponse respons public void checkLogout(String accessToken) { String value = redisService.getValues(accessToken); if (value.equals(LOGOUT_VALUE)) { - throw new LogoutException(BaseResponseStatus.UNAUTHORIZED_ACCESS); + throw new CustomAuthenticationException(ErrorCode.SECURITY_UNAUTHORIZED); } } @@ -95,7 +98,7 @@ private void reissueAndSendTokens(HttpServletResponse response, String refreshTo public void sendTokens(HttpServletResponse response, String accessToken, String refreshToken) { - response.setHeader(ACCESS_HEADER, BEARER + accessToken); - response.setHeader(REFRESH_HEADER, BEARER + refreshToken); + response.setHeader(ACCESS_HEADER, BEARER_PREFIX + accessToken); + response.setHeader(REFRESH_HEADER, BEARER_PREFIX + refreshToken); } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java index 85e9e74..17c0bb7 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java @@ -33,7 +33,7 @@ public class JwtUtil { @Value("$jwt.refresh.header") private String REFRESH_HEADER; - public final String BEARER = "Bearer "; + public final String BEARER_PREFIX = "Bearer "; public JwtUtil(@Value("${jwt.secret}") String secret) { this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); @@ -124,13 +124,13 @@ public String getUserNameFromToken(String token) { public Optional extractAccessToken(HttpServletRequest request) { return Optional.ofNullable(request.getHeader(ACCESS_HEADER)) - .filter(accessToken -> accessToken.startsWith(BEARER)) - .map(accessToken -> accessToken.replace(BEARER, "")); + .filter(accessToken -> accessToken.startsWith(BEARER_PREFIX)) + .map(accessToken -> accessToken.replace(BEARER_PREFIX, "")); } public Optional extractRefreshToken(HttpServletRequest request) { return Optional.ofNullable(request.getHeader(REFRESH_HEADER)) - .filter(refreshToken -> refreshToken.startsWith(BEARER)) - .map(refreshToken -> refreshToken.replace(BEARER, "")); + .filter(refreshToken -> refreshToken.startsWith(BEARER_PREFIX)) + .map(refreshToken -> refreshToken.replace(BEARER_PREFIX, "")); } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java index 5c14c99..6d1eb0d 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java @@ -1,15 +1,21 @@ package com.WhoIsRoom.WhoIs_Server.global.config; +import com.WhoIsRoom.WhoIs_Server.domain.auth.filter.CustomLoginFilter; import com.WhoIsRoom.WhoIs_Server.domain.auth.filter.JwtAuthenticationFilter; import com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception.CustomAccessDeniedHandler; import com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception.CustomAuthenticationEntryPoint; +import com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception.CustomJsonAuthenticationFailureHandler; import com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception.JwtExceptionHandlerFilter; import com.WhoIsRoom.WhoIs_Server.domain.auth.handler.success.CustomAuthenticationSuccessHandler; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 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.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -21,46 +27,44 @@ public class SecurityConfig { // Custom한 것들은 Component로 주입하기 (싱글턴 Bean) // private final CustomOAuth2UserService customOuth2UserService; private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler; + private final CustomJsonAuthenticationFailureHandler customJsonAuthenticationFailureHandler; private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; private final JwtExceptionHandlerFilter jwtExceptionHandlerFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final ObjectMapper objectMapper; + private final AuthenticationConfiguration configuration; @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomLoginFilter customLoginFilter) throws Exception { http - .cors(cors -> cors.disable()) + // 기본 옵션, 폼 로그인 비활성화 .csrf(csrf -> csrf.disable()) - .httpBasic(httpBasic -> httpBasic.disable()); + .cors(cors -> cors.disable()) + .httpBasic(b -> b.disable()) + .formLogin(fl -> fl.disable()) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); http + .addFilterBefore(jwtExceptionHandlerFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(jwtExceptionHandlerFilter, JwtAuthenticationFilter.class); + .addFilterAt(customLoginFilter, UsernamePasswordAuthenticationFilter.class); http + // 시큐리티 표준 예외 핸들러 (401/403) .exceptionHandling(exception -> exception .authenticationEntryPoint(customAuthenticationEntryPoint) .accessDeniedHandler(customAccessDeniedHandler) ); http + // 인가 규칙 .authorizeHttpRequests(authorize -> authorize .requestMatchers("/**").permitAll() // 나머지 모든 요청은 인증 필요 .anyRequest().authenticated() ); - // 일반 ID/비밀번호 로그인 설정 - http - .formLogin(formLogin -> formLogin - .loginPage("/login") - .loginProcessingUrl("/login-process") // 로그인 폼 제출 처리 URL - .usernameParameter("email") - .passwordParameter("password") - .successHandler(customAuthenticationSuccessHandler) // 일반 로그인도 동일한 성공 핸들러 사용 - .permitAll() // 로그인 페이지는 모두 접근 가능 - ); - // OAuth2 소셜 로그인 설정 // http // .oauth2Login(oauth2Login -> oauth2Login @@ -72,4 +76,19 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticat // ); return http.build(); } + + @Bean + public AuthenticationManager authenticationManager() throws Exception { + return configuration.getAuthenticationManager(); + } + + @Bean + public CustomLoginFilter customLoginFilter() throws Exception { + return new CustomLoginFilter( + authenticationManager(), + objectMapper, + customAuthenticationSuccessHandler, + customJsonAuthenticationFailureHandler + ); + } } From b8954c7f443622ba7638f4814b6557084f0089d7 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 17:55:36 +0900 Subject: [PATCH 21/36] [Feat] #3 CustomJsonAuthenticationFailureHandler --- .../domain/auth/filter/CustomLoginFilter.java | 2 + ...ustomJsonAuthenticationFailureHandler.java | 39 ++++++++----------- .../global/common/response/ErrorCode.java | 6 ++- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java index e3db277..431986f 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java @@ -40,6 +40,8 @@ public CustomLoginFilter(AuthenticationManager authenticationManager, @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + log.info("=== Login Filter 진입 ==="); + //클라이언트 요청에서 username, password 추출 String email = obtainUsername(request); String password = obtainPassword(request); diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomJsonAuthenticationFailureHandler.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomJsonAuthenticationFailureHandler.java index 3857da4..e7dd814 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomJsonAuthenticationFailureHandler.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomJsonAuthenticationFailureHandler.java @@ -1,41 +1,36 @@ package com.WhoIsRoom.WhoIs_Server.domain.auth.handler.exception; +import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AccountExpiredException; +import org.springframework.security.authentication.CredentialsExpiredException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.LockedException; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import java.io.IOException; +import static com.WhoIsRoom.WhoIs_Server.domain.auth.util.SecurityErrorResponseUtil.setErrorResponse; + +@Slf4j @Component public class CustomJsonAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override - public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse res, - AuthenticationException ex) throws IOException { - res.setContentType("application/json;charset=UTF-8"); + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authenticationException) throws IOException { + log.info("=== AuthenticationFailureHandler 진입 ==="); - int http = HttpServletResponse.SC_UNAUTHORIZED; // 401 기본 - String code = "INVALID_CREDENTIALS"; - String message = "이메일 또는 비밀번호가 올바르지 않습니다."; + ErrorCode code = ErrorCode.INVALID_ID_OR_PASSWORD; - if (ex instanceof UsernameNotFoundException) { - code = "USER_NOT_FOUND"; - message = ex.getMessage(); - } else if (ex instanceof LockedException) { - http = 423; code = "ACCOUNT_LOCKED"; message = "잠긴 계정입니다."; - } else if (ex instanceof DisabledException) { - http = 403; code = "ACCOUNT_DISABLED"; message = "비활성화된 계정입니다."; - } else if (ex instanceof AccountExpiredException) { - http = 403; code = "ACCOUNT_EXPIRED"; message = "만료된 계정입니다."; - } else if (ex instanceof CredentialsExpiredException) { - code = "CREDENTIALS_EXPIRED"; message = "비밀번호가 만료되었습니다."; + if (authenticationException instanceof UsernameNotFoundException) { + code = ErrorCode.USER_NOT_FOUND; } - - res.setStatus(http); - res.getWriter().write(""" - {"success":false,"code":"%s","message":"%s"} - """.formatted(code, message)); + setErrorResponse(response, code); } } \ No newline at end of file diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java index bee281b..5fea50b 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java @@ -18,6 +18,9 @@ public enum ErrorCode{ METHOD_NOT_ALLOWED(102, HttpStatus.METHOD_NOT_ALLOWED.value(), "유효하지 않은 Http 메서드입니다."), SERVER_ERROR(103, INTERNAL_SERVER_ERROR.value(), "서버에 오류가 발생했습니다."), + // User + USER_NOT_FOUND(200, HttpStatus.NOT_FOUND.value(), "사용자를 찾을 수 없습니다."), + // Auth SECURITY_UNAUTHORIZED(600,HttpStatus.UNAUTHORIZED.value(), "인증 정보가 유효하지 않습니다"), INVALID_TOKEN_TYPE(601, HttpStatus.UNAUTHORIZED.value(), "토큰 타입이 유효하지 않습니다."), @@ -35,7 +38,8 @@ public enum ErrorCode{ EXPIRED_ACCESS_TOKEN(613, HttpStatus.BAD_REQUEST.value(), "이미 만료된 Access 토큰입니다."), UNSUPPORTED_TOKEN_TYPE(614, HttpStatus.BAD_REQUEST.value(),"지원되지 않는 토큰 형식입니다."), MALFORMED_TOKEN_TYPE(615, HttpStatus.BAD_REQUEST.value(),"인증 토큰이 올바르게 구성되지 않았습니다."), - INVALID_SIGNATURE_JWT(616, HttpStatus.BAD_REQUEST.value(), "인증 시그니처가 올바르지 않습니다"); + INVALID_SIGNATURE_JWT(616, HttpStatus.BAD_REQUEST.value(), "인증 시그니처가 올바르지 않습니다"), + INVALID_ID_OR_PASSWORD(617, HttpStatus.BAD_REQUEST.value(), "이메일 또는 비밀번호가 올바르지 않습니다."); private final int code; From 1535ea5ee416a7e15880c785dd413a2690657144 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 18:04:04 +0900 Subject: [PATCH 22/36] [Feat] #3 AuthController --- .../auth/controller/AuthController.java | 33 +++++++++++++++++++ .../domain/auth/filter/CustomLoginFilter.java | 2 +- .../domain/auth/service/JwtService.java | 4 +-- 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/controller/AuthController.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/controller/AuthController.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/controller/AuthController.java new file mode 100644 index 0000000..5cd7dd2 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/controller/AuthController.java @@ -0,0 +1,33 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.controller; + +import com.WhoIsRoom.WhoIs_Server.domain.auth.service.JwtService; +import com.WhoIsRoom.WhoIs_Server.global.common.response.BaseResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/auth") +public class AuthController { + + private final JwtService jwtService; + + @PostMapping("/logout") + public BaseResponse logout(HttpServletRequest request){ + jwtService.logout(request); + return BaseResponse.ok(null); + } + + @PostMapping("/reissue") + public BaseResponse reissueTokens(HttpServletRequest request, HttpServletResponse response) { + jwtService.reissueTokens(request, response); + return BaseResponse.ok(null); + } +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java index 431986f..a15fc84 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java @@ -27,7 +27,7 @@ public CustomLoginFilter(AuthenticationManager authenticationManager, this.objectMapper = objectMapper; // 기본 설정들 캡슐화 - super.setFilterProcessesUrl("/api/login"); // 로그인 엔드포인트 고정 + super.setFilterProcessesUrl("/api/auth/login"); // 로그인 엔드포인트 고정 super.setUsernameParameter("email"); // username 대신 email super.setPasswordParameter("password"); diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java index 75e4331..9ab46de 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java @@ -7,11 +7,9 @@ import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.stereotype.Service; import java.time.Duration; @@ -51,7 +49,7 @@ public void logout(HttpServletRequest request) { invalidAccessToken(accessToken); } - public void reissueToken(HttpServletRequest request, HttpServletResponse response) { + public void reissueTokens(HttpServletRequest request, HttpServletResponse response) { String refreshToken = jwtUtil.extractRefreshToken(request) .orElseThrow(() -> new CustomAuthenticationException(ErrorCode.SECURITY_INVALID_REFRESH_TOKEN)); jwtUtil.validateToken(refreshToken); From 05d0dab2bd1ac01f0bbe6b97ac90589b4f300cf2 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 18:07:48 +0900 Subject: [PATCH 23/36] [Feat] #3 UserController --- .../user/controller/UserController.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/controller/UserController.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/controller/UserController.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/controller/UserController.java new file mode 100644 index 0000000..3ca7d67 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/controller/UserController.java @@ -0,0 +1,23 @@ +package com.WhoIsRoom.WhoIs_Server.domain.user.controller; + +import com.WhoIsRoom.WhoIs_Server.global.common.response.BaseResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/users") +public class UserController { + + private final UserService userService; + + @PostMapping("/signup") + public BaseResponse signUp(User user) { + userService.signUp(); + return BaseResponse.ok(null); + } +} From f28ba2fb6bff8ebbe18e0c43fe67cbb66ae95ad5 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 18:33:13 +0900 Subject: [PATCH 24/36] [Feat] #3 SignupRequest --- .../domain/user/dto/request/SignupRequest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/dto/request/SignupRequest.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/dto/request/SignupRequest.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/dto/request/SignupRequest.java new file mode 100644 index 0000000..28ebbf0 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/dto/request/SignupRequest.java @@ -0,0 +1,13 @@ +package com.WhoIsRoom.WhoIs_Server.domain.user.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SignupRequest { + private String email; + private String nickName; + private String password; +} From 78d1c7bb00ee52c1efccaa7fcdc1645812bed661 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 18:43:16 +0900 Subject: [PATCH 25/36] [Feat] #3 UserService --- .../user/controller/UserController.java | 7 ++-- .../user/repository/UserRepository.java | 2 ++ .../domain/user/service/UserService.java | 35 +++++++++++++++++++ .../global/common/response/ErrorCode.java | 6 ++-- 4 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserService.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/controller/UserController.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/controller/UserController.java index 3ca7d67..94db5b2 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/controller/UserController.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/controller/UserController.java @@ -1,9 +1,12 @@ package com.WhoIsRoom.WhoIs_Server.domain.user.controller; +import com.WhoIsRoom.WhoIs_Server.domain.user.dto.request.SignupRequest; +import com.WhoIsRoom.WhoIs_Server.domain.user.service.UserService; import com.WhoIsRoom.WhoIs_Server.global.common.response.BaseResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -16,8 +19,8 @@ public class UserController { private final UserService userService; @PostMapping("/signup") - public BaseResponse signUp(User user) { - userService.signUp(); + public BaseResponse signUp(@RequestBody SignupRequest request) { + userService.signUp(request); return BaseResponse.ok(null); } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/repository/UserRepository.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/repository/UserRepository.java index 20cdfec..6457836 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/repository/UserRepository.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/repository/UserRepository.java @@ -7,4 +7,6 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); + boolean existsByEmail(String email); + boolean existsByNickName(String nickName); } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserService.java new file mode 100644 index 0000000..5ecd7e0 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserService.java @@ -0,0 +1,35 @@ +package com.WhoIsRoom.WhoIs_Server.domain.user.service; + +import com.WhoIsRoom.WhoIs_Server.domain.user.dto.request.SignupRequest; +import com.WhoIsRoom.WhoIs_Server.domain.user.model.User; +import com.WhoIsRoom.WhoIs_Server.domain.user.repository.UserRepository; +import com.WhoIsRoom.WhoIs_Server.global.common.exception.BusinessException; +import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserService { + private final UserRepository userRepository; + + @Transactional + public void signUp(SignupRequest request) { + if (userRepository.existsByEmail(request.getEmail())) { + throw new BusinessException(ErrorCode.USER_DUPLICATE_EMAIL); + } + if (userRepository.existsByNickName(request.getNickName())) { + throw new BusinessException(ErrorCode.USER_DUPLICATE_NICKNAME); + } + + User user = User.builder() + .email(request.getEmail()) + .nickName(request.getNickName()) + .password(request.getPassword()) + .build(); + userRepository.save(user); + } +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java index 5fea50b..828eb4c 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java @@ -13,13 +13,15 @@ public enum ErrorCode{ // Common - ILLEGAL_ARGUMENT(100, BAD_REQUEST.value(), "잘못된 요청값입니다."), + ILLEGAL_ARGUMENT(100, HttpStatus.BAD_REQUEST.value(), "잘못된 요청값입니다."), NOT_FOUND(101, HttpStatus.NOT_FOUND.value(), "존재하지 않는 API 입니다."), METHOD_NOT_ALLOWED(102, HttpStatus.METHOD_NOT_ALLOWED.value(), "유효하지 않은 Http 메서드입니다."), - SERVER_ERROR(103, INTERNAL_SERVER_ERROR.value(), "서버에 오류가 발생했습니다."), + SERVER_ERROR(103, HttpStatus.INTERNAL_SERVER_ERROR.value(), "서버에 오류가 발생했습니다."), // User USER_NOT_FOUND(200, HttpStatus.NOT_FOUND.value(), "사용자를 찾을 수 없습니다."), + USER_DUPLICATE_EMAIL(201, HttpStatus.BAD_REQUEST.value(), "중복된 이메일의 시용자가 있습니다."), + USER_DUPLICATE_NICKNAME(202, HttpStatus.BAD_REQUEST.value(), "중복된 닉네임의 사용자가 있습니다."), // Auth SECURITY_UNAUTHORIZED(600,HttpStatus.UNAUTHORIZED.value(), "인증 정보가 유효하지 않습니다"), From 0a7a934abf7bade0217c6f4310c5bd3722eb76c7 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 18:45:15 +0900 Subject: [PATCH 26/36] [Feat] #3 CurrentUserId --- .../global/common/resolver/CurrentUserId.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/resolver/CurrentUserId.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/resolver/CurrentUserId.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/resolver/CurrentUserId.java new file mode 100644 index 0000000..d1d07e9 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/resolver/CurrentUserId.java @@ -0,0 +1,12 @@ +package com.WhoIsRoom.WhoIs_Server.global.common.resolver; + +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 CurrentUserId { +} + From c89a3b0420fbab030887464b38dc1c342cfaa051 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 18:47:29 +0900 Subject: [PATCH 27/36] [Feat] #3 CurrentUserIdArgumentResolver --- .../CurrentUserIdArgumentResolver.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/resolver/CurrentUserIdArgumentResolver.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/resolver/CurrentUserIdArgumentResolver.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/resolver/CurrentUserIdArgumentResolver.java new file mode 100644 index 0000000..c363b4c --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/resolver/CurrentUserIdArgumentResolver.java @@ -0,0 +1,37 @@ +package com.WhoIsRoom.WhoIs_Server.global.common.resolver; + +import com.WhoIsRoom.WhoIs_Server.domain.user.repository.UserRepository; +import com.WhoIsRoom.WhoIs_Server.global.common.exception.BusinessException; +import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; +import lombok.RequiredArgsConstructor; +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 +@RequiredArgsConstructor +public class CurrentUserIdArgumentResolver implements HandlerMethodArgumentResolver { + + private final UserRepository userRepository; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(Long.class) && parameter.hasParameterAnnotation(CurrentUserId.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + + String email = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return userRepository.findByEmail(email) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)) + .getId(); + } +} From 380265a95ef07560fd75eaa33652c87f7e25030a Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 20:02:59 +0900 Subject: [PATCH 28/36] =?UTF-8?q?[Refact]=20#3=20JwtFilter=20pass=20URI=20?= =?UTF-8?q?ANT=20=ED=8C=A8=ED=84=B4=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/filter/JwtAuthenticationFilter.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java index 81a6068..25fef88 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java @@ -21,6 +21,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; import org.springframework.web.filter.GenericFilterBean; import org.springframework.web.filter.OncePerRequestFilter; @@ -39,15 +40,15 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { // 인증을 안해도 되니 토큰이 필요없는 URL들 (에러: 로그인이 필요합니다) public final static List PASS_URIS = Arrays.asList( - "/api/login", - "/api/logout", - "/api/signup" + "/api/auth/**" ); + private static final AntPathMatcher ANT = new AntPathMatcher(); + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - if(isPassUris(request.getRequestURI())) { + if(isPassUri(request.getRequestURI())) { log.info("JWT Filter Passed (pass uri) : {}", request.getRequestURI()); filterChain.doFilter(request, response); return; @@ -103,7 +104,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse filterChain.doFilter(request, response); } - private boolean isPassUris(String uri) { - return PASS_URIS.contains(uri); + private boolean isPassUri(String uri) { + return PASS_URIS.stream().anyMatch(pattern -> ANT.match(pattern, uri)); } } From e68e0497bb5e4232a9d0172f43377dc86a1c4607 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 20:05:37 +0900 Subject: [PATCH 29/36] [Feat] #3 CorsConfig --- .../global/config/CorsConfig.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/CorsConfig.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/CorsConfig.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/CorsConfig.java new file mode 100644 index 0000000..c2cadeb --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/CorsConfig.java @@ -0,0 +1,25 @@ +package com.WhoIsRoom.WhoIs_Server.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +public class CorsConfig { + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.addAllowedOriginPattern("*"); // 모든 출처 허용 + configuration.addAllowedMethod("*"); // 모든 HTTP 메서드 허용 + configuration.addAllowedHeader("*"); // 모든 헤더 허용 + configuration.setAllowCredentials(true); // 인증 정보 허용 + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} + From fe1380efa3fde5a3f07240e6218f4c8303a36fd0 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 20:07:04 +0900 Subject: [PATCH 30/36] [Feat] #3 WebConfig --- .../WhoIs_Server/global/config/WebConfig.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/WebConfig.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/WebConfig.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/WebConfig.java new file mode 100644 index 0000000..2977fd9 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/WebConfig.java @@ -0,0 +1,22 @@ +package com.WhoIsRoom.WhoIs_Server.global.config; + +import com.WhoIsRoom.WhoIs_Server.global.common.resolver.CurrentUserIdArgumentResolver; +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 CurrentUserIdArgumentResolver CurrentUserIdArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(CurrentUserIdArgumentResolver); + } +} + From 6e35ccc9c05eee30a077be8608aa2d5dbb2a002c Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 20:51:22 +0900 Subject: [PATCH 31/36] [Feat] #3 LoginRequest --- .../domain/auth/dto/LoginRequest.java | 12 ++++ .../domain/auth/filter/CustomLoginFilter.java | 60 ++++++++++++------- .../global/config/SecurityConfig.java | 2 +- src/main/resources/application.yml | 2 +- 4 files changed, 51 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/LoginRequest.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/LoginRequest.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/LoginRequest.java new file mode 100644 index 0000000..371430b --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/LoginRequest.java @@ -0,0 +1,12 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.dto; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LoginRequest { + private String email; + private String password; +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java index a15fc84..62e0ad8 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java @@ -1,21 +1,21 @@ package com.WhoIsRoom.WhoIs_Server.domain.auth.filter; +import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.LoginRequest; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.stereotype.Component; @Slf4j -@Component public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter { private final ObjectMapper objectMapper; @@ -25,36 +25,50 @@ public CustomLoginFilter(AuthenticationManager authenticationManager, AuthenticationSuccessHandler successHandler, AuthenticationFailureHandler failureHandler) { this.objectMapper = objectMapper; - - // 기본 설정들 캡슐화 - super.setFilterProcessesUrl("/api/auth/login"); // 로그인 엔드포인트 고정 - super.setUsernameParameter("email"); // username 대신 email - super.setPasswordParameter("password"); - - // AuthenticationManager + 핸들러 세팅 + super.setFilterProcessesUrl("/api/auth/login"); super.setAuthenticationManager(authenticationManager); super.setAuthenticationSuccessHandler(successHandler); super.setAuthenticationFailureHandler(failureHandler); } @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException { - log.info("=== Login Filter 진입 ==="); + log.info("=== Login Filter (JSON only) 진입 ==="); - //클라이언트 요청에서 username, password 추출 - String email = obtainUsername(request); - String password = obtainPassword(request); + // 1) 메서드 강제: POST만 허용 + if (!"POST".equalsIgnoreCase(request.getMethod())) { + throw new AuthenticationServiceException("지원하지 않는 HTTP 메서드입니다."); + } - //스프링 시큐리티에서 username과 password를 검증하기 위해서는 token에 담아야 함 - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, password, null); + // 2) Content-Type 강제: application/json만 허용 + String contentType = request.getContentType(); + if (contentType == null || !contentType.toLowerCase().contains("application/json")) { + throw new AuthenticationServiceException("Content-Type application/json 만 허용됩니다."); + } - //token에 담은 검증을 위한 AuthenticationManager로 전달 - return this.getAuthenticationManager().authenticate(authToken); - } + try { + // 3) JSON 바디 파싱 + LoginRequest login = objectMapper.readValue(request.getInputStream(), LoginRequest.class); - @Override - protected String obtainUsername(HttpServletRequest request) { - return request.getParameter("email"); // username 대신 email + String email = login.getEmail(); + String password = login.getPassword(); + + // 4) 값 검증 (비었으면 실패로 위임) + if (email == null || email.isBlank() || password == null || password.isBlank()) { + throw new BadCredentialsException("이메일/비밀번호가 비어있습니다."); + } + + // 5) AuthenticationManager에게 위임 + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken(email.trim(), password); + + return this.getAuthenticationManager().authenticate(authToken); + + } catch (Exception e) { + // 파싱 실패/기타 예외도 실패 핸들러로 위임 + throw new AuthenticationServiceException("로그인 요청 파싱 실패", e); + } } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java index 6d1eb0d..e5bdb86 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java @@ -46,7 +46,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomLoginFil .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); http - .addFilterBefore(jwtExceptionHandlerFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(jwtExceptionHandlerFilter, JwtAuthenticationFilter.class) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .addFilterAt(customLoginFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 20fd30f..b24d5f6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,7 +19,7 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: - ddl-auto: update + ddl-auto: create properties: hibernate: format_sql: true From d396d422272fcba57559be736e454fbde02bc2da Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 21:59:11 +0900 Subject: [PATCH 32/36] [Feat] #3 PasswordEncoder --- .../WhoIs_Server/WhoIsServerApplication.java | 2 + .../domain/auth/dto/LoginRequest.java | 2 + .../domain/auth/filter/CustomLoginFilter.java | 41 +++++++++-------- .../auth/filter/JwtAuthenticationFilter.java | 1 + ...ustomJsonAuthenticationFailureHandler.java | 45 +++++++++++++++---- .../domain/user/service/UserService.java | 6 ++- .../global/config/SecurityConfig.java | 9 +++- 7 files changed, 76 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/WhoIsServerApplication.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/WhoIsServerApplication.java index c8376f1..d81a2dd 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/WhoIsServerApplication.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/WhoIsServerApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class WhoIsServerApplication { diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/LoginRequest.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/LoginRequest.java index 371430b..961efa4 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/LoginRequest.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/LoginRequest.java @@ -3,8 +3,10 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; @Getter +@Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class LoginRequest { private String email; diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java index 62e0ad8..d871c0a 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java @@ -15,6 +15,8 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import java.io.IOException; + @Slf4j public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter { @@ -37,38 +39,39 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ log.info("=== Login Filter (JSON only) 진입 ==="); - // 1) 메서드 강제: POST만 허용 + // 1) 메서드 강제 if (!"POST".equalsIgnoreCase(request.getMethod())) { throw new AuthenticationServiceException("지원하지 않는 HTTP 메서드입니다."); } - // 2) Content-Type 강제: application/json만 허용 + // 2) Content-Type 강제 String contentType = request.getContentType(); if (contentType == null || !contentType.toLowerCase().contains("application/json")) { throw new AuthenticationServiceException("Content-Type application/json 만 허용됩니다."); } + // 3) JSON 파싱만 try-catch + LoginRequest login; try { - // 3) JSON 바디 파싱 - LoginRequest login = objectMapper.readValue(request.getInputStream(), LoginRequest.class); - - String email = login.getEmail(); - String password = login.getPassword(); + login = objectMapper.readValue(request.getInputStream(), LoginRequest.class); + } catch (IOException e) { + // ✅ 진짜 파싱 실패만 여기로 + throw new AuthenticationServiceException("JSON 파싱 실패", e); + } - // 4) 값 검증 (비었으면 실패로 위임) - if (email == null || email.isBlank() || password == null || password.isBlank()) { - throw new BadCredentialsException("이메일/비밀번호가 비어있습니다."); - } + String email = login.getEmail(); + String password = login.getPassword(); - // 5) AuthenticationManager에게 위임 - UsernamePasswordAuthenticationToken authToken = - new UsernamePasswordAuthenticationToken(email.trim(), password); + if (email == null || email.isBlank() || password == null || password.isBlank()) { + // ✅ 자격증명 문제는 그대로 던져서 FailureHandler가 처리 + throw new BadCredentialsException("이메일/비밀번호가 비어있습니다."); + } - return this.getAuthenticationManager().authenticate(authToken); + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken(email.trim(), password); - } catch (Exception e) { - // 파싱 실패/기타 예외도 실패 핸들러로 위임 - throw new AuthenticationServiceException("로그인 요청 파싱 실패", e); - } + // ✅ 여기서 발생하는 UsernameNotFoundException/BadCredentialsException 등은 + // 래핑하지 말고 그대로 던진다 → FailureHandler에서 올바른 코드로 매핑됨 + return this.getAuthenticationManager().authenticate(authToken); } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java index 25fef88..16be3c0 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java @@ -40,6 +40,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { // 인증을 안해도 되니 토큰이 필요없는 URL들 (에러: 로그인이 필요합니다) public final static List PASS_URIS = Arrays.asList( + "/api/users/signup", "/api/auth/**" ); diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomJsonAuthenticationFailureHandler.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomJsonAuthenticationFailureHandler.java index e7dd814..692cf30 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomJsonAuthenticationFailureHandler.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/exception/CustomJsonAuthenticationFailureHandler.java @@ -4,10 +4,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.authentication.AccountExpiredException; -import org.springframework.security.authentication.CredentialsExpiredException; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.authentication.LockedException; +import org.springframework.security.authentication.*; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -24,13 +21,43 @@ public class CustomJsonAuthenticationFailureHandler implements AuthenticationFai @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException { - log.info("=== AuthenticationFailureHandler 진입 ==="); + log.warn("=== AuthenticationFailureHandler 진입: type={}, msg={} ===", + authenticationException.getClass().getSimpleName(), authenticationException.getMessage()); - ErrorCode code = ErrorCode.INVALID_ID_OR_PASSWORD; + ErrorCode code = mapToErrorCode(authenticationException); + setErrorResponse(response, code); + } + + private ErrorCode mapToErrorCode(AuthenticationException ex) { - if (authenticationException instanceof UsernameNotFoundException) { - code = ErrorCode.USER_NOT_FOUND; + // 1) 아이디 없음 + if (ex instanceof UsernameNotFoundException) { + return ErrorCode.USER_NOT_FOUND; } - setErrorResponse(response, code); + + // 2) 잘못된 자격 증명(값 누락/불일치) + if (ex instanceof BadCredentialsException) { + return ErrorCode.INVALID_ID_OR_PASSWORD; + } + + // 4) 요청 형식/메서드/파싱 문제 (JSON only 강제) + if (ex instanceof AuthenticationServiceException) { + String msg = ex.getMessage() != null ? ex.getMessage() : ""; + + if (msg.contains("HTTP 메서드")) { + return ErrorCode.METHOD_NOT_ALLOWED; + } + if (msg.contains("Content-Type")) { + return ErrorCode.ILLEGAL_ARGUMENT; + } + if (msg.contains("파싱")) { + return ErrorCode.ILLEGAL_ARGUMENT; + } + // 그 외 서비스 예외는 일단 잘못된 요청으로 처리 + return ErrorCode.ILLEGAL_ARGUMENT; + } + + // 5) 그 외 디폴트 + return ErrorCode.ILLEGAL_ARGUMENT; } } \ No newline at end of file diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserService.java index 5ecd7e0..ae0cbe9 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserService.java @@ -1,12 +1,14 @@ package com.WhoIsRoom.WhoIs_Server.domain.user.service; import com.WhoIsRoom.WhoIs_Server.domain.user.dto.request.SignupRequest; +import com.WhoIsRoom.WhoIs_Server.domain.user.model.Role; import com.WhoIsRoom.WhoIs_Server.domain.user.model.User; import com.WhoIsRoom.WhoIs_Server.domain.user.repository.UserRepository; import com.WhoIsRoom.WhoIs_Server.global.common.exception.BusinessException; import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,6 +17,7 @@ @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; @Transactional public void signUp(SignupRequest request) { @@ -28,7 +31,8 @@ public void signUp(SignupRequest request) { User user = User.builder() .email(request.getEmail()) .nickName(request.getNickName()) - .password(request.getPassword()) + .password(passwordEncoder.encode(request.getPassword())) + .role(Role.MEMBER) .build(); userRepository.save(user); } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java index e5bdb86..af0d268 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/SecurityConfig.java @@ -16,6 +16,8 @@ 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.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -46,7 +48,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomLoginFil .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); http - .addFilterBefore(jwtExceptionHandlerFilter, JwtAuthenticationFilter.class) + .addFilterBefore(jwtExceptionHandlerFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .addFilterAt(customLoginFilter, UsernamePasswordAuthenticationFilter.class); @@ -91,4 +93,9 @@ public CustomLoginFilter customLoginFilter() throws Exception { customJsonAuthenticationFailureHandler ); } + + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } } From 1885c32205bbfcffd18b464a4bccd7a0f0b16fab Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 22:50:37 +0900 Subject: [PATCH 33/36] [Feat] #3 LoginResponse --- .../auth/dto/{ => request}/LoginRequest.java | 2 +- .../auth/dto/response/LoginResponse.java | 11 +++++++++++ .../domain/auth/filter/CustomLoginFilter.java | 2 +- .../CustomAuthenticationSuccessHandler.java | 19 +++++++++++++++++++ .../domain/auth/service/JwtService.java | 4 ++-- .../domain/auth/util/JwtUtil.java | 4 ++-- .../auth/util/SecurityErrorResponseUtil.java | 2 +- 7 files changed, 37 insertions(+), 7 deletions(-) rename src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/{ => request}/LoginRequest.java (81%) create mode 100644 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/response/LoginResponse.java diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/LoginRequest.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/request/LoginRequest.java similarity index 81% rename from src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/LoginRequest.java rename to src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/request/LoginRequest.java index 961efa4..65b6a97 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/LoginRequest.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/request/LoginRequest.java @@ -1,4 +1,4 @@ -package com.WhoIsRoom.WhoIs_Server.domain.auth.dto; +package com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request; import lombok.AccessLevel; import lombok.Getter; diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/response/LoginResponse.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/response/LoginResponse.java new file mode 100644 index 0000000..c4a0641 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/response/LoginResponse.java @@ -0,0 +1,11 @@ +package com.WhoIsRoom.WhoIs_Server.domain.auth.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class LoginResponse { + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java index d871c0a..16c750b 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java @@ -1,6 +1,6 @@ package com.WhoIsRoom.WhoIs_Server.domain.auth.filter; -import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.LoginRequest; +import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request.LoginRequest; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java index 218521d..b11e646 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java @@ -1,12 +1,17 @@ package com.WhoIsRoom.WhoIs_Server.domain.auth.handler.success; +import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.response.LoginResponse; import com.WhoIsRoom.WhoIs_Server.domain.auth.util.AuthenticationUtil; import com.WhoIsRoom.WhoIs_Server.domain.auth.util.JwtUtil; import com.WhoIsRoom.WhoIs_Server.domain.auth.service.JwtService; +import com.WhoIsRoom.WhoIs_Server.global.common.response.BaseErrorResponse; +import com.WhoIsRoom.WhoIs_Server.global.common.response.BaseResponse; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; @@ -22,6 +27,7 @@ public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationS private final AuthenticationUtil authenticationUtil; private final JwtUtil jwtUtil; private final JwtService jwtService; + private final ObjectMapper objectMapper; @Override @Transactional @@ -43,5 +49,18 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo log.info("[CustomAuthenticationSuccessHandler], refreshToken={}", refreshToken); jwtService.sendTokens(response, accessToken, refreshToken); + + LoginResponse data = LoginResponse.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + + BaseResponse body = BaseResponse.ok(data); + + response.setStatus(HttpServletResponse.SC_OK); + response.setCharacterEncoding("UTF-8"); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + objectMapper.writeValue(response.getWriter(), body); } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java index 9ab46de..40de347 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java @@ -25,10 +25,10 @@ public class JwtService { @Value("${jwt.refresh.expiration}") private Long REFRESH_TOKEN_EXPIRED_IN; - @Value("$jwt.access.header") + @Value("${jwt.access.header}") private String ACCESS_HEADER; - @Value("$jwt.refresh.header") + @Value("${jwt.refresh.header}") private String REFRESH_HEADER; private static final String LOGOUT_VALUE = "logout"; diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java index 17c0bb7..f1dfb31 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java @@ -27,10 +27,10 @@ public class JwtUtil { @Value("${jwt.refresh.expiration}") private Long REFRESH_TOKEN_EXPIRED_IN; - @Value("$jwt.access.header") + @Value("${jwt.access.header}") private String ACCESS_HEADER; - @Value("$jwt.refresh.header") + @Value("${jwt.refresh.header}") private String REFRESH_HEADER; public final String BEARER_PREFIX = "Bearer "; diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/SecurityErrorResponseUtil.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/SecurityErrorResponseUtil.java index d95fc7d..bcbe248 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/SecurityErrorResponseUtil.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/SecurityErrorResponseUtil.java @@ -10,7 +10,7 @@ import java.io.IOException; public class SecurityErrorResponseUtil { - private static final ObjectMapper objectMapper = new ObjectMapper() + public static final ObjectMapper objectMapper = new ObjectMapper() .registerModule(new JavaTimeModule()) .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); From 0047f72ce0ab914726db7ff2bfe416def9e6b0d4 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Wed, 24 Sep 2025 23:42:22 +0900 Subject: [PATCH 34/36] =?UTF-8?q?[Refact]=20#3=20Token=20payload=20?= =?UTF-8?q?=EC=B5=9C=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../success/CustomAuthenticationSuccessHandler.java | 8 +++----- .../WhoIs_Server/domain/auth/service/JwtService.java | 4 ++-- .../WhoIs_Server/domain/auth/util/JwtUtil.java | 12 ++++-------- .../WhoIs_Server/global/config/RedisConfig.java | 6 +++--- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java index b11e646..b4c5ee3 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java @@ -36,13 +36,11 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo String providerId = authenticationUtil.getProviderId(); String role = authenticationUtil.getRole(); Long memberId = authenticationUtil.getMemberId(); - String userName = authenticationUtil.getUsername(); - String email = authenticationUtil.getEmail(); - log.info("[CustomAuthenticationSuccessHandler] providerId={}, role={}, memberId={}, email={}", providerId, role, memberId, email); + log.info("[CustomAuthenticationSuccessHandler] providerId={}, role={}, memberId={}", providerId, role, memberId); // 토큰 생성 - String accessToken = jwtUtil.createAccessToken(memberId, providerId, role, userName, email); - String refreshToken = jwtUtil.createRefreshToken(memberId, providerId, role, email); + String accessToken = jwtUtil.createAccessToken(memberId, providerId, role); + String refreshToken = jwtUtil.createRefreshToken(memberId, providerId); // refresh token 저장 jwtService.storeRefreshToken(refreshToken); diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java index 40de347..6d6ffa1 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java @@ -82,8 +82,8 @@ private void invalidAccessToken(String accessToken) { private void reissueAndSendTokens(HttpServletResponse response, String refreshToken) { // 새로운 Refresh Token 발급 - String reissuedAccessToken = jwtUtil.createAccessToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken), jwtUtil.getName(refreshToken), jwtUtil.getEmail(refreshToken)); - String reissuedRefreshToken = jwtUtil.createRefreshToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken), jwtUtil.getEmail(refreshToken)); + String reissuedAccessToken = jwtUtil.createAccessToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken)); + String reissuedRefreshToken = jwtUtil.createRefreshToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken)); // 새로운 Refresh Token을 DB나 Redis에 저장 storeRefreshToken(reissuedRefreshToken); diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java index f1dfb31..d8e1cf5 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java @@ -67,32 +67,28 @@ public Boolean isTokenExpired(String token) { return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date()); } - public String createAccessToken(Long userId, String providerId, String role, String name, String email) { + public String createAccessToken(Long userId, String providerId, String role) { return Jwts.builder() .claim("tokenType", "access") .claim("userId", userId) .claim("providerId", providerId) .claim("role", role) - .claim("name", name) - .claim("email", email) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRED_IN)) - .signWith(secretKey) + .signWith(secretKey, Jwts.SIG.HS256) .compact(); } - public String createRefreshToken(Long userId, String providerId, String role, String email) { + public String createRefreshToken(Long userId, String providerId) { return Jwts.builder() .claim("tokenType", "refresh") .claim("userId", userId) .claim("providerId", providerId) - .claim("role", role) - .claim("email", email) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRED_IN)) - .signWith(secretKey) + .signWith(secretKey, Jwts.SIG.HS256) .compact(); } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/RedisConfig.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/RedisConfig.java index d677aad..49061e1 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/RedisConfig.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/config/RedisConfig.java @@ -33,11 +33,11 @@ public RedisConnectionFactory redisConnectionFactory() { // serializer 설정으로 redis-cli를 통해 직접 데이터를 조회할 수 있도록 설정 @Bean - public RedisTemplate redisTemplate() { - RedisTemplate redisTemplate = new RedisTemplate<>(); + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); - redisTemplate.setConnectionFactory(redisConnectionFactory()); return redisTemplate; } From 10a74c0ecbd907c8bd6f434f282e7ea1501d2cff Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Thu, 25 Sep 2025 00:14:08 +0900 Subject: [PATCH 35/36] =?UTF-8?q?[Feat]=20#3=20UserPrincipal=EC=97=90=20us?= =?UTF-8?q?erName=EB=A7=8C=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/filter/CustomLoginFilter.java | 7 ------- .../domain/auth/filter/JwtAuthenticationFilter.java | 4 +--- .../success/CustomAuthenticationSuccessHandler.java | 5 +++-- .../WhoIs_Server/domain/auth/model/UserPrincipal.java | 1 - .../domain/auth/service/CustomUserDetailsService.java | 1 - .../WhoIs_Server/domain/auth/service/JwtService.java | 4 ++-- .../domain/auth/util/AuthenticationUtil.java | 10 +++++----- .../WhoIs_Server/domain/auth/util/JwtUtil.java | 6 ++++-- 8 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java index 16c750b..a876f6f 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/CustomLoginFilter.java @@ -39,23 +39,19 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ log.info("=== Login Filter (JSON only) 진입 ==="); - // 1) 메서드 강제 if (!"POST".equalsIgnoreCase(request.getMethod())) { throw new AuthenticationServiceException("지원하지 않는 HTTP 메서드입니다."); } - // 2) Content-Type 강제 String contentType = request.getContentType(); if (contentType == null || !contentType.toLowerCase().contains("application/json")) { throw new AuthenticationServiceException("Content-Type application/json 만 허용됩니다."); } - // 3) JSON 파싱만 try-catch LoginRequest login; try { login = objectMapper.readValue(request.getInputStream(), LoginRequest.class); } catch (IOException e) { - // ✅ 진짜 파싱 실패만 여기로 throw new AuthenticationServiceException("JSON 파싱 실패", e); } @@ -63,15 +59,12 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ String password = login.getPassword(); if (email == null || email.isBlank() || password == null || password.isBlank()) { - // ✅ 자격증명 문제는 그대로 던져서 FailureHandler가 처리 throw new BadCredentialsException("이메일/비밀번호가 비어있습니다."); } UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email.trim(), password); - // ✅ 여기서 발생하는 UsernameNotFoundException/BadCredentialsException 등은 - // 래핑하지 말고 그대로 던진다 → FailureHandler에서 올바른 코드로 매핑됨 return this.getAuthenticationManager().authenticate(authToken); } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java index 16be3c0..0a884d9 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java @@ -77,14 +77,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse UserPrincipal principal = new UserPrincipal( jwtUtil.getUserId(accessToken), jwtUtil.getName(accessToken), - jwtUtil.getEmail(accessToken), null, // 패스워드는 필요 없음 jwtUtil.getProviderId(accessToken), authorities ); log.info("UserPrincipal.userId: {}", principal.getUserId()); - log.info("UserPrincipal.name: {}", principal.getUsername()); - log.info("UserPrincipal.email: {}", principal.getEmail()); + log.info("UserPrincipal.nickName: {}", principal.getUsername()); log.info("UserPrincipal.providerId: {}", principal.getProviderId()); log.info("UserPrincipal.role: {}", principal.getAuthorities().stream().findFirst().get().toString()); diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java index b4c5ee3..24cd7f8 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/handler/success/CustomAuthenticationSuccessHandler.java @@ -36,11 +36,12 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo String providerId = authenticationUtil.getProviderId(); String role = authenticationUtil.getRole(); Long memberId = authenticationUtil.getMemberId(); + String nickName = authenticationUtil.getUsername(); log.info("[CustomAuthenticationSuccessHandler] providerId={}, role={}, memberId={}", providerId, role, memberId); // 토큰 생성 - String accessToken = jwtUtil.createAccessToken(memberId, providerId, role); - String refreshToken = jwtUtil.createRefreshToken(memberId, providerId); + String accessToken = jwtUtil.createAccessToken(memberId, providerId, role, nickName); + String refreshToken = jwtUtil.createRefreshToken(memberId, providerId, nickName); // refresh token 저장 jwtService.storeRefreshToken(refreshToken); diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/model/UserPrincipal.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/model/UserPrincipal.java index e3f6849..f71bcb3 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/model/UserPrincipal.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/model/UserPrincipal.java @@ -13,7 +13,6 @@ public class UserPrincipal implements UserDetails{ private final Long userId; private final String username; - private final String email; private final String password; private final String providerId; private final Collection authorities; diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java index 7595055..6dd6556 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/CustomUserDetailsService.java @@ -28,7 +28,6 @@ public UserPrincipal loadUserByUsername(String email) throws UsernameNotFoundExc return new UserPrincipal( user.getId(), user.getNickName(), - user.getEmail(), user.getPassword(), "localhost", Collections.singleton(user.getRole().toAuthority()) diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java index 6d6ffa1..81526b1 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java @@ -82,8 +82,8 @@ private void invalidAccessToken(String accessToken) { private void reissueAndSendTokens(HttpServletResponse response, String refreshToken) { // 새로운 Refresh Token 발급 - String reissuedAccessToken = jwtUtil.createAccessToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken)); - String reissuedRefreshToken = jwtUtil.createRefreshToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken)); + String reissuedAccessToken = jwtUtil.createAccessToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken), jwtUtil.getName(refreshToken)); + String reissuedRefreshToken = jwtUtil.createRefreshToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getName(refreshToken)); // 새로운 Refresh Token을 DB나 Redis에 저장 storeRefreshToken(reissuedRefreshToken); diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/AuthenticationUtil.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/AuthenticationUtil.java index 0207000..6a133a2 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/AuthenticationUtil.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/AuthenticationUtil.java @@ -38,9 +38,9 @@ public String getUsername() { return principal.getUsername(); } - public String getEmail(){ - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); - return principal.getEmail(); - } +// public String getEmail(){ +// Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); +// UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); +// return principal.getEmail(); +// } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java index d8e1cf5..f7deb2a 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/util/JwtUtil.java @@ -67,12 +67,13 @@ public Boolean isTokenExpired(String token) { return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date()); } - public String createAccessToken(Long userId, String providerId, String role) { + public String createAccessToken(Long userId, String providerId, String role, String name) { return Jwts.builder() .claim("tokenType", "access") .claim("userId", userId) .claim("providerId", providerId) + .claim("name", name) .claim("role", role) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRED_IN)) @@ -80,12 +81,13 @@ public String createAccessToken(Long userId, String providerId, String role) { .compact(); } - public String createRefreshToken(Long userId, String providerId) { + public String createRefreshToken(Long userId, String providerId, String name) { return Jwts.builder() .claim("tokenType", "refresh") .claim("userId", userId) .claim("providerId", providerId) + .claim("name", name) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRED_IN)) .signWith(secretKey, Jwts.SIG.HS256) From cde1ec93f4bf5190b20fe8233e66f13beb774534 Mon Sep 17 00:00:00 2001 From: Shinjongyun Date: Thu, 25 Sep 2025 00:47:23 +0900 Subject: [PATCH 36/36] =?UTF-8?q?[Feat]=20#3=20=EC=9E=AC=EB=B0=9C=EA=B8=89?= =?UTF-8?q?=EC=8B=9C=20tokenType=20=ED=99=95=EC=9D=B8=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WhoIs_Server/domain/auth/service/JwtService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java index 81526b1..a20d69b 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/service/JwtService.java @@ -52,7 +52,11 @@ public void logout(HttpServletRequest request) { public void reissueTokens(HttpServletRequest request, HttpServletResponse response) { String refreshToken = jwtUtil.extractRefreshToken(request) .orElseThrow(() -> new CustomAuthenticationException(ErrorCode.SECURITY_INVALID_REFRESH_TOKEN)); + jwtUtil.validateToken(refreshToken); + if (!"refresh".equals(jwtUtil.getTokenType(refreshToken))) { + throw new CustomJwtException(ErrorCode.INVALID_TOKEN_TYPE); + } reissueAndSendTokens(response, refreshToken); }