2024. 3. 23. 23:00ใBackend/Spring
์ด๋ฌ ์ด๋ถํฐ ์ง์ธ 3๋ถ๊ณผ ํจ๊ป ์คํ๋ง ํ ์ด ํ๋ก์ ํธ๋ฅผ ์งํํ๊ณ ์๋ค.
ํด๋น ํ๋ก์ ํธ์์๋ ์๋ํฌ์ธํธ ๋ณดํธ ์์ ์ ์ํด Spring Security์ JWT๋ฅผ ์ด์ฉ ์ค์ธ๋ฐ, ์ด๋ฅผ ์ํด Spring Security์ OncePerRequestFilter๋ฅผ ๊ตฌํํ๋ JwtAuthFilter๋ฅผ ์๋์ฒ๋ผ ๋ฐ๋ก ์์ฑํด์ค ๋ค ํํฐ ์ฒด์ธ์ ์ถ๊ฐํด์ฃผ์๋ค.
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// Cookie ๋ด jwt ์ถ์ถ + verify + authentication ์์ฑ
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
} catch (JwtException ex) {
// ๊ฒ์ฆ ์คํจ ์ ๋ก์ง
}
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
// ํํฐ๋ฅผ ์ ์ฉํ์ง ์์ ์ผ์ด์ค์ ๋ํด ์ ์ฉ ์ฌ๋ถ๋ฅผ true or false๋ก ๋ฐํ
}
}
๋ฌธ์ ์ํฉ
๋ฌธ์ ๋ ์ ์ฒด ์์ฒญ ํ๋ฆ(์๋ธ๋ฆฟ + ํํฐ + ์ปจํธ๋กค๋ฌ ~)์ ํ ์คํธํ๋ e2e ํ ์คํธ๋ฅผ ์์ฑํ๋ ์์ค์ ๋ฐ์ํ์๋ค.
์ ํ๋ก์ ํธ์๋ GET /ticketings/{ticketingId} EP๊ฐ ์กด์ฌํ๋๋ฐ, ํด๋น EP์ ์๊ตฌ์ฌํญ์ ๋ก๊ทธ์ธ์ ํ์ง ์์ ์ฌ์ฉ์๋ ํธ์ถํ ์ ์์ด์ผ ํ๋ค๋ ๊ฒ์ด์๋ค. ๋ฐ๋ผ์ ์์ shouldNotFilter ๋ฉ์๋์์๋ ํด๋น EP์ ๋ํ ํธ์ถ์ ๋ํด true๋ฅผ ๋ฐํํด์ผ ํ๋ค. ํ์ง๋ง ์์ฑํด๋ ๋ฉ์๋๋ ๊ทธ๋ ๊ฒ ๋์ํ์ง ์์๋ค.
ํด๋น ๋ฉ์๋์ ์ด์์ ์๋์ ๊ฐ๋ค. (์ดํด๋ฅผ ๋๊ธฐ ์ํด ์กฐ๊ธ ์ฌ๊ตฌ์ฑํ์ฌ ์ค์ ์์ค์ฝ๋์๋ ๋ค๋ฅผ ์ ์์)
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
return getAllPublicPaths.contains(path);
}
private List<String> getAllPublicPaths() {
return List.of(
// ์ธ์ฆ์ ์๋ตํ ๋ค๋ฅธ EP๋ค ํฌํจ
"/ticketings/*"
);
}
์๋ฐ์ List.contains ๋ฉ์๋๋ ๊ฒฐ๊ตญ List๋ฅผ ์ํํ๋ฉฐ equals ํตํด ์ฐธ ๊ฑฐ์ง ํ์ ์ ํ๊ธฐ ๋๋ฌธ์, ์ฌ์ค ์์ฒ๋ผ ์์ฑ๋ ์ฝ๋๋ ์๋์ ์ ํ ๋ง์ง ์๋ ์์ ํ ์๋ชป๋ ์ฝ๋๋ก ๋ณผ ์ ์๋ค. (์ด๊ฑด ์ฌ์ค Spring Security ์ชฝ์์ ์ฌ์ฉํ๋ ๊ตฌ์ฑ ํด๋์ค์ RequestMatcher์์ ์ฌ์ฉํ๋ Ant ํจํด ๊ฐ๋ ์ ์ฐฉ๊ฐํ์ฌ ๊ฐ์ ธ์จ ์ค์์ด๋ค.)
์์ ๋ฐฉ์ 1
์ด๋ฐ ์ํฉ์์ ์ ์ผ ๋จผ์ ๋ ์ฌ๋ฆด ์ ์๋ ๋ฐฉ์์ ๊ฒฐ๊ตญ ์ ๊ทํํ์์ด๋ค.
์ด๋ฅผ ํตํ๋ฉด ์์ shouldNotFilter ๋ฉ์๋๋ ์๋์ฒ๋ผ ์์ฑ๋ ์ ์๋ค.
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
return getAllPublicPathRegexs.stream().anyMatch(regex ->
Pattern.compile(regex).matcher(input).matches()
);
}
private List<String> getAllPublicPathRegexs() {
return List.of(
// ์ธ์ฆ์ ์๋ตํ ๋ค๋ฅธ EP๋ค์ ์ ๊ท์ ํฌํจ
"/ticketings/([\\w-]+)$"
);
}
๋ค๋ง ํด๋น ๋ฐฉ์์ ๋์ ํ๋๋ฐ๋ ํฌ๊ฒ ๋๊ฐ์ง ๋ฌธ์ ๊ฐ ์๋ค๊ณ ์๊ฐํ๋ค.
๋ฌธ์ 1. ์ ์ง๋ณด์์ ์ด๋ ค์
์์ฒ๋ผ ์์ฑํ ๊ฒฝ์ฐ, ์๋ํ ๋ฐ๋๋ก ๋์์ํฌ ์๋ ์์ง๋ง ์ ์ง๋ณด์๊ฐ ์ด๋ ค์์ง๋ค. ๋๋ฅผ ํฌํจํ ํ์๋ค์ ๋ชจ๋ ํํฐ๋ฅผ ์ ์ฉํ์ง ์์ ์๋ํฌ์ธํธ๋ฅผ ๊ฐ๋ฐํ ๋๋ง๋ค ์ ๊ท์์ ํ๋์ฉ ์์ฑํด์ผํ๊ธฐ ๋๋ฌธ์ ํ๋์ ์ง์ ์ฅ๋ฒฝ์ผ๋ก ์์ฉํ ๊ฐ๋ฅ์ฑ์ด ์์ผ๋ฉฐ, ๋ฌผ๋ก ๊ตฌ๊ธ๋ง ํน์ ์์ฑํ AI์ ํ์ ๋น๋ฆด ์๋ ์๊ฒ ์ง๋ง ์์ ํจ์จ์ด ์๋์ค์ง๋ ์์ ๊ฒ์ด๋ค.
๋ฌธ์ 2. SecurityFilterChain์์ ๊ด๋ฆฌํ๋ EP์์ ์ด์ํ๋ก ์ธํ ๊ด๋ฆฌํฌ์ธํธ ์ฆ๊ฐ
์ฌ์ค shouldNotFilter์์ ์ํํ๋ ค๋ ์์ ์ ์ด๋ฏธ ์ ์ฌํ ๋ฐฉ์์ผ๋ก SecurityFilterChain์ ๊ตฌ์ฑํ๋ Configuration์ ์๋์ฒ๋ผ ์์ฑ๋์ด ์๋ค. (๊ฐ์ํํด์ ์์ฑ)
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(req ->
req.requestMatchers(GET, "/ticketings/*").permitAll()
);
return http.build();
}
๊ทธ๋ฐ๋ฐ ํด๋น ๋ถ๋ถ์์๋ ์ ๊ท์๋ณด๋ค ์กฐ๊ธ ๋ ๊ฐ์ํ๋ Ant ํจํด์ ์์ฑํด์ ๋ฃ์ด์ค๋ค. ์ฆ, ํด๋น ํ๋ก์ ํธ๋ ํน์ ์๋ํฌ์ธํธ๋ฅผ ์ธ๊ฐ ์์ด ๊ฐ๋ฐฉํ๊ธฐ ์ํด "Ant ํจํด"๊ณผ "์ ๊ท์"์ ๊ฐ๊ฐ ๊ด๋ฆฌํด์ผ ํ๋ ๊ฒ์ด๋ค. ์ด๋ ๊ฐ๋ฐํ๋๋ฐ ์์ด ๋ฌด์กฐ๊ฑด ์ค์๊ฐ ๋ฐ์ํ ์ ์๋ ์ง์ ์ด๊ณ , ๊ฐ๋ฐ ํจ์จ์ฑ์ ์ ํด์ํค๋ ์์ธ์ด ๋ ์ ์๋ค.
์์ ๋ฐฉ์ 2 (์ฑํ)
๋๋ฒ์งธ ์์ ๋ฐฉ์์ ๋ฐ๋ก Spring Security์ RequestMatcher๋ฅผ ์ด์ฉํ๋ ๊ฒ์ด๋ค.
์์ ๋ฌธ์ 2๋ฅผ ์ด๋ป๊ฒ ํด๊ฒฐํ ์ ์์๊น ๊ณ ๋ฏผํ๋ค๊ฐ,
FilterChain์์ EP์ ์ธ๊ฐ ๊ด๋ฆฌ๋ฅผ ์ํํ ๋ ์ฌ์ฉํ๋ RequestMatcher๋ฅผ ๊ทธ๋ฅ ๊ฐ์ ธ์์ ์ธ ์๋ ์๋?
๋ผ๋ ์์ด๋์ด์์ ์๊ฐํด๋ธ ๋ฐฉ์์ด๋ค.
RequestMatcher๋ matches ๋ฉ์๋๋ฅผ ๊ตฌํํด์ผํ๋ ์ธํฐํ์ด์ค์ ๋ถ๊ณผํ๊ธฐ ๋๋ฌธ์ ์ด์ ๊ตฌํ์ฒด ์ค AntPathRequestMatcher๋ฅผ ์ดํด๋ณด์.
// AntPathRequestMatcher.matches ๋ก์ง
@Override
public boolean matches(HttpServletRequest request) {
if (this.httpMethod != null && StringUtils.hasText(request.getMethod())
&& this.httpMethod != HttpMethod.valueOf(request.getMethod())) {
return false;
}
if (this.pattern.equals(MATCH_ALL)) {
return true;
}
String url = getRequestPath(request);
return this.matcher.matches(url);
}
private String getRequestPath(HttpServletRequest request) {
if (this.urlPathHelper != null) {
return this.urlPathHelper.getPathWithinApplication(request);
}
String url = request.getServletPath();
String pathInfo = request.getPathInfo();
if (pathInfo != null) {
url = StringUtils.hasLength(url) ? url + pathInfo : pathInfo;
}
return url;
}
RequestMatcher๋ฅผ ๊ตฌํํ๊ธฐ ์ํด ์์ฑ๋ matches ๋ก์ง์ ์ดํด๋ณด๋ฉด ์๋๋ก ์์ฝ์ด ๊ฐ๋ฅํ๋ค.
- HttpMethod ์ผ์น ์ฌ๋ถ ํ์ธ
- pattern์ด MATCH_ALL(/**) ์ธ์ง ํ์ธ
- url ํ๋ (servletPath + pathInfo, ์ด๋ฅผ ํตํด context-path๋ ๋ฌด์๋จ)
- this.matcher์๊ฒ ์ผ์น ํ์ ์์ (์ด๋ ์ต์ข ์ ์ผ๋ก AntPathMatcher์ ์์๋์ด Ant ํจํด์ ํตํด ๋งค์นญ ํ์ )
์ฆ, ์ด๋ฅผ ํตํ๋ฉด FilterChain๊ณผ OncePerRequestFilter์์ ๋ชจ๋ Ant ํจํด์ ํตํด ๊ฒ์ฆ์ ์ํํ ์ ์์ผ๋ฏ๋ก ๋ ๊ณณ์์ ์ฌ์ฉํ ๊ฒฝ๋ก ์ ๋ณด๋ฅผ ํ ๊ณณ์์ ๊ด๋ฆฌํ ์ ์๊ฒ๋๋ค. (context-path๋ฅผ ์ ๊ฒฝ ์์จ๋ ๋๋๊ฑด ๋ค)
๊ตฌํ
๊ตฌํ์ ์์ ๋ น์ฌ๋ด์ผํ ์ปจํ ์คํธ๋ฅผ ์ ๋ฆฌํ๋ฉด ์๋์ ๊ฐ๋ค.
- EP์ ๊ฒฝ๋ก๋ Ant ํจํด ํ์์ผ๋ก ํ ๊ณณ์์ ๊ด๋ฆฌ
- ํ๋ก์ ํธ ๋ด์์ ์ฌ์ฉํ๋ Role์ ์ข ๋ฅ๋ ๊ตฌ๋งค์, ํ๋งค์๊ฐ ์กด์ฌํ๊ณ ์ฌ๊ธฐ์ ์ถ๊ฐ์ ์ผ๋ก ๋ก๊ทธ์ธ์ ์ํํ์ง ์์ "๋ฐฉ๋ฌธ์"์ ๊ฐ๋ ๋ ์กด์ฌ (๋ฐฉ๋ฌธ์ < ๊ตฌ๋งค์ < ํ๋งค์ ์)
- ์ธ์๋ก Role์ ๋๊ฒจ์ฃผ๋ฉด ๊ทธ์ ๋์๋๋ RequestMatcher ๋ฒํฌ๋ฅผ ๋ฐํํด์ผ ํจ
- ์ด ๋ ๊ฒฝ๋ก๋ค์ HttpMethod์ ํจํด์ผ๋ก ๊ตฌ๋ถ๋จ
๊ทธ๋ฆฌ๊ณ ์ด์ ๋ฐ๋ผ ์์ฑํ ์ค์ ๋ก์ง์ ์๋์ ๊ฐ๋ค.
@Component
public class RequestMatcherManager {
/**
* if role == null, return permitAll Path
*/
public RequestMatcher getRequestMatchersByMinRole(@Nullable RoleEnum minRole) {
return new OrRequestMatcher(REQUEST_INFO_LIST.stream()
.filter(reqInfo -> {
if (reqInfo.minRole() == null) {
return minRole == null;
}
return reqInfo.minRole().equals(minRole);
})
.map(reqInfo -> new AntPathRequestMatcher(reqInfo.pattern(), reqInfo.method().name()))
.toArray(AntPathRequestMatcher[]::new));
}
// ๊ธธ์ด ๋ฌธ์ ์ ์ผ๋ถ๋ง ์์ฑ
private static final List<RequestInfo> REQUEST_INFO_LIST = List.of(
// member
new RequestInfo(GET, "/members/*/purchases", RoleEnum.BUYER),
new RequestInfo(GET, "/members/*/sale", RoleEnum.SELLER),
// auth
new RequestInfo(POST, "/auth/login", null),
new RequestInfo(POST, "/auth/refresh", null),
// ticketing
new RequestInfo(GET, "/ticketings", null),
new RequestInfo(GET, "/ticketings/*", null),
new RequestInfo(POST, "/ticketings", RoleEnum.SELLER),
// swagger
new RequestInfo(GET, "/v3/api-docs/**", null),
new RequestInfo(GET, "/swagger-ui.html", null),
new RequestInfo(GET, "/swagger-ui/**", null)
);
private record RequestInfo(HttpMethod method, String pattern, RoleEnum minRole) {
}
}
์์ฑ๋ ๋ก์ง์ ๋ํด ๊ฐ๋ตํ ์ค๋ช ํ๋ฉด ์๋์ ๊ฐ๋ค.
- ํผ๋ธ๋ฆญ ๋ฉ์๋๋ ๋ฐํํ ๊ฒฝ๋ก๋ค์ ์ต์ Role์ ๋ฐ๋๋ก ํ๋ค. ๋ฐฉ๋ฌธ์ ๊ฐ๋ ๋ ์กด์ฌํ๊ธฐ ๋๋ฌธ์ nullable๋ก ๋์๋ค.
- ๋ด๋ถ์ ์ผ๋ก ํ๋ผ์ด๋น ๋ ์ฝ๋์ธ RequestInfo๋ฅผ ํตํด EP ์ธ๊ฐ ๊ด๋ฆฌ์ ํ์ํ ์ ๋ณด๋ค์ ๋ด๋๋ก ํ๋ค. (๋ฉ์๋ - ๊ฒฝ๋ก - ์ต์ Role)
- ๊ทธ๋ฆฌ๊ณ ๊ฐ๋ฐ์๋ค์ด EP๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํ ๋ฆฌ์คํธ๋ฅผ ๋๊ณ , ๋ฉ์๋๊ฐ ํธ์ถ๋๋ฉด ์ด ์ค ์ต์ ๊ถํ์ด ๋์ผํ ์์๋ค๋ง์ ์ด์ฉํ์ฌ RequestMatcher๋ฅผ ๊ตฌ์ฑํ๋ค.
- ์ด ๋ ์ฌ๋ฌ RequestMatcher๋ฅผ ํ๋๋ก ๋ฌถ์ด์ฃผ๋ OrRequestMatcher๋ฅผ ์ด์ฉํ์ฌ ํ๋์ RequestMatcher๋ก ์ฌ๋ฌ EP์ ์ ๋ณด๋ฅผ ๋ด์ ์ ์๋๋ก ๊ตฌํํ๋ค.
์ ๊ตฌํ์ SecurityFilterChain ๊ตฌ์ฑ ๋ฉ์๋์ OncePerRequestFilter์ shouldNotFilter ๋ฉ์๋์ ์ ์ฉํ๋ฉด ๊ฐ๊ฐ ๋ค์๊ณผ ๊ฐ๋ค.
// SecurityConfig ๊ตฌ์ฑ ํด๋์ค ๋ด ๋น ๋ฑ๋ก ๋ฉ์๋
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// ๊ฐ๋ตํํ ์์ฑ
http.addFilterAfter(jwtAuthenticationFilter, BasicAuthenticationFilter.class)
.authorizeHttpRequests(req ->
req
.requestMatchers(requestMatcherManager.getRequestMatchersByMinRole(null))
.permitAll()
.requestMatchers(requestMatcherManager.getRequestMatchersByMinRole(SELLER))
.hasAnyAuthority(SELLER.name())
.requestMatchers(requestMatcherManager.getRequestMatchersByMinRole(BUYER))
.hasAnyAuthority(BUYER.name(), SELLER.name())
.anyRequest().authenticated()
);
return http.build();
}
// OncePerRequestFilter๋ฅผ ๊ตฌํํ JwtAuthFilter์ ๋ฉ์๋
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
// ํํฐ๋ฅผ ์ ์ฉํ์ง ์์ ๊ฒฝ๋ก๋ฅผ ๋ฐํํด์ผํ๊ธฐ ๋๋ฌธ์ ๋ฐฉ๋ฌธ์ Role (null)
return requestMatcherManager.getRequestMatchersByMinRole(null).matches(request);
}
์ด๋ฅผ ํตํด, GET /member/{memberId} ๊ฒฝ๋ก๋ฅผ ์ ํต๊ณผ์ํฌ ๋ฟ ์๋๋ผ, ์ด์ ์๋ ๋ ๊ด๋ฆฌ ์ง์ ์ ๊ฐ๊ฐ ๊ด๋ฆฌํ์ด์ผ ํ๋ ๋ฌธ์ ๋ฅผ ํจ๊ป ํด๊ฒฐํ ์ ์์๋ค. ๋ํ ์ด์ ์๋ context-path๋ฅผ ์ ๊ฒฝ์จ์คฌ์ด์ผ ํ๋ ๊ฒ๊ณผ ๋ฌ๋ฆฌ, AntPathRequestMatcher์ ๋ด๋ถ๊ตฌํ ๋ฐฉ์์ ์ํด ์ด์ ๋ํ ๊ณ ๋ ค๋ ํ์์์ด์ก๋ค.