Skip to content

Commit c228325

Browse files
committed
[refactor/#350] WebSecurityConfigurerAdapter → SecurityFilterChain 마이그레이션 및 보안 설정 강화
1 parent 5d27a14 commit c228325

9 files changed

Lines changed: 539 additions & 228 deletions

File tree

module-auth/src/main/java/com/inhabas/api/auth/config/AuthBeansConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import lombok.RequiredArgsConstructor;
66

7+
import org.springframework.boot.ApplicationRunner;
78
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
89
import org.springframework.context.annotation.Bean;
910
import org.springframework.context.annotation.Configuration;
@@ -35,6 +36,11 @@ public class AuthBeansConfig {
3536
private final AuthProperties authProperties;
3637
private final RefreshTokenRepository refreshTokenRepository;
3738

39+
@Bean
40+
public ApplicationRunner jwtSecretKeyStrengthChecker(JwtTokenUtil jwtTokenUtil) {
41+
return args -> jwtTokenUtil.validateSecretKeyStrength();
42+
}
43+
3844
@Bean
3945
public HttpCookieOAuth2AuthorizationRequestRepository
4046
httpCookieOAuth2AuthorizationRequestRepository() {

module-auth/src/main/java/com/inhabas/api/auth/config/AuthSecurityConfig.java

Lines changed: 38 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
import lombok.RequiredArgsConstructor;
44

5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
57
import org.springframework.context.annotation.Profile;
68
import org.springframework.core.annotation.Order;
79
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
810
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
9-
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
11+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
1012
import org.springframework.security.config.http.SessionCreationPolicy;
1113
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
14+
import org.springframework.security.web.SecurityFilterChain;
15+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
1216
import org.springframework.web.cors.CorsUtils;
1317

1418
import com.inhabas.api.auth.domain.oauth2.CustomOAuth2UserService;
@@ -18,9 +22,10 @@
1822

1923
@Order(0) // 인증 관련 security filter chain 은 우선순위가 가장 높아야 함.
2024
@EnableWebSecurity
25+
@Configuration
2126
@RequiredArgsConstructor
2227
@Profile({"dev1", "dev2", "local", "prod1", "prod2"}) // 테스트에는 포함시키지 않음.
23-
public class AuthSecurityConfig extends WebSecurityConfigurerAdapter {
28+
public class AuthSecurityConfig {
2429

2530
private final CustomOAuth2UserService customOAuth2UserService;
2631
private final OAuth2AuthorizedClientService authorizedClientService;
@@ -29,62 +34,40 @@ public class AuthSecurityConfig extends WebSecurityConfigurerAdapter {
2934
private final HttpCookieOAuth2AuthorizationRequestRepository
3035
httpCookieOAuth2AuthorizationRequestRepository;
3136

32-
/**
33-
* 소셜 로그인 api <br>
34-
* <br>
35-
* 진행과정은 아래와 같다.<br>
36-
*
37-
* <ol>
38-
* <li>사용자가 소셜로그인 시작. (프론트에서 redirect_url 보내줘야함.)
39-
* <li>OAuth2 인증 진행 -> 기존 회원인지 검사
40-
* <ol style="list-style-type:lower-alpha">
41-
* <li>성공 -> OAuth2AuthenticationSuccessHandler
42-
* <ol>
43-
* <li>프론트에서 보내준 redirect_url 검증 (-> 실패하면 failure handler 에서 처리)
44-
* <li>jwt 토큰 발급 및 로그인 처리
45-
* <li>리다이렉트
46-
* </ol>
47-
* <li>실패 -> OAuth2AuthenticationFailureHandler
48-
* </ol>
49-
* </ol>
50-
*
51-
* 회원가입이나, jwt 토큰 발급을 위한 url 로 함부로 접근할 수 없게 하기 위해 jwt 토근이 발급되기 이전까지는 OAuth2 인증 결과를 세션을 통해서 유지함.
52-
* 따라서 critical 한 url 에 대해서 OAuth2 인증이 완료된 세션에 한해서만 허용.
53-
*/
54-
@Override
55-
protected void configure(HttpSecurity http) throws Exception {
37+
@Bean
38+
@Order(0)
39+
public SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exception {
5640

57-
http.requestMatchers()
58-
.antMatchers("/login/**")
59-
.and()
41+
http
42+
// /login/** 경로에만 이 보안 체인 적용
43+
.requestMatcher(new AntPathRequestMatcher("/login/**"))
6044
// 세션 생성 금지
61-
.sessionManagement()
62-
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
63-
.and()
64-
.cors()
65-
.and()
66-
.authorizeRequests()
67-
.requestMatchers(CorsUtils::isPreFlightRequest)
68-
.permitAll()
69-
.anyRequest()
70-
.permitAll()
71-
.and()
72-
.csrf()
73-
.disable()
74-
45+
.sessionManagement(
46+
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
47+
.cors(cors -> {})
48+
.csrf(AbstractHttpConfigurer::disable)
49+
.authorizeHttpRequests(
50+
authorize ->
51+
authorize
52+
.requestMatchers(request -> CorsUtils.isPreFlightRequest(request))
53+
.permitAll()
54+
.anyRequest()
55+
.permitAll())
7556
// Oauth 로그인 설정
76-
.oauth2Login()
77-
.authorizedClientService(authorizedClientService)
78-
.authorizationEndpoint()
79-
.baseUri("/login/oauth2/authorization")
80-
.authorizationRequestRepository(httpCookieOAuth2AuthorizationRequestRepository)
81-
.and()
57+
.oauth2Login(
58+
oauth2 ->
59+
oauth2
60+
.authorizedClientService(authorizedClientService)
61+
.authorizationEndpoint(
62+
authorization ->
63+
authorization
64+
.baseUri("/login/oauth2/authorization")
65+
.authorizationRequestRepository(
66+
httpCookieOAuth2AuthorizationRequestRepository))
67+
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
68+
.failureHandler(oauth2AuthenticationFailureHandler)
69+
.successHandler(oauth2AuthenticationSuccessHandler));
8270

83-
// 사용자 정보를 가져오는 엔드포인트에 대한 설정
84-
.userInfoEndpoint()
85-
.userService(customOAuth2UserService)
86-
.and()
87-
.failureHandler(oauth2AuthenticationFailureHandler)
88-
.successHandler(oauth2AuthenticationSuccessHandler);
71+
return http.build();
8972
}
9073
}

module-auth/src/main/java/com/inhabas/api/auth/domain/oauth2/cookie/CookieUtils.java

Lines changed: 91 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.inhabas.api.auth.domain.oauth2.cookie;
22

3+
import java.time.Duration;
34
import java.util.Base64;
45
import java.util.Objects;
56
import java.util.Optional;
@@ -8,19 +9,36 @@
89
import javax.servlet.http.HttpServletRequest;
910
import javax.servlet.http.HttpServletResponse;
1011

12+
import org.springframework.http.ResponseCookie;
1113
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
1214
import org.springframework.util.SerializationUtils;
1315

1416
import io.micrometer.core.instrument.util.StringUtils;
1517

1618
public interface CookieUtils {
1719

20+
enum SameSite {
21+
LAX("Lax"),
22+
STRICT("Strict"),
23+
NONE("None");
24+
private final String value;
25+
26+
SameSite(String v) {
27+
this.value = v;
28+
}
29+
30+
@Override
31+
public String toString() {
32+
return value;
33+
}
34+
}
35+
1836
/** request 에 담겨 있는 쿠키를 꺼낸다. */
1937
static Optional<Cookie> resolveCookie(HttpServletRequest request, String cookieName) {
2038

2139
Cookie[] cookies = request.getCookies();
2240

23-
if (cookies != null && cookies.length > 0) {
41+
if (cookies != null) {
2442
for (Cookie cookie : cookies) {
2543
if (cookie.getName().equals(cookieName)) {
2644
return Optional.of(cookie);
@@ -31,38 +49,80 @@ static Optional<Cookie> resolveCookie(HttpServletRequest request, String cookieN
3149
return Optional.empty();
3250
}
3351

34-
/** 쿠키를 지우는 작업은 없고, maxAge 를 0으로 설정해서 브라우저가 파기하도록 한다. */
52+
/** 기본 삭제: SameSite=Lax, Secure=request.isSecure(), path="/" 로 설정하여 Max-Age=0 으로 파기. */
3553
static void deleteCookie(
3654
HttpServletRequest request, HttpServletResponse response, String cookieName) {
3755

38-
Cookie[] cookies = request.getCookies();
39-
if (cookies != null && cookies.length > 0) {
40-
for (Cookie cookie : cookies) {
41-
if (cookie.getName().equals(cookieName)) {
42-
cookie.setValue("");
43-
cookie.setPath("/");
44-
cookie.setMaxAge(0);
45-
response.addCookie(cookie);
46-
break;
47-
}
48-
}
56+
// 동일 이름 쿠키를 빈 값과 Max-Age=0 으로 덮어써서 삭제
57+
deleteCookie(request, response, cookieName, SameSite.LAX, null);
58+
}
59+
60+
/**
61+
* 생성 시와 동일한 속성으로 삭제할 수 있도록 SameSite/Secure 를 제어하는 오버로드. secure=null 이면 request.isSecure() 사용.
62+
* SameSite=None 이면 Secure=true 강제.
63+
*/
64+
static void deleteCookie(
65+
HttpServletRequest request,
66+
HttpServletResponse response,
67+
String cookieName,
68+
SameSite sameSite,
69+
Boolean secure) {
70+
71+
boolean secureFlag = secure != null ? secure : request.isSecure();
72+
if (sameSite == SameSite.NONE && !secureFlag) {
73+
// 브라우저 정책: SameSite=None 은 Secure 필요. 강제 상승.
74+
secureFlag = true;
4975
}
76+
77+
ResponseCookie rc =
78+
ResponseCookie.from(cookieName, "")
79+
.path("/")
80+
.httpOnly(true)
81+
.secure(secureFlag)
82+
.maxAge(Duration.ZERO)
83+
.sameSite(sameSite.toString())
84+
.build();
85+
response.addHeader("Set-Cookie", rc.toString());
86+
}
87+
88+
/** 기본값: SameSite=Lax, Secure=request.isSecure() */
89+
static void setCookie(
90+
HttpServletRequest request,
91+
HttpServletResponse response,
92+
String cookieName,
93+
String cookieContents,
94+
int maxAge) {
95+
setCookie(request, response, cookieName, cookieContents, maxAge, SameSite.LAX, null);
5096
}
5197

5298
/**
53-
* @param response 응답에 쿠키를 적어서 보내줌
54-
* @param cookieName key
55-
* @param cookieContents value
56-
* @param maxAge 초 단위
99+
* SameSite/Secure 를 제어할 수 있는 오버로드. secure=null 이면 request.isSecure() 사용. SameSite=None 이면
100+
* Secure=true 강제.
57101
*/
58102
static void setCookie(
59-
HttpServletResponse response, String cookieName, String cookieContents, int maxAge) {
103+
HttpServletRequest request,
104+
HttpServletResponse response,
105+
String cookieName,
106+
String cookieContents,
107+
int maxAge,
108+
SameSite sameSite,
109+
Boolean secure) {
110+
111+
boolean secureFlag = secure != null ? secure : request.isSecure();
112+
if (sameSite == SameSite.NONE && !secureFlag) {
113+
// 브라우저 정책: SameSite=None 은 Secure 필요. 강제 상승.
114+
secureFlag = true;
115+
}
60116

61-
Cookie cookie = new Cookie(cookieName, cookieContents);
62-
cookie.setPath("/");
63-
cookie.setHttpOnly(true);
64-
cookie.setMaxAge(maxAge);
65-
response.addCookie(cookie);
117+
ResponseCookie rc =
118+
ResponseCookie.from(cookieName, cookieContents)
119+
.path("/")
120+
.httpOnly(true)
121+
.secure(secureFlag)
122+
.maxAge(Duration.ofSeconds(Math.max(0, maxAge)))
123+
.sameSite(sameSite.toString())
124+
.build();
125+
response.addHeader("Set-Cookie", rc.toString());
66126
}
67127

68128
/**
@@ -82,9 +142,14 @@ static String serialize(OAuth2AuthorizationRequest request) {
82142
static <T> T deserialize(Cookie cookie, Class<T> clz) {
83143

84144
if (isDeleted(cookie)) return null;
85-
else
86-
return clz.cast(
87-
SerializationUtils.deserialize(Base64.getUrlDecoder().decode(cookie.getValue())));
145+
else {
146+
try {
147+
return clz.cast(
148+
SerializationUtils.deserialize(Base64.getUrlDecoder().decode(cookie.getValue())));
149+
} catch (RuntimeException ex) { // Base64 decoding error or deserialization error
150+
return null;
151+
}
152+
}
88153
}
89154

90155
private static boolean isDeleted(Cookie cookie) {

module-auth/src/main/java/com/inhabas/api/auth/domain/oauth2/cookie/HttpCookieOAuth2AuthorizationRequestRepository.java

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,34 @@ public void saveAuthorizationRequest(
3535
HttpServletRequest request,
3636
HttpServletResponse response) {
3737
if (authorizationRequest == null) {
38-
CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
39-
CookieUtils.deleteCookie(request, response, REDIRECT_URL_PARAM_COOKIE_NAME);
38+
CookieUtils.deleteCookie(
39+
request,
40+
response,
41+
OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME,
42+
CookieUtils.SameSite.LAX,
43+
null);
44+
CookieUtils.deleteCookie(
45+
request, response, REDIRECT_URL_PARAM_COOKIE_NAME, CookieUtils.SameSite.LAX, null);
4046
return;
4147
}
4248
CookieUtils.setCookie(
49+
request,
4350
response,
4451
OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME,
4552
CookieUtils.serialize(authorizationRequest),
46-
cookieExpireSeconds);
53+
cookieExpireSeconds,
54+
CookieUtils.SameSite.LAX,
55+
null);
4756
String redirectUrlAfterLogin = request.getParameter(REDIRECT_URL_PARAM_COOKIE_NAME);
4857
if (StringUtils.isNotBlank(redirectUrlAfterLogin)) {
4958
CookieUtils.setCookie(
50-
response, REDIRECT_URL_PARAM_COOKIE_NAME, redirectUrlAfterLogin, cookieExpireSeconds);
59+
request,
60+
response,
61+
REDIRECT_URL_PARAM_COOKIE_NAME,
62+
redirectUrlAfterLogin,
63+
cookieExpireSeconds,
64+
CookieUtils.SameSite.LAX,
65+
null);
5166
}
5267
}
5368

@@ -68,14 +83,25 @@ public OAuth2AuthorizationRequest removeAuthorizationRequest(
6883

6984
// 쿠키 삭제하기 전에 쿠키 문자열을 객체로 변환
7085
OAuth2AuthorizationRequest authorizationRequest = this.loadAuthorizationRequest(request);
71-
CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
86+
CookieUtils.deleteCookie(
87+
request,
88+
response,
89+
OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME,
90+
CookieUtils.SameSite.LAX,
91+
null);
7292

7393
return authorizationRequest;
7494
}
7595

7696
/** redirect_url이 담긴 쿠키는 인증이 완전히 완료된 후에 제거되어야함. */
7797
public void clearCookies(HttpServletRequest request, HttpServletResponse response) {
78-
CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
79-
CookieUtils.deleteCookie(request, response, REDIRECT_URL_PARAM_COOKIE_NAME);
98+
CookieUtils.deleteCookie(
99+
request,
100+
response,
101+
OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME,
102+
CookieUtils.SameSite.LAX,
103+
null);
104+
CookieUtils.deleteCookie(
105+
request, response, REDIRECT_URL_PARAM_COOKIE_NAME, CookieUtils.SameSite.LAX, null);
80106
}
81107
}

0 commit comments

Comments
 (0)