페이지 불러오는 중
B2B 엔터프라이즈 솔루션을 공급하는 과정에서 고객사별로 상이한 인증 인프라 환경과 엄격한 권한 분리 요구사항이 있었습니다.
인증 확인 로직을 Controller나 DispatcherServlet 이전인 서블릿 컨테이너 레벨(Spring Security Filter Chain 직전/선행)에 배치하여, 미인증 요청이 내부 비즈니스 로직에 접근하지 못하도록 원천 차단했습니다.
// ⭕ Security Filter Chain 선행 커스텀 SSO 필터
public class SsoAuthenticationFilter extends OncePerRequestFilter {
private final SsoAuthenticationService ssoService;
public SsoAuthenticationFilter(SsoAuthenticationService ssoService) {
this.ssoService = ssoService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 1. 추상화된 SSO 서비스를 통한 인증 결과 획득
SsoUserPrincipal principal = ssoService.authenticate(request, response);
if (principal != null) {
// 2. 내부 RBAC DB 조회를 통해 인가(Authority) 목록 생성
List<GrantedAuthority> authorities = ssoService.loadUserAuthorities(principal.getUserId());
// 3. Spring SecurityContextHolder에 인증 토큰 주입 (기존 세션/인가 체계 100% 재활용)
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(principal, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
고객사마다 SSO 인증을 전달하는 방식이 상이하므로, 인증 기능을 SsoAuthenticationService 인터페이스로 추상화하여 설정 파일(Property) 기반으로 구현체를 갈아끼울 수 있는 플러그앤플레이(Plug-and-Play) 구조를 확립했습니다.
[다양한 B2B 연동 방식에 대응하는 3대 전략 구현체]
1. AgentRedirectStrategy: 전용 SSO Agent를 통해 매 요청마다 Redirect 파라미터/헤더로 토큰 전달
2. InternalSocketApiStrategy: 백채널(Back-channel) 소켓 통신 또는 REST API로 인증 티켓 검증
3. ExternalDbAgentStrategy: 자체 에이전트가 외부 IdP/인사 DB를 직접 조회하여 인증 컨텍스트 주입
AWS IAM이나 Azure RBAC과 유사하게, 기능 단위의 고유 권한(Permission)을 정의하고 이를 역할(Role) 단위로 그룹핑하여 사용자에게 다중 부여할 수 있는 정규화된 ERD를 설계했다.
[RBAC ERD 구조]
TB_USER (사용자)
├── 1:N ── TB_USER_ROLE (사용자-역할 매핑)
└── N:1 ── TB_ROLE (역할/그룹)
└── 1:N ── TB_ROLE_PERMISSION (역할-권한 매핑)
└── N:1 ── TB_PERMISSION (기능/메뉴 권한 단위)
@Aspect) 기반 메서드 단위 통제초기 레거시 시스템은 URL 구조가 RESTful하지 않고 비표준화되어 있어 URL 패턴만으로 인가를 통제하기 어려웠다. 따라서 핵심 Controller/Service 메서드에 @RequirePermission 커스텀 어노테이션과 @Aspect를 적용하여 선제적으로 권한을 검증했다.
@Aspect
@Component
public class AuthorizationAspect {
@Before("@annotation(requirePermission)")
public void checkPermission(JoinPoint joinPoint, RequirePermission requirePermission) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String requiredRole = requirePermission.value();
boolean hasAuthority = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals(requiredRole));
if (!hasAuthority) {
throw new AccessDeniedCustomException("해당 기능에 대한 실행 권한이 없습니다.");
}
}
}
HandlerInterceptor 기반 단일 진입점 통합 제어URL 체계를 표준화/패턴화한 이후에는 유지보수성과 성능을 위해 HandlerInterceptor로 인가 통제 레이어를 통합했다.
antMatcher를 통해 인터셉터 로직을 안전하게 우회(excludePathPatterns).@Component
public class MenuAccessInterceptor implements HandlerInterceptor {
@Autowired
private MenuAuthorizationCacheService menuCacheService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestUri = request.getRequestURI();
// 1. 메뉴 DB 기반 접근 가능 여부 캐시 조회
if (menuCacheService.isProtectedMenu(requestUri)) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (!menuCacheService.hasMenuAccess(auth, requestUri)) {
// 2. 권한 부족 시 전용 랜딩 페이지로 안전 리다이렉트
response.sendRedirect("/error/access-denied");
return false;
}
}
return true;
}
}