diff --git a/build.gradle b/build.gradle
index 50af1e314..c80b50dbb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,8 +1,13 @@
plugins {
- id 'org.springframework.boot' version '3.3.5'
+ id 'org.springframework.boot' version '3.5.7'
id 'io.spring.dependency-management' version '1.1.6'
id 'java-library'
- id 'com.diffplug.spotless' version '6.25.0'
+ id 'com.diffplug.spotless' version '6.23.3'
+}
+
+ext {
+ set('springCloudVersion', "2025.0.0")
+ set('querydslVersion', "5.1.0")
}
spotless {
@@ -64,11 +69,9 @@ subprojects {
}
}
- testing {
- suites {
- test {
- useJUnitJupiter()
- }
+ dependencyManagement {
+ imports {
+ mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
@@ -78,19 +81,20 @@ subprojects {
'org.springframework.boot:spring-boot-configuration-processor',
'jakarta.persistence:jakarta.persistence-api',
'jakarta.annotation:jakarta.annotation-api',
- 'com.querydsl:querydsl-apt:5.0.0:jakarta'
+ "com.querydsl:querydsl-apt:${querydslVersion}:jakarta"
)
implementation (
'org.springframework.boot:spring-boot-starter-web',
'org.springframework.boot:spring-boot-starter-validation',
- 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0',
+ 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0',
'com.google.code.findbugs:jsr305:3.0.2',
+ 'io.awspring.cloud:spring-cloud-starter-aws:2.4.4',
// cloud config
- 'org.springframework.cloud:spring-cloud-starter-config:4.1.4',
+ 'org.springframework.cloud:spring-cloud-starter-config',
'org.springframework.boot:spring-boot-starter-actuator',
- 'org.springframework.cloud:spring-cloud-starter-bootstrap:4.1.4',
+ 'org.springframework.cloud:spring-cloud-starter-bootstrap',
// mail
'org.springframework.boot:spring-boot-starter-mail',
@@ -111,6 +115,10 @@ subprojects {
)
}
+ test {
+ useJUnitPlatform()
+ }
+
}
project(':module-jpa') {
@@ -120,8 +128,8 @@ project(':module-jpa') {
dependencies {
api (
'org.springframework.boot:spring-boot-starter-data-jpa',
- 'com.querydsl:querydsl-jpa:5.0.0:jakarta',
- 'com.jcraft:jsch:0.1.55',
+ "com.querydsl:querydsl-jpa:${querydslVersion}:jakarta",
+ 'com.jcraft:jsch:0.1.55', // 로컬 개발용 db ssh tunneling, https://mavenlibs.com/maven/dependency/com.jcraft/jsch
// 'org.mariadb.jdbc:mariadb-java-client',
'com.mysql:mysql-connector-j',
'com.h2database:h2'
@@ -136,18 +144,17 @@ project(':module-auth') {
dependencies {
api project(':module-jpa')
// jwt
- api 'io.jsonwebtoken:jjwt-api:0.11.5'
- runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5',
+ api 'io.jsonwebtoken:jjwt-api:0.11.2'
+ runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2',
// Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
//'org.bouncycastle:bcprov-jdk15on:1.60',
- 'io.jsonwebtoken:jjwt-jackson:0.11.5'
+ 'io.jsonwebtoken:jjwt-jackson:0.11.2' // or 'io.jsonwebtoken:jjwt-gson:0.11.2' for gson
// security
api 'org.springframework.boot:spring-boot-starter-security'
api 'org.springframework.boot:spring-boot-starter-oauth2-client'
- api 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.2'
testImplementation 'org.springframework.security:spring-security-test'
- testImplementation 'org.mockito:mockito-inline:5.2.0'
+ testImplementation 'org.mockito:mockito-inline:2.13.0'
}
}
@@ -158,10 +165,8 @@ project(':module-fileStorage') {
dependencies {
api project(':module-jpa')
- api 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.2'
- implementation 'com.amazonaws:aws-java-sdk-s3:1.12.188'
testImplementation 'org.springframework.security:spring-security-test'
- testImplementation 'org.mockito:mockito-inline:5.2.0'
+ testImplementation 'org.mockito:mockito-inline:2.13.0'
}
}
@@ -171,7 +176,6 @@ project(':resource-server') {
api project(':module-auth')
api project(':module-fileStorage')
api 'org.springframework.boot:spring-boot-starter-security'
- implementation 'com.amazonaws:aws-java-sdk-s3:1.12.188'
testImplementation 'org.springframework.security:spring-security-test'
}
@@ -179,7 +183,7 @@ project(':resource-server') {
delete file('src/main/generated')
}
- tasks.register('cleanGeneratedDir', Delete) {
+ task cleanGeneratedDir(type: Delete) {
delete file('src/main/generated')
}
}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 48c0a02ca..5c82cb032 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/module-auth/src/main/java/com/inhabas/api/auth/config/AuthBeansConfig.java b/module-auth/src/main/java/com/inhabas/api/auth/config/AuthBeansConfig.java
index a432e3db6..e5a6d4f39 100644
--- a/module-auth/src/main/java/com/inhabas/api/auth/config/AuthBeansConfig.java
+++ b/module-auth/src/main/java/com/inhabas/api/auth/config/AuthBeansConfig.java
@@ -4,7 +4,6 @@
import lombok.RequiredArgsConstructor;
-import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -36,11 +35,6 @@ public class AuthBeansConfig {
private final AuthProperties authProperties;
private final RefreshTokenRepository refreshTokenRepository;
- @Bean
- public ApplicationRunner jwtSecretKeyStrengthChecker(JwtTokenUtil jwtTokenUtil) {
- return args -> jwtTokenUtil.validateSecretKeyStrength();
- }
-
@Bean
public HttpCookieOAuth2AuthorizationRequestRepository
httpCookieOAuth2AuthorizationRequestRepository() {
diff --git a/module-auth/src/main/java/com/inhabas/api/auth/config/AuthSecurityConfig.java b/module-auth/src/main/java/com/inhabas/api/auth/config/AuthSecurityConfig.java
index 7479742cc..3a060276e 100644
--- a/module-auth/src/main/java/com/inhabas/api/auth/config/AuthSecurityConfig.java
+++ b/module-auth/src/main/java/com/inhabas/api/auth/config/AuthSecurityConfig.java
@@ -8,7 +8,6 @@
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.web.SecurityFilterChain;
@@ -20,8 +19,8 @@
import com.inhabas.api.auth.domain.oauth2.handler.Oauth2AuthenticationSuccessHandler;
@Order(0) // 인증 관련 security filter chain 은 우선순위가 가장 높아야 함.
-@EnableWebSecurity
@Configuration
+@EnableWebSecurity
@RequiredArgsConstructor
@Profile({"dev1", "dev2", "local", "prod1", "prod2"}) // 테스트에는 포함시키지 않음.
public class AuthSecurityConfig {
@@ -33,25 +32,44 @@ public class AuthSecurityConfig {
private final HttpCookieOAuth2AuthorizationRequestRepository
httpCookieOAuth2AuthorizationRequestRepository;
+ /**
+ * 소셜 로그인 api
+ *
+ * 진행과정은 아래와 같다.
+ *
+ *
+ * - 사용자가 소셜로그인 시작. (프론트에서 redirect_url 보내줘야함.)
+ *
- OAuth2 인증 진행 -> 기존 회원인지 검사
+ *
+ * - 성공 -> OAuth2AuthenticationSuccessHandler
+ *
+ * - 프론트에서 보내준 redirect_url 검증 (-> 실패하면 failure handler 에서 처리)
+ *
- jwt 토큰 발급 및 로그인 처리
+ *
- 리다이렉트
+ *
+ * - 실패 -> OAuth2AuthenticationFailureHandler
+ *
+ *
+ *
+ * 회원가입이나, jwt 토큰 발급을 위한 url 로 함부로 접근할 수 없게 하기 위해 jwt 토근이 발급되기 이전까지는 OAuth2 인증 결과를 세션을 통해서 유지함.
+ * 따라서 critical 한 url 에 대해서 OAuth2 인증이 완료된 세션에 한해서만 허용.
+ */
@Bean
- @Order(0)
- public SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exception {
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
- http
- // /login/** 경로에만 이 보안 체인 적용
- .securityMatcher("/login/**")
+ http.securityMatcher("/login/**")
// 세션 생성 금지
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
- .cors(cors -> {})
- .csrf(AbstractHttpConfigurer::disable)
+ .cors(cors -> cors.disable())
.authorizeHttpRequests(
authorize ->
authorize
- .requestMatchers(request -> CorsUtils.isPreFlightRequest(request))
+ .requestMatchers(CorsUtils::isPreFlightRequest)
.permitAll()
.anyRequest()
.permitAll())
+ .csrf(csrf -> csrf.disable())
// Oauth 로그인 설정
.oauth2Login(
oauth2 ->
@@ -63,6 +81,7 @@ public SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exc
.baseUri("/login/oauth2/authorization")
.authorizationRequestRepository(
httpCookieOAuth2AuthorizationRequestRepository))
+ // 사용자 정보를 가져오는 엔드포인트에 대한 설정
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
.failureHandler(oauth2AuthenticationFailureHandler)
.successHandler(oauth2AuthenticationSuccessHandler));
diff --git a/module-auth/src/main/java/com/inhabas/api/auth/domain/oauth2/cookie/CookieUtils.java b/module-auth/src/main/java/com/inhabas/api/auth/domain/oauth2/cookie/CookieUtils.java
index de5eec814..836c9deb7 100644
--- a/module-auth/src/main/java/com/inhabas/api/auth/domain/oauth2/cookie/CookieUtils.java
+++ b/module-auth/src/main/java/com/inhabas/api/auth/domain/oauth2/cookie/CookieUtils.java
@@ -1,45 +1,26 @@
package com.inhabas.api.auth.domain.oauth2.cookie;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.time.Duration;
import java.util.Base64;
+import java.util.Objects;
import java.util.Optional;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
-import org.springframework.http.ResponseCookie;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.util.SerializationUtils;
-public interface CookieUtils {
-
- enum SameSite {
- LAX("Lax"),
- STRICT("Strict"),
- NONE("None");
- private final String value;
-
- SameSite(String v) {
- this.value = v;
- }
+import io.micrometer.core.instrument.util.StringUtils;
- @Override
- public String toString() {
- return value;
- }
- }
+public interface CookieUtils {
/** request 에 담겨 있는 쿠키를 꺼낸다. */
static Optional resolveCookie(HttpServletRequest request, String cookieName) {
Cookie[] cookies = request.getCookies();
- if (cookies != null) {
+ if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(cookieName)) {
return Optional.of(cookie);
@@ -50,80 +31,38 @@ static Optional resolveCookie(HttpServletRequest request, String cookieN
return Optional.empty();
}
- /** 기본 삭제: SameSite=Lax, Secure=request.isSecure(), path="/" 로 설정하여 Max-Age=0 으로 파기. */
+ /** 쿠키를 지우는 작업은 없고, maxAge 를 0으로 설정해서 브라우저가 파기하도록 한다. */
static void deleteCookie(
HttpServletRequest request, HttpServletResponse response, String cookieName) {
- // 동일 이름 쿠키를 빈 값과 Max-Age=0 으로 덮어써서 삭제
- deleteCookie(request, response, cookieName, SameSite.LAX, null);
- }
-
- /**
- * 생성 시와 동일한 속성으로 삭제할 수 있도록 SameSite/Secure 를 제어하는 오버로드. secure=null 이면 request.isSecure() 사용.
- * SameSite=None 이면 Secure=true 강제.
- */
- static void deleteCookie(
- HttpServletRequest request,
- HttpServletResponse response,
- String cookieName,
- SameSite sameSite,
- Boolean secure) {
-
- boolean secureFlag = secure != null ? secure : request.isSecure();
- if (sameSite == SameSite.NONE && !secureFlag) {
- // 브라우저 정책: SameSite=None 은 Secure 필요. 강제 상승.
- secureFlag = true;
+ Cookie[] cookies = request.getCookies();
+ if (cookies != null && cookies.length > 0) {
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals(cookieName)) {
+ cookie.setValue("");
+ cookie.setPath("/");
+ cookie.setMaxAge(0);
+ response.addCookie(cookie);
+ break;
+ }
+ }
}
-
- ResponseCookie rc =
- ResponseCookie.from(cookieName, "")
- .path("/")
- .httpOnly(true)
- .secure(secureFlag)
- .maxAge(Duration.ZERO)
- .sameSite(sameSite.toString())
- .build();
- response.addHeader("Set-Cookie", rc.toString());
- }
-
- /** 기본값: SameSite=Lax, Secure=request.isSecure() */
- static void setCookie(
- HttpServletRequest request,
- HttpServletResponse response,
- String cookieName,
- String cookieContents,
- int maxAge) {
- setCookie(request, response, cookieName, cookieContents, maxAge, SameSite.LAX, null);
}
/**
- * SameSite/Secure 를 제어할 수 있는 오버로드. secure=null 이면 request.isSecure() 사용. SameSite=None 이면
- * Secure=true 강제.
+ * @param response 응답에 쿠키를 적어서 보내줌
+ * @param cookieName key
+ * @param cookieContents value
+ * @param maxAge 초 단위
*/
static void setCookie(
- HttpServletRequest request,
- HttpServletResponse response,
- String cookieName,
- String cookieContents,
- int maxAge,
- SameSite sameSite,
- Boolean secure) {
-
- boolean secureFlag = secure != null ? secure : request.isSecure();
- if (sameSite == SameSite.NONE && !secureFlag) {
- // 브라우저 정책: SameSite=None 은 Secure 필요. 강제 상승.
- secureFlag = true;
- }
+ HttpServletResponse response, String cookieName, String cookieContents, int maxAge) {
- ResponseCookie rc =
- ResponseCookie.from(cookieName, cookieContents)
- .path("/")
- .httpOnly(true)
- .secure(secureFlag)
- .maxAge(Duration.ofSeconds(Math.max(0, maxAge)))
- .sameSite(sameSite.toString())
- .build();
- response.addHeader("Set-Cookie", rc.toString());
+ Cookie cookie = new Cookie(cookieName, cookieContents);
+ cookie.setPath("/");
+ cookie.setHttpOnly(true);
+ cookie.setMaxAge(maxAge);
+ response.addCookie(cookie);
}
/**
@@ -131,14 +70,8 @@ static void setCookie(
* @return 브라우저 쿠키에 담기 위해 OAuth2AuthorizationRequest 를 string 으로 변환.
*/
static String serialize(OAuth2AuthorizationRequest request) {
- try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
- ObjectOutputStream oos = new ObjectOutputStream(bos)) {
- oos.writeObject(request);
- oos.flush();
- return Base64.getUrlEncoder().encodeToString(bos.toByteArray());
- } catch (IOException e) {
- throw new IllegalStateException("Failed to serialize OAuth2AuthorizationRequest", e);
- }
+
+ return Base64.getUrlEncoder().encodeToString(SerializationUtils.serialize(request));
}
/**
@@ -148,19 +81,13 @@ static String serialize(OAuth2AuthorizationRequest request) {
*/
static T deserialize(Cookie cookie, Class clz) {
- if (cookie == null || isBlank(cookie.getValue())) return null;
- try {
- byte[] data = Base64.getUrlDecoder().decode(cookie.getValue());
- try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
- Object obj = ois.readObject();
- return clz.cast(obj);
- }
- } catch (IOException | ClassNotFoundException | IllegalArgumentException ex) {
- return null;
- }
+ if (isDeleted(cookie)) return null;
+ else
+ return clz.cast(
+ SerializationUtils.deserialize(Base64.getUrlDecoder().decode(cookie.getValue())));
}
- private static boolean isBlank(String s) {
- return s == null || s.trim().isEmpty();
+ private static boolean isDeleted(Cookie cookie) {
+ return StringUtils.isBlank(cookie.getValue()) || Objects.isNull(cookie.getValue());
}
}
diff --git a/module-auth/src/main/java/com/inhabas/api/auth/domain/oauth2/cookie/HttpCookieOAuth2AuthorizationRequestRepository.java b/module-auth/src/main/java/com/inhabas/api/auth/domain/oauth2/cookie/HttpCookieOAuth2AuthorizationRequestRepository.java
index 172982f8b..baa981716 100644
--- a/module-auth/src/main/java/com/inhabas/api/auth/domain/oauth2/cookie/HttpCookieOAuth2AuthorizationRequestRepository.java
+++ b/module-auth/src/main/java/com/inhabas/api/auth/domain/oauth2/cookie/HttpCookieOAuth2AuthorizationRequestRepository.java
@@ -35,37 +35,27 @@ public void saveAuthorizationRequest(
HttpServletRequest request,
HttpServletResponse response) {
if (authorizationRequest == null) {
- CookieUtils.deleteCookie(
- request,
- response,
- OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME,
- CookieUtils.SameSite.LAX,
- null);
- CookieUtils.deleteCookie(
- request, response, REDIRECT_URL_PARAM_COOKIE_NAME, CookieUtils.SameSite.LAX, null);
+ CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
+ CookieUtils.deleteCookie(request, response, REDIRECT_URL_PARAM_COOKIE_NAME);
return;
}
CookieUtils.setCookie(
- request,
response,
OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME,
CookieUtils.serialize(authorizationRequest),
- cookieExpireSeconds,
- CookieUtils.SameSite.LAX,
- null);
+ cookieExpireSeconds);
String redirectUrlAfterLogin = request.getParameter(REDIRECT_URL_PARAM_COOKIE_NAME);
if (StringUtils.isNotBlank(redirectUrlAfterLogin)) {
CookieUtils.setCookie(
- request,
- response,
- REDIRECT_URL_PARAM_COOKIE_NAME,
- redirectUrlAfterLogin,
- cookieExpireSeconds,
- CookieUtils.SameSite.LAX,
- null);
+ response, REDIRECT_URL_PARAM_COOKIE_NAME, redirectUrlAfterLogin, cookieExpireSeconds);
}
}
+ @Deprecated
+ public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
+ return null;
+ }
+
/**
* OAuth2AuthorizationRequest 를 쿠키에서 제거함.
*
@@ -77,25 +67,14 @@ public OAuth2AuthorizationRequest removeAuthorizationRequest(
// 쿠키 삭제하기 전에 쿠키 문자열을 객체로 변환
OAuth2AuthorizationRequest authorizationRequest = this.loadAuthorizationRequest(request);
- CookieUtils.deleteCookie(
- request,
- response,
- OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME,
- CookieUtils.SameSite.LAX,
- null);
+ CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
return authorizationRequest;
}
/** redirect_url이 담긴 쿠키는 인증이 완전히 완료된 후에 제거되어야함. */
public void clearCookies(HttpServletRequest request, HttpServletResponse response) {
- CookieUtils.deleteCookie(
- request,
- response,
- OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME,
- CookieUtils.SameSite.LAX,
- null);
- CookieUtils.deleteCookie(
- request, response, REDIRECT_URL_PARAM_COOKIE_NAME, CookieUtils.SameSite.LAX, null);
+ CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
+ CookieUtils.deleteCookie(request, response, REDIRECT_URL_PARAM_COOKIE_NAME);
}
}
diff --git a/module-auth/src/main/java/com/inhabas/api/auth/domain/oauth2/member/security/DefaultRoleHierarchy.java b/module-auth/src/main/java/com/inhabas/api/auth/domain/oauth2/member/security/DefaultRoleHierarchy.java
index a56e96c48..d0956a32c 100644
--- a/module-auth/src/main/java/com/inhabas/api/auth/domain/oauth2/member/security/DefaultRoleHierarchy.java
+++ b/module-auth/src/main/java/com/inhabas/api/auth/domain/oauth2/member/security/DefaultRoleHierarchy.java
@@ -29,6 +29,8 @@ public class DefaultRoleHierarchy implements Hierarchical {
@Override
public RoleHierarchy getHierarchy() {
+ RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
+
Map> roleHierarchyMap =
new HashMap<>() {
{
@@ -60,12 +62,14 @@ public RoleHierarchy getHierarchy() {
put(SECRETARY, Arrays.asList(BASIC, DEACTIVATED, NOT_APPROVED, ANONYMOUS));
put(BASIC, Arrays.asList(DEACTIVATED, NOT_APPROVED, ANONYMOUS));
put(DEACTIVATED, Arrays.asList(NOT_APPROVED, ANONYMOUS));
- put(NOT_APPROVED, Collections.singletonList(ANONYMOUS));
- put(SIGNING_UP, Collections.singletonList(ANONYMOUS));
+ put(NOT_APPROVED, Arrays.asList(ANONYMOUS));
+ put(SIGNING_UP, Arrays.asList(ANONYMOUS));
}
};
String roles = RoleHierarchyUtils.roleHierarchyFromMap(roleHierarchyMap);
- return RoleHierarchyImpl.fromHierarchy(roles);
+ roleHierarchy.setHierarchy(roles);
+
+ return roleHierarchy;
}
}
diff --git a/module-auth/src/main/java/com/inhabas/api/auth/domain/token/jwtUtils/JwtTokenUtil.java b/module-auth/src/main/java/com/inhabas/api/auth/domain/token/jwtUtils/JwtTokenUtil.java
index b3b55bcf4..8fe1980b8 100644
--- a/module-auth/src/main/java/com/inhabas/api/auth/domain/token/jwtUtils/JwtTokenUtil.java
+++ b/module-auth/src/main/java/com/inhabas/api/auth/domain/token/jwtUtils/JwtTokenUtil.java
@@ -3,11 +3,8 @@
import java.security.Key;
import java.util.Date;
import java.util.List;
-import java.util.UUID;
import java.util.stream.Collectors;
-import jakarta.annotation.PostConstruct;
-
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -31,7 +28,6 @@
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
-import io.jsonwebtoken.security.WeakKeyException;
// https://github.com/jwtk/jjwt
@Slf4j
@@ -44,116 +40,35 @@ public class JwtTokenUtil implements TokenUtil {
@Value("${jwt.secretKey}")
private String SECRET_KEY;
- @Value("${jwt.issuer:inhabas.com}")
- private String ISSUER;
-
- @Value("${jwt.audience:inhabas-client}")
- private String AUDIENCE;
-
- private final Long ACCESS_TOKEN_VALID_MILLIS = 30 * 60 * 1000L; // 0.5 hour
- private static final Long REFRESH_TOKEN_VALID_MILLIS = 7 * 24 * 60 * 60 * 1000L; // 7 days
+ private final Long ACCESS_TOKEN_VALID_MILLISECOND = 30 * 60 * 1000L; // 0.5 hour
+ private static final Long REFRESH_TOKEN_VALID_MILLI_SECOND = 7 * 24 * 60 * 60 * 1000L; // 7 days
private static final String AUTHORITY = "authorities";
private static final String MEMBER_ID = "memberId";
private static final String MEMBER_NAME = "memberName";
private static final String MEMBER_PICTURE = "memberPicture";
- private static final int MIN_HS512_KEY_BYTES = 64; // 512 bits
-
- // 캐시된 키와 파서
- private volatile Key signingKey;
- private volatile JwtParser jwtParser;
-
- @PostConstruct
- public void init() {
- validateSecretKeyStrength();
- }
-
- /** HS512 용 시크릿 키 강도 검증 및 Key/Parser 캐싱. */
- public void validateSecretKeyStrength() {
- if (SECRET_KEY == null || SECRET_KEY.trim().isEmpty()) {
- throw new IllegalStateException("jwt.secretKey is not configured or blank");
- }
-
- final byte[] decoded;
- try {
- decoded = Decoders.BASE64.decode(SECRET_KEY.trim());
- } catch (IllegalArgumentException e) {
- throw new IllegalStateException("jwt.secretKey must be Base64-encoded", e);
- }
-
- if (decoded.length < MIN_HS512_KEY_BYTES) {
- throw new IllegalStateException(
- "Weak jwt.secretKey for HS512: require >= 64 bytes after Base64 decoding, got "
- + decoded.length
- + " bytes");
- }
-
- try {
- // 키/파서 캐시 생성 (WeakKeyException 등 조기 검출)
- Key key = Keys.hmacShaKeyFor(decoded);
- this.signingKey = key;
- this.jwtParser =
- Jwts.parserBuilder()
- .setSigningKey(key)
- .requireIssuer(ISSUER)
- .requireAudience(AUDIENCE)
- .build();
- } catch (WeakKeyException e) {
- throw new IllegalStateException("Weak jwt.secretKey: " + e.getMessage(), e);
- }
- }
-
- private Key getSigningKey() {
- Key key = this.signingKey;
- if (key != null) {
- return key;
- }
- // lazy-init (테스트 등 부팅 훅 없이 사용되는 경우 대비)
- Key newKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_KEY));
- this.signingKey = newKey;
- return newKey;
- }
-
- private JwtParser getJwtParser() {
- JwtParser parser = this.jwtParser;
- if (parser != null) {
- return parser;
- }
- // lazy-init
- JwtParser newParser =
- Jwts.parserBuilder()
- .setSigningKey(getSigningKey())
- .requireIssuer(ISSUER)
- .requireAudience(AUDIENCE)
- .build();
- this.jwtParser = newParser;
- return newParser;
- }
@Override
public String createAccessToken(Authentication authentication) {
- return createToken(authentication, ACCESS_TOKEN_VALID_MILLIS);
+ return createToken(authentication, ACCESS_TOKEN_VALID_MILLISECOND);
}
@Override
public String createRefreshToken(Authentication authentication) {
- String token = this.createToken(authentication, REFRESH_TOKEN_VALID_MILLIS);
+ String token = this.createToken(authentication, REFRESH_TOKEN_VALID_MILLI_SECOND);
refreshTokenRepository.save(new RefreshToken(token));
return token;
}
@Override
public Long getExpiration() {
- // 초 단위로 통일
- return this.ACCESS_TOKEN_VALID_MILLIS / 1000;
+ return this.ACCESS_TOKEN_VALID_MILLISECOND / 1000;
}
private String createToken(Authentication authentication, Long expiration) {
- if (authentication == null) {
- throw new IllegalArgumentException("authentication must not be null");
- }
+ assert authentication != null;
OAuth2UserInfo oAuth2UserInfo =
OAuth2UserInfoFactory.getOAuth2UserInfo((OAuth2AuthenticationToken) authentication);
@@ -168,13 +83,10 @@ private String createToken(Authentication authentication, Long expiration) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
- final Key key = getSigningKey();
+ final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_KEY));
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
- .setIssuer(ISSUER)
- .setAudience(AUDIENCE)
- .setId(UUID.randomUUID().toString())
.setSubject(uid)
.claim(MEMBER_ID, customOAuth2User.getMemberId())
.claim(MEMBER_NAME, customOAuth2User.getMemberName())
@@ -190,7 +102,7 @@ private String createToken(Authentication authentication, Long expiration) {
public void validate(String token) {
try {
- getJwtParser().parseClaimsJws(token);
+ Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token);
} catch (SecurityException ex) {
log.error("Invalid JWT signature");
throw new InvalidTokenException();
@@ -218,11 +130,8 @@ public JwtAuthenticationToken getAuthentication(String token) throws JwtExceptio
Claims claims = this.parseClaims(token);
- Number memberIdNumber = claims.get(MEMBER_ID, Number.class);
- Long memberId = memberIdNumber == null ? null : memberIdNumber.longValue();
-
+ Long memberId = claims.get(MEMBER_ID, Long.class);
List> rawAuthorities = claims.get(AUTHORITY, List.class);
- if (rawAuthorities == null) rawAuthorities = List.of();
List grantedAuthorities =
rawAuthorities.stream()
@@ -236,7 +145,7 @@ public JwtAuthenticationToken getAuthentication(String token) throws JwtExceptio
/* 토큰 body 에 넣어둔 사용자 정보를 가져옴
* validation 검사를 먼저 꼭 해야함! */
private Claims parseClaims(String token) throws JwtException {
- return getJwtParser().parseClaimsJws(token).getBody();
+ return Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token).getBody();
}
@Override
@@ -253,37 +162,14 @@ public TokenDto reissueAccessTokenUsing(String refreshToken) throws InvalidToken
private TokenDto createAccessTokenOnly(Claims claims) {
Date now = new Date();
- final Key key = getSigningKey();
-
- // 화이트리스트 기반으로 클레임 재구성: 필요한 필드만 복사
- String subject = claims.getSubject();
- Number memberIdNumber = claims.get(MEMBER_ID, Number.class);
- Long memberId = memberIdNumber == null ? null : memberIdNumber.longValue();
- String memberName = claims.get(MEMBER_NAME, String.class);
- String memberPicture = claims.get(MEMBER_PICTURE, String.class);
- List> rawAuthorities = claims.get(AUTHORITY, List.class);
-
- List authorities =
- rawAuthorities == null
- ? List.of()
- : rawAuthorities.stream()
- .filter(v -> v instanceof String)
- .map(v -> (String) v)
- .collect(Collectors.toList());
+ final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_KEY));
String accessToken =
Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
- .setIssuer(ISSUER)
- .setAudience(AUDIENCE)
- .setId(UUID.randomUUID().toString())
- .setSubject(subject)
- .claim(MEMBER_ID, memberId)
- .claim(MEMBER_NAME, memberName)
- .claim(MEMBER_PICTURE, memberPicture)
- .claim(AUTHORITY, authorities)
+ .setClaims(claims)
.setIssuedAt(now)
- .setExpiration(new Date(now.getTime() + ACCESS_TOKEN_VALID_MILLIS))
+ .setExpiration(new Date(now.getTime() + ACCESS_TOKEN_VALID_MILLISECOND))
.signWith(key, SignatureAlgorithm.HS512)
.compact();
@@ -291,7 +177,7 @@ private TokenDto createAccessTokenOnly(Claims claims) {
.grantType("Bearer")
.accessToken(accessToken)
.refreshToken("")
- .accessTokenExpireDate(ACCESS_TOKEN_VALID_MILLIS / 1000) // 초 단위
+ .accessTokenExpireDate(ACCESS_TOKEN_VALID_MILLISECOND)
.build();
}
}
diff --git a/module-auth/src/main/java/com/inhabas/api/auth/domain/token/securityFilter/JwtAuthenticationFilter.java b/module-auth/src/main/java/com/inhabas/api/auth/domain/token/securityFilter/JwtAuthenticationFilter.java
index e1523de54..9109abef8 100644
--- a/module-auth/src/main/java/com/inhabas/api/auth/domain/token/securityFilter/JwtAuthenticationFilter.java
+++ b/module-auth/src/main/java/com/inhabas/api/auth/domain/token/securityFilter/JwtAuthenticationFilter.java
@@ -23,7 +23,6 @@
import org.springframework.security.web.util.matcher.RequestMatcher;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.inhabas.api.auth.domain.error.ErrorCode;
import com.inhabas.api.auth.domain.error.ErrorResponse;
import com.inhabas.api.auth.domain.error.authException.CustomAuthException;
import com.inhabas.api.auth.domain.token.TokenResolver;
@@ -48,7 +47,7 @@ public JwtAuthenticationFilter(
@Override
public Authentication attemptAuthentication(
HttpServletRequest request, HttpServletResponse response)
- throws AuthenticationException, IOException {
+ throws AuthenticationException, IOException, ServletException {
final String token = tokenResolver.resolveAccessTokenOrNull(request);
@@ -81,27 +80,14 @@ protected void successfulAuthentication(
@Override
protected void unsuccessfulAuthentication(
HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
- throws IOException {
+ throws IOException, ServletException {
SecurityContextHolder.clearContext();
- log.info("Failed to process authentication request: {}", failed.getMessage());
+ log.info("Failed to process authentication request", failed);
+ response.setStatus(((CustomAuthException) failed).getErrorCode().getStatus());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
-
- if (failed instanceof CustomAuthException) {
- response.setStatus(((CustomAuthException) failed).getErrorCode().getStatus());
- try (OutputStream os = response.getOutputStream()) {
- ObjectMapper objectMapper = new ObjectMapper();
- objectMapper.writeValue(
- os, ErrorResponse.of(((CustomAuthException) failed).getErrorCode()));
- os.flush();
- }
- return;
- }
-
- // 일반 인증 실패에 대해서는 401로 응답하고 자세한 정보를 노출하지 않음
- response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
try (OutputStream os = response.getOutputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
- objectMapper.writeValue(os, ErrorResponse.of(ErrorCode.JWT_INVALID));
+ objectMapper.writeValue(os, ErrorResponse.of(((CustomAuthException) failed).getErrorCode()));
os.flush();
}
}
diff --git a/module-auth/src/test/java/com/inhabas/api/auth/domain/oauth2/cookie/CookieUtilsTest.java b/module-auth/src/test/java/com/inhabas/api/auth/domain/oauth2/cookie/CookieUtilsTest.java
index ec504a0a8..974f1694e 100644
--- a/module-auth/src/test/java/com/inhabas/api/auth/domain/oauth2/cookie/CookieUtilsTest.java
+++ b/module-auth/src/test/java/com/inhabas/api/auth/domain/oauth2/cookie/CookieUtilsTest.java
@@ -44,13 +44,12 @@ public void resolveCookieFromRequest() {
@Test
public void saveCookieToResponse() {
// given
- MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
String cookieName = "myCookie";
String cookieContents = "hello";
// when
- CookieUtils.setCookie(request, response, cookieName, cookieContents, 180);
+ CookieUtils.setCookie(response, cookieName, cookieContents, 180);
// then
Cookie resolvedCookie = response.getCookie(cookieName);
@@ -98,7 +97,7 @@ public void serializingTest()
OAuth2AuthorizationRequest.Builder builder =
(OAuth2AuthorizationRequest.Builder)
constructor.newInstance(AuthorizationGrantType.AUTHORIZATION_CODE);
- OAuth2AuthorizationRequest requestObj =
+ OAuth2AuthorizationRequest request =
builder
.authorizationUri("https://kauth.kakao.com/oauth/authorize")
.clientId("1234")
@@ -110,7 +109,7 @@ public void serializingTest()
.build();
// when
- String serializedRequest = CookieUtils.serialize(requestObj);
+ String serializedRequest = CookieUtils.serialize(request);
// then
assertTrue(Base64.isBase64(serializedRequest));
@@ -155,56 +154,4 @@ public void deserializingTest()
// then
assertThat(deserializedRequest).usingRecursiveComparison().isEqualTo(originalRequest);
}
-
- @DisplayName("역직렬화 실패 시 null 반환한다.")
- @Test
- public void deserializeReturnsNullOnInvalidBase64OrPayload() {
- // given
- Cookie invalid = new Cookie("bad", "not_base64!!");
-
- // when
- OAuth2AuthorizationRequest result =
- CookieUtils.deserialize(invalid, OAuth2AuthorizationRequest.class);
-
- // then
- assertThat(result).isNull();
- }
-
- @DisplayName("setCookie 시 SameSite=Lax 헤더를 추가한다.")
- @Test
- public void setCookieAddsSameSiteHeader() {
- // given
- MockHttpServletRequest request = new MockHttpServletRequest();
- MockHttpServletResponse response = new MockHttpServletResponse();
-
- // when
- CookieUtils.setCookie(request, response, "s", "v", 60);
-
- // then
- java.util.List setCookies = response.getHeaders("Set-Cookie");
- assertThat(setCookies).isNotEmpty();
- assertThat(String.join(" ", setCookies)).contains("SameSite=Lax");
- }
-
- @DisplayName("deleteCookie 시 SameSite=Lax, Max-Age=0, HttpOnly를 포함한다.")
- @Test
- public void deleteCookieAddsAttributesProperly() {
- // given
- MockHttpServletRequest request = new MockHttpServletRequest();
- MockHttpServletResponse response = new MockHttpServletResponse();
- Cookie cookie = new Cookie("del", "bye");
- cookie.setMaxAge(120);
- request.setCookies(cookie);
-
- // when
- CookieUtils.deleteCookie(request, response, "del");
-
- // then
- java.util.List setCookies = response.getHeaders("Set-Cookie");
- assertThat(setCookies).isNotEmpty();
- String combined = String.join(" ", setCookies);
- assertThat(combined).contains("SameSite=Lax");
- assertThat(combined).contains("Max-Age=0");
- assertThat(combined).contains("HttpOnly");
- }
}
diff --git a/module-auth/src/test/java/com/inhabas/api/auth/domain/token/JwtTokenUtilTest.java b/module-auth/src/test/java/com/inhabas/api/auth/domain/token/JwtTokenUtilTest.java
index 06869c878..bf5cee8a8 100644
--- a/module-auth/src/test/java/com/inhabas/api/auth/domain/token/JwtTokenUtilTest.java
+++ b/module-auth/src/test/java/com/inhabas/api/auth/domain/token/JwtTokenUtilTest.java
@@ -80,7 +80,7 @@ public void createJwtTokenTest() {
@Test
public void nullAuthenticationTokenTest() {
- assertThrows(IllegalArgumentException.class, () -> jwtTokenUtil.createAccessToken(null));
+ assertThrows(AssertionError.class, () -> jwtTokenUtil.createAccessToken(null));
}
@DisplayName("토큰을 정상적으로 decode")
diff --git a/resource-server/src/main/java/com/inhabas/api/config/WebSecurityConfig.java b/resource-server/src/main/java/com/inhabas/api/config/WebSecurityConfig.java
index f5b9d36f2..6c15863da 100644
--- a/resource-server/src/main/java/com/inhabas/api/config/WebSecurityConfig.java
+++ b/resource-server/src/main/java/com/inhabas/api/config/WebSecurityConfig.java
@@ -3,36 +3,29 @@
import static com.inhabas.api.auth.domain.oauth2.member.domain.valueObject.Role.*;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
-import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
-import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
-import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
-import org.springframework.web.cors.CorsConfiguration;
-import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.CorsUtils;
-import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import com.inhabas.api.auth.config.AuthBeansConfig;
import com.inhabas.api.auth.domain.oauth2.member.security.Hierarchical;
@@ -50,10 +43,10 @@
public class WebSecurityConfig {
private static final String[] AUTH_WHITELIST_SWAGGER = {
- "/swagger-ui/**", "/swagger/**", "/docs/**", "/v3/api-docs/**", "/swagger-resources/**"
+ "/swagger-ui/**", "/swagger/**", "/docs/**"
};
private static final String[] AUTH_WHITELIST_STATIC = {
- "/static/css/**", "/static/js/**", "/**/*.ico", "/favicon.ico"
+ "/static/css/**", "/static/js/**", "*.ico"
};
private static final String[] AUTH_WHITELIST_TOKEN = {"/token/**"};
private static final String[] AUTH_WHITELIST_PATH = {
@@ -68,10 +61,13 @@ public class WebSecurityConfig {
"/club/activity/**", "/club/activities"
};
private static final String[] AUTH_WHITELIST_NORMAL_BOARD = {"/board/count"};
+
private static final String[] AUTH_WHITELIST_PROJECT_BOARD = {"/project/count"};
+
private static final String[] AUTH_WHITELIST_CONTEST_BOARD = {"/contest/count"};
@Configuration
+ @Order(1)
@EnableMethodSecurity(jsr250Enabled = true)
@EnableWebSecurity
@RequiredArgsConstructor
@@ -82,173 +78,135 @@ public static class ApiSecurityForDev {
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final Hierarchical hierarchy;
private final JwtTokenUtil jwtTokenUtil;
- private final AuthBeansConfig authBeansConfig;
private final JwtAuthenticationProvider jwtAuthenticationProvider;
-
- @Value("${cors.allowed-origins:*}")
- private String allowedOriginsProp;
+ private final AuthBeansConfig authBeansConfig;
@Bean
- public AuthenticationManager authenticationManager() {
- return new ProviderManager(List.of(jwtAuthenticationProvider));
+ public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
+ AuthenticationManagerBuilder builder =
+ http.getSharedObject(AuthenticationManagerBuilder.class);
+ builder.authenticationProvider(jwtAuthenticationProvider);
+ return builder.build();
}
@Bean
- public CorsConfigurationSource corsConfigurationSource() {
- CorsConfiguration config = new CorsConfiguration();
-
- List origins =
- Arrays.stream(allowedOriginsProp.split(","))
- .map(String::trim)
- .filter(s -> !s.isEmpty())
- .collect(Collectors.toList());
-
- boolean wildcard = origins.contains("*");
- if (wildcard) {
- // 패턴 전체 허용 시, 자격 증명은 허용하지 않음 (보안 권장)
- config.setAllowedOriginPatterns(List.of("*"));
- config.setAllowCredentials(false);
- } else {
- config.setAllowedOrigins(origins);
- config.setAllowCredentials(true);
+ public JwtAuthenticationFilter jwtAuthenticationFilter(
+ AuthenticationManager authenticationManager) {
+ final List skipPaths = new ArrayList<>();
+
+ for (String path : AUTH_WHITELIST_CLUB_ACTIVITY) {
+ skipPaths.add(new CustomRequestMatcher(path, "GET"));
}
- config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
- config.setAllowedHeaders(List.of("*"));
- config.setExposedHeaders(List.of("Authorization", "Content-Disposition"));
- config.setMaxAge(3600L);
+ for (String path : AUTH_WHITELIST_TOKEN) {
+ skipPaths.add(new CustomRequestMatcher(path, "POST"));
+ }
- UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
- source.registerCorsConfiguration("/**", config);
- return source;
- }
+ final RequestMatcher requestMatcher = new AndRequestMatcher(skipPaths);
+ final JwtAuthenticationFilter filter =
+ new JwtAuthenticationFilter(
+ requestMatcher, jwtTokenUtil, authBeansConfig.tokenResolver());
- @Bean
- public WebSecurityCustomizer webSecurityCustomizer() {
- return (web) -> {};
+ filter.setAuthenticationManager(authenticationManager);
+ return filter;
}
@Bean
@Order(1)
- public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
- http.anonymous(
- anonymous -> anonymous.principal("anonymousUser").authorities("ROLE_" + ANONYMOUS))
- .httpBasic(httpBasic -> httpBasic.disable())
+ public SecurityFilterChain apiSecurityFilterChain(
+ HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
+ http.securityMatcher("/**")
+ .anonymous(anon -> anon.authorities("ROLE_" + ANONYMOUS))
+ .httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(
- sessionManagement ->
- sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
- .cors(cors -> cors.configurationSource(corsConfigurationSource()))
+ session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .cors(Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable)
- .headers(
- headers ->
- headers
- .referrerPolicy(
- r -> r.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER))
- .frameOptions(frame -> frame.sameOrigin()))
.exceptionHandling(
- exceptionHandling ->
- exceptionHandling
- .authenticationEntryPoint(jwtAuthenticationEntryPoint)
+ ex ->
+ ex.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler))
.authorizeHttpRequests(
authorize ->
authorize
.requestMatchers(CorsUtils::isPreFlightRequest)
.permitAll()
- // Swagger 및 공개 경로는 필터 체인은 타되 인가만 면제
- .requestMatchers(pathMatchers(AUTH_WHITELIST_SWAGGER))
+ .requestMatchers(HttpMethod.GET, AUTH_WHITELIST_POLICY)
.permitAll()
- .requestMatchers(pathMatchers(AUTH_WHITELIST_STATIC))
+ .requestMatchers(HttpMethod.GET, AUTH_WHITELIST_SIGNUP)
.permitAll()
- .requestMatchers(pathMatchers(AUTH_WHITELIST_PATH))
+ .requestMatchers(HttpMethod.GET, AUTH_WHITELIST_CLUB)
.permitAll()
- // GET 공개 엔드포인트들
- .requestMatchers(methodMatchers(HttpMethod.GET, AUTH_WHITELIST_POLICY))
+ .requestMatchers(HttpMethod.GET, AUTH_WHITELIST_NORMAL_BOARD)
.permitAll()
- .requestMatchers(methodMatchers(HttpMethod.GET, AUTH_WHITELIST_SIGNUP))
+ .requestMatchers(HttpMethod.GET, AUTH_WHITELIST_PROJECT_BOARD)
.permitAll()
- .requestMatchers(methodMatchers(HttpMethod.GET, AUTH_WHITELIST_CLUB))
+ .requestMatchers(HttpMethod.GET, AUTH_WHITELIST_CONTEST_BOARD)
.permitAll()
- .requestMatchers(methodMatchers(HttpMethod.GET, AUTH_WHITELIST_NORMAL_BOARD))
+ .requestMatchers(AUTH_WHITELIST_SWAGGER)
.permitAll()
- .requestMatchers(methodMatchers(HttpMethod.GET, AUTH_WHITELIST_PROJECT_BOARD))
+ .requestMatchers(AUTH_WHITELIST_STATIC)
.permitAll()
- .requestMatchers(methodMatchers(HttpMethod.GET, AUTH_WHITELIST_CONTEST_BOARD))
+ .requestMatchers(AUTH_WHITELIST_PATH)
.permitAll()
- // 나머지 기존 인가 규칙 유지
- .requestMatchers(pathMatchers("/myInfo/requests"))
+ .requestMatchers("/myInfo/requests")
.hasAnyRole(CHIEF.toString(), VICE_CHIEF.toString())
- .requestMatchers(pathMatchers("/myInfo/request/**"))
+ .requestMatchers("/myInfo/request/**")
.hasAnyRole(CHIEF.toString(), VICE_CHIEF.toString())
- .requestMatchers(pathMatchers("/members/executive"))
+ // 회원 관리
+ .requestMatchers("/members/executive")
.hasRole(ANONYMOUS.toString())
- .requestMatchers(pathMatchers("/members/hof"))
+ .requestMatchers("/members/hof")
.hasRole(DEACTIVATED.toString())
- .requestMatchers(pathMatchers("/members/approved/role"))
+ .requestMatchers("/members/approved/role")
.hasRole(SECRETARY.toString())
- .requestMatchers(pathMatchers("/members/approved/type"))
+ .requestMatchers("/members/approved/type")
.hasAnyRole(CHIEF.toString(), VICE_CHIEF.toString())
- .requestMatchers(pathMatchers("/members/**", "/member/**"))
+ .requestMatchers("/members/**", "/member/**")
.hasAnyRole(SECRETARY.toString(), EXECUTIVES.toString())
+ // 회계내역
.requestMatchers(
- pathMatchers(
- "/budget/history/**",
- "/budget/histories",
- "/budget/application/**",
- "/budget/applications"))
+ "/budget/history/**",
+ "/budget/histories",
+ "/budget/application/**",
+ "/budget/applications")
.hasRole(DEACTIVATED.toString())
- .requestMatchers(pathMatchers("/lecture/**/status"))
+ // 강의
+ .requestMatchers(
+ "/lecture/*/status",
+ "/lecture/*/students/status",
+ "/lecture/*/student/*/status")
.hasRole(EXECUTIVES.toString())
- .requestMatchers(pathMatchers("/lecture/**"))
+ .requestMatchers("/lecture/**")
.hasRole(DEACTIVATED.toString())
- .requestMatchers(methodMatchers(HttpMethod.PUT, "/signUp/schedule"))
+ // 회원가입 일정 수정
+ .requestMatchers(HttpMethod.PUT, "/signUp/schedule")
.hasAnyRole(CHIEF.toString(), VICE_CHIEF.toString())
- .requestMatchers(pathMatchers("/signUp/check"))
- .hasAnyRole(ANONYMOUS.toString(), SIGNING_UP.toString())
- .requestMatchers(pathMatchers("/signUp/**"))
+ // 회원가입은 ANONYMOUS 권한은 명시적으로 부여받은 상태에서만 가능
+ .requestMatchers("/signUp/check")
+ .hasRole(ANONYMOUS.toString())
+ .requestMatchers("/signUp/**")
.hasRole(SIGNING_UP.toString())
- .requestMatchers(pathMatchers("/club/history/**"))
+ // 동아리 연혁 수정
+ .requestMatchers("/club/history/**")
.hasRole(EXECUTIVES.toString())
- .requestMatchers(methodMatchers(HttpMethod.PUT, "/policy/**"))
+ // 정책 수정
+ .requestMatchers(HttpMethod.PUT, "/policy/**")
.hasAnyRole(CHIEF.toString(), VICE_CHIEF.toString())
- .requestMatchers(pathMatchers("/scholarship/history/**"))
+ // 장학회 연혁 수정
+ .requestMatchers("/scholarship/history/**")
.hasRole(SECRETARY.toString())
+ // 그 외
.anyRequest()
.hasRole(ANONYMOUS.toString()));
- http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
-
+ http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
- private static RequestMatcher[] pathMatchers(String... patterns) {
- return Arrays.stream(patterns).map(AntPathRequestMatcher::new).toArray(RequestMatcher[]::new);
- }
-
- private static RequestMatcher[] methodMatchers(HttpMethod method, String... patterns) {
- return Arrays.stream(patterns)
- .map(p -> new AntPathRequestMatcher(p, method.name()))
- .toArray(RequestMatcher[]::new);
- }
-
@Bean
- public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
- final List skipPaths = new ArrayList<>();
-
- for (String path : AUTH_WHITELIST_CLUB_ACTIVITY) {
- skipPaths.add(new CustomRequestMatcher(path, "GET"));
- }
-
- for (String path : AUTH_WHITELIST_TOKEN) {
- skipPaths.add(new CustomRequestMatcher(path, "POST"));
- }
-
- final RequestMatcher requestMatcher = new AndRequestMatcher(skipPaths);
- final JwtAuthenticationFilter filter =
- new JwtAuthenticationFilter(
- requestMatcher, jwtTokenUtil, authBeansConfig.tokenResolver());
- filter.setAuthenticationManager(authenticationManager());
- return filter;
+ public RoleHierarchy roleHierarchy() {
+ return hierarchy.getHierarchy();
}
}
}
diff --git a/resource-server/src/main/java/com/inhabas/api/domain/menu/repository/MenuRepositoryImpl.java b/resource-server/src/main/java/com/inhabas/api/domain/menu/repository/MenuRepositoryImpl.java
index 44cfdb9e7..9e538b0c9 100644
--- a/resource-server/src/main/java/com/inhabas/api/domain/menu/repository/MenuRepositoryImpl.java
+++ b/resource-server/src/main/java/com/inhabas/api/domain/menu/repository/MenuRepositoryImpl.java
@@ -1,6 +1,7 @@
package com.inhabas.api.domain.menu.repository;
import static com.inhabas.api.domain.menu.domain.QMenu.menu;
+import static com.inhabas.api.domain.menu.domain.QMenuGroup.menuGroup;
import java.util.ArrayList;
import java.util.LinkedHashMap;
@@ -12,6 +13,7 @@
import lombok.RequiredArgsConstructor;
import com.inhabas.api.domain.menu.domain.Menu;
+import com.inhabas.api.domain.menu.domain.MenuGroup;
import com.inhabas.api.domain.menu.domain.valueObject.MenuId;
import com.inhabas.api.domain.menu.dto.MenuDto;
import com.inhabas.api.domain.menu.dto.MenuGroupDto;
@@ -23,35 +25,29 @@ public class MenuRepositoryImpl implements MenuRepositoryCustom {
private final JPAQueryFactory jpaQueryFactory;
public List findAllMenuByMenuGroup() {
- // Hibernate 6 호환: transform/ScrollableResults 없이 한 번에 조회 후 Java에서 그룹핑
List