JWT → SecurityFilterChain → Custom Filter → Interceptor → Controller/Service
@Component
public class UserInfoFilter extends OncePerRequestFilter {
private final UserService userService;
private final ObjectMapper objectMapper;
public UserInfoFilter(UserService userService, ObjectMapper objectMapper) {
this.userService = userService;
this.objectMapper = objectMapper;
}
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain)
throws ServletException, IOException {
try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof JwtAuthenticationToken jwt && jwt.getToken() != null) {
Long userKey = jwt.getToken().getClaim("userKey");
if (userKey == null) { writeError(res, ResultCode.UNAUTHORIZED); return; }
var user = userService.getUserInfo(userKey);
if (user == null) { writeError(res, ResultCode.UNAUTHORIZED); return; }
req.setAttribute("userInfo", user);
}
chain.doFilter(req, res);
} catch (Exception e) { writeError(res, ResultCode.INTERNAL_SERVER_ERROR); }
}
private void writeError(HttpServletResponse res, ResultCode code) throws IOException {
if(res.isCommitted()) return;
res.resetBuffer();
res.setStatus(code.getHttpStatus().value());
res.setContentType("application/json");
objectMapper.writeValue(res.getWriter(), Map.of("code", code.getCode(), "desc", code.getDescription()));
}
}
Interceptor
@Component
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object h) {
if(RequestAttributeUtils.getUserInfo(req) == null)
throw new HttpException(ResultCode.UNAUTHORIZED);
return true;
}
}
SecurityConfig
@Configuration
public class SecurityConfig {
private final UserInfoFilter userInfoFilter;
public SecurityConfig(UserInfoFilter userInfoFilter) {
this.userInfoFilter = userInfoFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.addFilterAfter(userInfoFilter, BearerTokenAuthenticationFilter.class);
return http.build();
}
}
@Component
public class AdminInfoFilter extends OncePerRequestFilter {
private final AdminUserService service;
private final ObjectMapper mapper;
public AdminInfoFilter(AdminUserService service, ObjectMapper mapper) {
this.service = service;
this.mapper = mapper;
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof JwtAuthenticationToken jwt && jwt.getToken() != null) {
Long key = jwt.getToken().getClaim("adminKey");
if(key == null) { writeError(res, ResultCode.UNAUTHORIZED); return; }
var admin = service.getAdminInfo(key);
if(admin == null) { writeError(res, ResultCode.UNAUTHORIZED); return; }
req.setAttribute("userInfo", admin);
}
chain.doFilter(req, res);
} catch(Exception e){ writeError(res, ResultCode.INTERNAL_SERVER_ERROR); }
}
private void writeError(HttpServletResponse res, ResultCode code) throws IOException {
if(res.isCommitted()) return;
res.resetBuffer();
res.setStatus(code.getHttpStatus().value());
res.setContentType("application/json");
mapper.writeValue(res.getWriter(), Map.of("code", code.getCode(), "desc", code.getDescription()));
}
}
Interceptor
@Component
public class AdminInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object h) {
if(RequestAttributeUtils.getUserInfo(req) == null)
throw new HttpException(ResultCode.UNAUTHORIZED);
return true;
}
}
SecurityConfig
@Configuration
public class AdminSecurityConfig {
private final AdminInfoFilter adminInfoFilter;
public AdminSecurityConfig(AdminInfoFilter filter) { this.adminInfoFilter = filter; }
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.securityMatcher("/admin/**")
.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.addFilterAfter(adminInfoFilter, BearerTokenAuthenticationFilter.class);
return http.build();
}
}
| 항목 | User | Admin |
|---|---|---|
| Filter | UserInfoFilter | AdminInfoFilter |
| Interceptor | UserInfoInterceptor | AdminInfoInterceptor |
| RequestAttribute key | "userInfo" | "userInfo" (공용) |
| SecurityFilterChain | /api/** | /admin/** |
| addFilter 위치 | .addFilterAfter(..., BearerTokenAuthenticationFilter.class) | 동일 |
✅ Tip
.addFilter(filter, Class)는 6.x에서 제거됨 → .addFilterBefore / .addFilterAfter 사용
Interceptor는 단순 검증만, DB 조회는 Filter에서 처리
공용 RequestAttribute("userInfo")로 User/Admin 구분 가능
이렇게 UserInfoInterceptor / AdminInfoInterceptor 이 각각 존재하면
장점
User/Admin 인증 로직 분리 → 역할별로 세밀하게 커스터마이징 가능
유지보수 시, User쪽과 Admin쪽 로직 충돌 없음
단점
코드 중복: 지금 인터셉터는 사실 “RequestAttribute 존재 여부 확인”만 함
라는 장단점이 있다.
나는 코드중복을 좋아하지 않기 때문에 합치고 싶어 ai에게 물어봤다..
그렇게
하나의 공용 인터셉터로 통합
@Component
public class InfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object h) {
if(RequestAttributeUtils.getUserInfo(req) == null)
throw new HttpException(ResultCode.UNAUTHORIZED);
return true;
}
}
장점
코드 단순화 → User/Admin 공용
단점
특정 경로(/admin/, /api/)에서만 적용하려면, WebMvcConfigurer에서 path별 등록 필요
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(infoInterceptor)
.addPathPatterns("/api/**", "/admin/**");
}
필터에서 이미 User/Admin 구분해서 RequestAttribute 저장하므로, 인터셉터는 단순 체크만으로 충분
결론
필터에서 이미 JWT → DB 조회 → RequestAttribute 까지 처리하니까
인터셉터는 단순히 RequestAttribute 존재 여부만 체크하면 O
그래서 굳이 User/Admin 두 개 만들 필요는 없고, 공용 InfoInterceptor 하나로 충분
UserInfoFilter → JWT 확인 + DB 조회 → RequestAttribute
AdminInfoFilter → JWT 확인 + DB 조회 → RequestAttribute
InfoInterceptor → RequestAttribute 존재 여부 확인 (공용)
이런 구조가 되었다.