Spring Security 6.x User/Admin Filter & Interceptor 구조

정서·2026년 2월 3일
  1. 전체 흐름

JWT → SecurityFilterChain → Custom Filter → Interceptor → Controller/Service

  1. user 구조
@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();
    }
}
  1. Admin 구조
    Filter
@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();
    }
}
항목UserAdmin
FilterUserInfoFilterAdminInfoFilter
InterceptorUserInfoInterceptorAdminInfoInterceptor
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 존재 여부 확인 (공용)

이런 구조가 되었다.

profile
잘 부탁드립니다!

0개의 댓글