์ธํฐ์
ํฐ๋ ์์ฒญ ์ /ํ์ ๊ณตํต ์ฒ๋ฆฌ ๋ก์ง์ ๋ด๋นํ๋ ์ปดํฌ๋ํธ์ด๋ค.
์ด์ ์ ์ ๋ฆฌํ Spring Filter์ ์ ์ฌํ์ง๋ง ๋์ํ๋ ๋ ๋ฒจ์์์ ์ฐจ์ด์ ์ด ์กด์ฌํ๋ค.
[ํด๋ผ์ด์ธํธ ์์ฒญ]
โ
[Filter]
โ
[DispatcherServlet]
โ
[Interceptor - preHandle()]
โ
[Controller]
โ
[Interceptor - postHandle()]
โ
[View Resolver & View]
โ
[Interceptor - afterCompletion()]
โ
[Filter ์๋ต ์ฒ๋ฆฌ]
โ
[ํด๋ผ์ด์ธํธ ์๋ต]
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !jwtProvider.isValid(token)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false; // ์ธ์ฆ ์คํจ: Controller๋ก ๋์ด๊ฐ์ง ์์
}
return true; // ์ธ์ฆ ์ฑ๊ณต: ๋ค์์ผ๋ก ์งํ
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
if (modelAndView != null) {
modelAndView.addObject("serverTime", LocalDateTime.now());
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
long startTime = (long) request.getAttribute("startTime");
long endTime = System.currentTimeMillis();
System.out.println("์์ฒญ ์๋ฃ - ์ด ์์ ์๊ฐ: " + (endTime - startTime) + "ms");
}
finally
์ ์ ์ฌํ ๋๋์ด๋ค.ํํฐ๋ฅผ ์จ์ ์ด๋ฏธ ๋ชจ๋ ์์ฒญ์ ๋ํด ๊ฑธ๋ฌ์ค ์ ์๋๋ฐ, ์ ์ธํฐ์
ํฐ๊น์ง ์ฐ๋ ๊ฑธ๊น?
์ด์ ๋ํ ์ด์ ๋ค์ ์๋์ ๊ฐ๋ค.
ํํฐ๋ HTTP ์์ฒญ/์๋ต ์ ์ฒด๋ฅผ ๋์์ผ๋ก ํ๋ค.
๋๋ฌธ์ ํด๋น ์์ฒญ์ด ์ด๋ ์ปจํธ๋กค๋ฌ๋ก ๊ฐ๊ณ , ์ด๋ ํ ๋น์ฆ๋์ค ํ๋ฆ์ ๊ฐ์ง๊ณ ์๋์ง ์ ์ ์๋ค.
๋ฐ๋ฉด ์ธํฐ์ ํฐ๋ ํด๋น ์์ฒญ์ด ์ด๋ ํ ์ปจํธ๋กค๋ฌ๋ก ๊ฐ๋์ง ์๊ณ ์์ผ๋ฉฐ, HandlerMethod๋ฅผ ํตํด์ ๋น์ฆ๋์ค ํ๋ฆ, ์ฌ์ฉ๋ ์ด๋ ธํ ์ด์ ๋ฑ์ ๋ชจ๋ ํ์ธํ ์๊ฐ ์๋ค.
๊ทธ๋ ๊ธฐ์ ํํฐ์ ๋นํด์ ๋์ฑ ์ธ๋ฐํ ๊ฑฐ๋ฆ๋ง
์ด ๋์ด์ค ์ ์๊ธฐ์ ์ฌ์ฉํ๋ ๊ฒ์ด๋ค.
์๋ฅผ ๋ค์ด์ ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ง ์ ๊ทผ์ด ๊ฐ๋ฅํ API๊ฐ ์๋ค๊ณ ๊ฐ์ ์ ํด ๋ณด์.
@GetMapping("/api/user/profile")
public ResponseEntity<UserProfile> getProfile(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token == null || !jwtProvider.validate(token)) {
throw new UnauthorizedException();
}
// ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ํ๋กํ ์กฐํ
UserProfile profile = userService.getProfile(token);
return ResponseEntity.ok(profile);
}
์์ ๊ฐ์ ํํ๋ฅผ ๋๊ฒ ๋ ๊ฒ์ด๋ค.
์ธ์ฆ์ด ํ์ํ API๊ฐ 1~2๊ฐ ์ ๋๋ผ๋ฉด ํฐ ๋ฌธ์ ๊ฐ ์ ๋๊ฒ ์ง๋ง, ๋ณดํต ๋๋ถ๋ถ์ API์์ ์ธ์ฆ์ ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค.
๊ทธ๋ ๋ค๋ฉด ํด๋นํ๋ ๋ชจ๋ API ๋ค์ ๋ํ์ฌ ์ธ์ฆ ์ฝ๋๊ฐ ๊ณตํต์ ์ผ๋ก ํฌํจ๋ ๊ฒ์ด๊ณ , ์ด๋ ์ฝ๋ ์ค๋ณต์ ์ผ๊ธฐํ๋ค.
๋ํ ์์ ์ฝ๋์์๋ ์ปจํธ๋กค๋ฌ์์ ์ธ์ฆ๊น์ง ๋งก๊ณ ์๊ธฐ์, ๊ด์ฌ์ฌ๊ฐ ์ ์ ํ๊ฒ ๋ถ๋ฆฌ๋์ง ์์ ๋ณด์ธ๋ค.
Interceptor๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ค ์์ผ๋ก ๊ฐ์ ์ด ๋ ๊น?
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !jwtProvider.validate(token)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return false;
}
return true; // ์ ์ ํต๊ณผ
}
}
ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฆ๋ง ์งํํ๋ AuthInterceptor
๋ฅผ ๋ง๋ค์ด ๋๋ค.
public class AdminInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !jwtProvider.validate(token)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return false;
}
UserInfo userInfo = jwtProvider.getUserInfo(token);
if (!userInfo.isAdmin()) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden: Admins only");
return false;
}
return true; // ์ ์ ํต๊ณผ
}
}
ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฆ ์ดํ ๊ด๋ฆฌ์์ธ์ง ํ์ธ๊น์ง ์งํํ๋ AdminInterceptor
๋ ๋ง๋ค์ด ๋๋ค.
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// ์ผ๋ฐ ์ฌ์ฉ์ ์ธ์ฆ์ฉ
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/api/users/**");
// ๊ด๋ฆฌ์ ๊ถํ ์ธ์ฆ์ฉ
registry.addInterceptor(new AdminInterceptor())
.addPathPatterns("/api/admin/**");
}
}
์ด๋ฅผ WebMvcConfig
์ ๋ฑ๋กํจ์ผ๋ก์จ ์์์ ๋ง๋ค์ด ๋ ์ธํฐ์
ํฐ๋ฅผ ์ ์ฉํ ์ ์๋ค.
.addPathPatterns("/api/users/**");
๋ /api/users/**
์๋ํฌ์ธํธ๋ก ์์ฒญ์ด ๋ค์ด์ค๋ ๊ฒฝ์ฐ์ ์๋ํ๋ค๋ ๋ป์ด๋ค.
์ฆ, ์์ ๊ฐ์ด ์ธํฐ์ ํฐ๋ฅผ ๊ฐ๊ฐ ๋ง๋ ํ์ ์ํ๋ ์๋ํฌ์ธํธ์ ์ ์ฉํ ์ ์๋ค. ๋ชจ๋ ์์ฒญ์ ๋ํด ์ ์ฉ๋๋ ํํฐ์ ๋นํด์ ๋์ฑ ์ธ๋ฐํ ๋ชจ์ต์ ๋ณผ ์ ์๋ค.
ํ์ง๋ง ์ ์์ ์ฝ๋๋ฅผ ๋ณด๋ฉด ๋ ๋ค์ ์ค๋ณต๋๋ ๋ก์ง์ด ๋ณด์ธ๋ค.
if (token == null || !jwtProvider.validate(token)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return false;
}
๋ฐ๋ก ์ด ๋ถ๋ถ์ธ๋ฐ, ํด๋น ๋ถ๋ถ๊น์ง ๊ณตํต ๋ก์ง์ผ๋ก ๋ถ๋ฆฌํด๋ณด๋ ค๊ณ ํ๋ค.
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !jwtProvider.validate(token)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return false;
}
// ์ ํจํ ํ ํฐ์ด๋ฉด ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ์ฅ
UserInfo userInfo = jwtProvider.getUserInfo(token);
request.setAttribute("userInfo", userInfo);
return true;
}
}
ํ ํฐ ๊ฒ์ฆ์ ๋ง์น ํ, ๋ฌธ์ ๊ฐ ์๋ค๋ฉด ๋ค์ ์ธํฐ์
ํฐ ๋์์ ์ํด์ request
์ ์ ์ ์ ๋ณด๋ฅผ ์ ์ฅํด ๋๋ค.
public class AdminInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
UserInfo userInfo = (UserInfo) request.getAttribute("userInfo");
if (userInfo == null || !userInfo.isAdmin()) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden: Admins only");
return false;
}
return true;
}
}
๊ด๋ฆฌ์์ธ์ง ๊น์ง ํ์ธํด์ผ ํ๋ API์์๋, ์ ๊ณผ์ ์์ ์ ์ฅ๋ ์ ์ ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์ต์ข ํ์ธํ๊ฒ ๋๋ค.
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/api/users/**", "/api/admin/**");
registry.addInterceptor(new AdminInterceptor())
.addPathPatterns("/api/admin/**");
}
๋ง์ง๋ง์ผ๋ก ์ธํฐ์
ํฐ ๋ฑ๋ก์ ์งํํด ์ฃผ๋ฉด ๋๋ฉฐ, AuthInterceptor
์ ๋์์ด ์ ํ๋์ด์ผ ํ๊ธฐ์ ๋จผ์ ๋ฑ๋กํด์ฃผ์ด์ผ ํ๋ค.
๊ฒฐ๋ก ๋ถํฐ ๋งํ์๋ฉด ์ธํฐ์
ํฐ๋ฅผ ์ฌ์ฉํ์ง๋ ์๊ณ , ์ธ์ฆ์ธ๊ฐ ์ฒ๋ฆฌ๋ฅผ ์ํด์ Spring Security
์ JwtFilter
๋ฅผ ํ์ฉํ๊ณ ์๋ค.
์ฝ๋๋ ์๋์ ๊ฐ๋ค.
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final CustomUserDetailsService customUserDetailsService;
/**
* ์์ฒญ์ ์ฒ๋ฆฌํ๋ฉฐ JWT ๊ฒ์ฆ ๋ฐ ์ธ์ฆ ์ค์ ์ ์ํํฉ๋๋ค.
*
* @param request HTTP ์์ฒญ ๊ฐ์ฒด
* @param response HTTP ์๋ต ๊ฐ์ฒด
* @param filterChain ํํฐ ์ฒด์ธ ๊ฐ์ฒด
* @throws ServletException ์๋ธ๋ฆฟ ์์ธ ๋ฐ์ ์
* @throws IOException ์
์ถ๋ ฅ ์์ธ ๋ฐ์ ์
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
// โ
OPTIONS ์์ฒญ์ผ ๊ฒฝ์ฐ ํํฐ๋ฅผ ๊ฑด๋๋ฐ๊ณ ๋ฐ๋ก ๋ค์ ํํฐ ์คํ
if (request.getMethod().equalsIgnoreCase("OPTIONS")) {
filterChain.doFilter(request, response);
return;
}
String token = jwtUtil.getTokenFromHeader(request.getHeader("Authorization"));
jwtUtil.validateToken(token);
Long userId = jwtUtil.getClaimFromToken(token, "userId", Long.class);
setAuthentication(userId);
} catch (Exception e) {
log.error("JWT validation failed: " + e.getMessage());
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
/**
* ์ธ์ฆ ์ ๋ณด๋ฅผ SecurityContext์ ์ค์ ํฉ๋๋ค.
*
* @param userId ์ธ์ฆ๋ ์ฌ์ฉ์์ ID
*/
private void setAuthentication(Long userId) {
UserDetails userDetails = customUserDetailsService.loadUserByUserId(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
/**
* ํน์ ๊ฒฝ๋ก์ ๋ํด JWT ๊ฒ์ฆ์ ์๋ตํฉ๋๋ค.
*
* @param request HTTP ์์ฒญ ๊ฐ์ฒด
* @return true์ผ ๊ฒฝ์ฐ ํด๋น ์์ฒญ์ ๋ํด ํํฐ๋ฅผ ์ ์ฉํ์ง ์์
*/
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getServletPath();
return path.startsWith("/api/v1/users/onboarding");
}
}
๋ ๊ธฐ์ ์ ๋ํด์๋ ์ถํ ๊ฐ๋ณ ํฌ์คํ ์ผ๋ก ์งํํ ์์ ์ด๋ฏ๋ก, ์ฌ๊ธฐ์๋ ๊ฐ๋จํ๊ฒ ์ฅ์ ๋ง ์ธ๊ธํด ๋ณด๋ ค๊ณ ํ๋ค.
ํํฐ๋ฅผ ์ฌ์ฉํ๋ฉด ๋์คํจ์ฒ์๋ธ๋ ์ด์ ์ ์์ฒญ์ ๊ฐ๋ก์ฑ ์ ์ ํ์ง ๊ฒ์ฆํด ์ค ์ ์๋ค.
๋๋ฌธ์ ๋์คํจ์ฒ์๋ธ๋ ์ดํ์ ์ธ์ฆ์ธ๊ฐ๋ฅผ ์งํํ๋ ์ธํฐ์ ํฐ ๋ฐฉ์๋ณด๋ค ๋ณด์์ฑ์ ๋์ผ ์ ์์ผ๋ฉฐ, ๋ถํ์ํ ์๋ฒ ๋ฆฌ์์ค ๋ญ๋น๋ ๋ง์ ์ ์๋ค.
์ธ์ฆ ์ฑ๊ณต ํ, SecurityContext
๋ผ๋ ์คํ๋ง ๋ณด์ ์ ์ฅ์์ ์ธ์ฆ ์ ๋ณด๋ฅผ ์ ์ฅํด ๋ ์ ์๋ค.
๊ทธ๋ฌ๋ฉด ์ดํ ํ๋ ์ ํ
์ด์
, ์ ํ๋ฆฌ์ผ์ด์
๋ ์ด์ด ์ด๋์๋
SecurityContextHolder.getContext().getAuthentication()
๋ฅผ ํตํด์ ํด๋น ์ ๋ณด๋ฅผ ๊บผ๋ด์ ์ฌ์ฉํ ์ ์๋ค.
๐ง๐ปโ๐ป ๋ ๋ง์ ์ฅ์ ์ด ์กด์ฌํ๊ฒ ์ง๋ง Spring Security์ ๋ฏธ์ํ ์ํ๋ก ์ ๋ ๊ฒ์ ์ข์ง ์์ ๊ฒ ๊ฐ์ ์ถํ ๋ ์์๋ณด๋ ค๊ณ ํ๋ค!