// Developer · Debugging Reports

뉴스도 디버깅이 필요해

마주친 오류를 재현하고, 원인을 추적하고, 기록합니다. Java · Spring · DB · 실무 트러블슈팅.

debug — today.log
$ debug --trace "오늘의 뉴스"
> 헤드라인 파싱 중...
> 원인 추적: 근거 누락 · 출처 불명
> 패치 적용: 사실 확인 + 출처 보강
✓ RESOLVED
TOTAL
TODAY
YESTERDAY

Categories

웹개발/Spring

Spring Security 없이 권한 체크 구현하기 정리

자바를잡아2026. 7. 5. 09:00
반응형

왜 Security 없이 권한 체크를 하게 되나

로그인 검사까지는 인터셉터로 처리했는데, 그다음 문제는 "로그인한 사람 중에서도 누구는 되고 누구는 안 되게" 거르는 것이다. 관리자만 들어갈 화면을 일반 사용자가 못 들어오게 막는 식이다. 정석은 Spring Security지만, 레거시 구조나 일정·학습 비용 때문에 당장 도입이 어려운 경우가 있다. 그래서 Spring Security 없이 권한 체크를 직접 구현하게 된다.

먼저 분명히 해 둘 점이 있다. 직접 구현하는 권한 체크는 "임시방편 또는 단순한 요구사항"에 한정하는 것이 좋다. 인증·인가는 보안의 핵심이라, 검증된 프레임워크가 막아 주는 수많은 공격 시나리오를 직접 구현으로 모두 커버하기는 어렵다. 이 글은 그 전제 위에서, 세션의 권한 정보(role)를 활용한 기본적인 접근 제어 방법을 정리한다.

인증과 인가 구분

구분 인증(Authentication) 인가(Authorization)
질문 누구인가? 무엇을 할 수 있는가?
처리 시점 로그인 시 로그인 이후 각 요청
판단 근거 아이디/비밀번호 세션의 권한(role)
실패 시 로그인 페이지(401/리다이렉트) 접근 거부(403)
본 글 범위 앞 글에서 처리 이 글의 주제

권한 정보를 세션에 두기

1. 로그인 시 role 저장

권한 체크의 출발은 로그인 시점에 사용자의 권한 정보를 세션에 담아 두는 것이다. 앞서 다룬 LoginUserVO에 role 필드를 두고, 신뢰할 수 있는 출처(DB)에서 가져온 값을 담는다.

// 로그인 성공 시 (권한은 반드시 서버/DB 기준 값)
LoginUserVO loginUser = new LoginUserVO();
loginUser.setUserId(member.getUserId());
loginUser.setUserName(member.getUserName());
loginUser.setRole(member.getRole()); // 예: "ADMIN", "USER"
session.setAttribute("loginUser", loginUser);

중요한 점은 권한 값이 서버가 DB에서 조회한 값이어야 한다는 것이다. 클라이언트가 보낸 파라미터나 쿠키 값을 그대로 권한으로 쓰면, 사용자가 임의로 권한을 올릴 수 있어 심각한 보안 구멍이 된다.

2. 권한이 여러 개인 경우

한 사용자가 여러 권한을 가질 수 있다면 단일 문자열 대신 목록으로 관리한다.

// 권한이 복수인 경우
loginUser.setRoles(Arrays.asList("USER", "BOARD_ADMIN"));
// 체크 시: loginUser.getRoles().contains("BOARD_ADMIN")

권한 체계가 복잡해질수록(역할-권한 매핑, 메뉴별 세분화) 직접 구현의 관리 부담이 커진다. 이 지점이 프레임워크 도입을 고민해야 하는 신호이기도 하다.

인터셉터로 권한 체크

로그인 검사 인터셉터에 권한 검사를 더한다. 특정 경로는 특정 권한이 있어야 통과시키고, 없으면 403(접근 거부)으로 처리한다.

public class RoleInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        LoginUserVO loginUser = (session != null)
                ? (LoginUserVO) session.getAttribute("loginUser") : null;

        // 인증 먼저 (비로그인은 로그인 페이지로)
        if (loginUser == null) {
            response.sendRedirect(request.getContextPath() + "/login.do");
            return false;
        }

        // 인가: /admin 경로는 ADMIN 권한 필요
        String uri = request.getRequestURI();
        if (uri.contains("/admin") && !"ADMIN".equals(loginUser.getRole())) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN); // 403
            return false;
        }
        return true;
    }
}

위는 경로 문자열로 단순 판단한 예시다. URL 패턴과 필요한 권한을 코드에 직접 박으면 관리가 어려워지므로, 매핑 정보를 설정으로 분리하는 편이 낫다. 다음 항목에서 본다.

권한 매핑을 설정으로 분리

경로별 필요한 권한을 코드에 흩뿌리지 않고 한곳에 모아 두면, 권한 정책이 바뀌어도 그 부분만 고치면 된다. 간단히는 Map으로 관리할 수 있다.

// 경로 패턴 → 필요한 권한 매핑 (예시)
private static final Map<String, String> URL_ROLE = new LinkedHashMap<>();
static {
    URL_ROLE.put("/admin/", "ADMIN");
    URL_ROLE.put("/manage/", "MANAGER");
}

// preHandle 안에서
for (Map.Entry<String, String> e : URL_ROLE.entrySet()) {
    if (uri.contains(e.getKey()) && !e.getValue().equals(loginUser.getRole())) {
        response.sendError(HttpServletResponse.SC_FORBIDDEN);
        return false;
    }
}

매핑이 많아지고 패턴이 복잡해지면 이 방식도 한계에 부딪힌다. 경로 매칭의 정밀함(와일드카드, 우선순위, 메서드별 구분)이 필요해지는 순간, 직접 구현보다 프레임워크가 훨씬 안전하고 정확하다.

화면(JSP)에서 권한별 메뉴 분기

접근을 막는 것과 별개로, 화면에서도 권한 없는 메뉴는 보이지 않게 하는 것이 사용성에 좋다. JSTL로 세션의 권한을 확인해 분기한다.

<%-- 관리자에게만 관리 메뉴 노출 --%>
<c:if test="${sessionScope.loginUser.role eq 'ADMIN'}">
    <li><a href="${pageContext.request.contextPath}/admin/main.do">관리자</a></li>
</c:if>

주의할 점은, 화면에서 메뉴를 숨기는 것은 보안 처리가 아니라는 것이다. 메뉴를 숨겨도 URL을 직접 입력하면 접근할 수 있다. 실제 차단은 반드시 서버(인터셉터)에서 해야 한다. 화면 분기는 사용성, 서버 검사는 보안 — 이 둘을 혼동하면 안 된다.

직접 구현의 한계와 주의점

  • 화면 숨김은 보안이 아니다 — URL 직접 접근을 서버에서 막지 않으면 메뉴를 숨겨도 뚫린다.
  • 권한 값은 서버 기준 — 클라이언트가 보낸 값을 권한으로 신뢰하지 않는다.
  • 경로 매칭의 허점 — 단순 contains 판단은 의도치 않은 경로를 허용/차단할 수 있다. 정밀한 매칭이 필요하면 프레임워크가 안전하다.
  • 일관성 유지의 어려움 — 권한 정책이 늘수록 여러 곳을 손봐야 해 누락 위험이 커진다.
  • 검증된 공격 방어 부재 — CSRF, 세션 처리 등 프레임워크가 기본 제공하는 보호를 직접 모두 구현하기 어렵다.

정리하면, 직접 구현은 단순하고 한정된 요구에는 쓸 수 있지만, 권한 체계가 조금만 복잡해지거나 보안 요구가 높아지면 Spring Security 같은 검증된 프레임워크로 가는 것이 안전하다. 특히 공공·금융처럼 보안 점검이 엄격한 환경에서는 직접 구현이 지적 대상이 되기 쉽다.

FAQ

Q1. 인증과 인가는 어떻게 다른가요?

인증은 "누구인지"를 확인하는 로그인 단계, 인가는 "무엇을 할 수 있는지"를 권한으로 판단하는 단계다. 인증 실패는 로그인 페이지로, 인가 실패는 403으로 처리한다.

Q2. 권한 정보는 어디에 두나요?

로그인 시 DB에서 조회한 권한을 세션의 사용자 VO에 담는다. 클라이언트가 보낸 값을 권한으로 쓰면 안 된다.

Q3. 메뉴를 숨기면 권한 처리가 끝나나요?

아니다. 화면에서 메뉴를 숨겨도 URL을 직접 입력하면 접근할 수 있다. 실제 차단은 서버(인터셉터)에서 해야 한다.

Q4. 권한 없을 때는 무엇을 보여주나요?

보통 403(접근 거부) 처리 후 안내 화면을 보여준다. 로그인은 됐으나 권한이 없는 상태이므로 로그인 페이지가 아니라 거부 안내가 맞다.

Q5. 직접 구현과 Spring Security 중 무엇을 써야 하나요?

요구가 단순하고 임시라면 직접 구현도 가능하지만, 권한 체계가 복잡하거나 보안 요구가 높으면 Spring Security를 권한다. 검증된 방어와 일관된 관리가 가능하다.

정리

Spring Security 없이 권한 체크는 "로그인 시 DB 기준 권한(role)을 세션에 담고, 인터셉터에서 경로별 필요한 권한과 비교해 없으면 403으로 막는" 구조다. 인증(누구인가)과 인가(무엇을 할 수 있는가)를 구분하고, 권한 매핑은 코드에 흩뿌리지 말고 설정으로 모아 관리한다. 화면의 메뉴 숨김은 사용성일 뿐 보안이 아니므로, 실제 차단은 반드시 서버에서 한다. 다만 직접 구현은 단순한 요구에 한정하는 것이 안전하다. 권한 체계가 복잡해지거나 보안 점검이 엄격한 환경이라면, 검증된 Spring Security로 전환하는 것을 적극 검토하는 편이 좋다.

반응형