스프링 시큐리티: 인증·인가와 세션 vs 토큰
웹 서비스에서 "이 사용자가 누구인지"와 "이 사용자가 뭘 할 수 있는지"는 어떻게 구분하고, 각각 어떤 방식으로 구현할 수 있을까요?
인증과 인가는 보안의 두 축입니다. 인증이 "신분증 확인"이라면, 인가는 "출입 권한 확인"입니다. 이 둘의 구현 방식에 따라 서비스의 확장성과 보안 수준이 달라집니다.
인증과 인가
-
인증(Authentication): 사용자나 장치의 신원을 확인하는 "누구인지 증명"하는 과정 입니다.
예시: 아이디/패스워드를 통한 로그인, 신분증·여권을 통한 본인 확인
-
** 인가(Authorization)**: 인증된 사용자가 특정 리소스에 접근할 수 있는 "권한이 있는지 확인"하는 절차 입니다.
예시: 관리자 페이지 접근, 자신이 작성한 게시글·댓글만 수정 가능
인증이 선행되어야 인가가 가능합니다. "누구인지" 모르면 "뭘 할 수 있는지"를 판단할 수 없기 때문입니다.
인증 방식 비교
매 요청마다 아이디/패스워드를 보내는 방식

모든 요청에 자격 증명을 포함시키는 가장 단순한 방식입니다.
- ** 장점:** 서버가 상태를 유지할 필요가 없어 구현이 단순합니다.
- ** 단점:** 매 요청마다 자격 증명이 네트워크를 타기 때문에 탈취 위험이 높고, 사용자 경험이 좋지 않습니다.
쿠키와 세션을 이용한 방식

로그인 성공 시 서버가 세션을 생성하고, 세션 ID를 쿠키로 내려보내 이후 요청마다 사용자를 식별하는 방식입니다.
이 방식이 동작하는 이유는 브라우저가 같은 도메인으로의 요청에 쿠키를 ** 자동으로** 포함시키기 때문입니다. 서버는 전달받은 세션 ID로 세션 저장소에서 사용자 정보를 찾습니다.
- ** 장점:** 서버가 세션을 중앙에서 관리하므로 강제 로그아웃이나 세션 만료 처리가 쉽고, 사용자는 한 번 로그인으로 여러 요청을 편리하게 보낼 수 있습니다.
- ** 단점:** 서버가 세션 상태를 들고 있어야 하기 때문에, 서버를 수평 확장할 때 세션 공유를 위한 Redis 같은 추가 인프라가 필요합니다.
토큰을 이용한 방식

로그인 성공 시 사용자 정보와 권한을 담은 토큰(예: JWT)을 발급하고, 이후 요청마다 클라이언트가 이 토큰을 직접 포함시키는 방식입니다.
세션 방식과의 핵심 차이는 ** 서버가 상태를 저장하지 않는다 **는 점입니다. 토큰 자체에 사용자 정보가 담겨 있기 때문에, 서버는 토큰의 서명만 검증하면 됩니다.
- ** 장점:** 무상태(Stateless)에 가깝게 운영할 수 있어 수평 확장이 쉽고, 모바일·외부 서비스 등 다양한 클라이언트를 지원하기에 유리합니다.
- ** 단점:** 한 번 발급된 토큰을 서버에서 즉시 무효화하기 어렵습니다. 만료 시간, Refresh 토큰, 블랙리스트 같은 추가 설계가 필요합니다.
인가 방식 비교
서블릿 필터 체인 기반 인가
서블릿 필터에서 요청이 컨트롤러에 도달하기 전에 URL·HTTP 메서드 기준으로 권한을 검사하는 방식입니다.
아래는 /admin/** 경로로 들어온 요청을 필터에서 관리자 여부를 확인하는 예시입니다.
public class AdminCheckFilter implements Filter {
@Override
public void doFilter(
ServletRequest req,
ServletResponse res,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) req;
HttpServletResponse httpRes = (HttpServletResponse) res;
String path = httpReq.getRequestURI();
if (path.startsWith("/admin/")) {
boolean isAdmin = checkIsAdmin(httpReq);
if (!isAdmin) {
httpRes.setStatus(HttpServletResponse.SC_FORBIDDEN);
return; // 컨트롤러로 넘기지 않고 바로 403 응답
}
}
chain.doFilter(req, res);
}
}
이 방식의 핵심은 권한이 없는 요청을 ** 서비스 레이어에 도달하기 전에 차단 **한다는 점입니다. 전역 공통 인가 정책을 한 곳에서 관리하기 좋습니다.
스프링 시큐리티 설정 기반 인가
authorizeHttpRequests DSL로 URL 패턴별·HTTP 메서드별 권한을 선언적으로 정의하는 방식입니다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/posts/**").authenticated()
.anyRequest().permitAll()
);
return http.build();
}
위 설정에서 /admin/**으로 들어오는 GET 요청은 내부적으로 ROLE_ADMIN 보유 여부를 검사하고, 조건을 만족하지 못하면 403을 반환합니다.
필터를 직접 구현하는 방식에 비해, 보안 규칙을 ** 한 곳에서 선언적으로** 관리할 수 있어 가독성과 유지보수성이 높습니다.
주의할 점
세션과 JWT를 동시에 사용하면 의도와 다르게 동작한다
SessionCreationPolicy.STATELESS로 설정했는데 formLogin()이 활성화되어 있으면, 로그인 성공 후 세션이 생성됩니다. JWT 기반 인증을 의도했다면 formLogin()을 비활성화하고, 세션 정책이 STATELESS인지 반드시 확인해야 합니다.
JWT를 localStorage에 저장하면 XSS에 취약하다
세션 방식에서 JSESSIONID는 httpOnly 쿠키에 저장되어 JavaScript에서 접근할 수 없습니다. 반면 localStorage에 저장한 JWT는 XSS 공격으로 탈취될 수 있습니다. httpOnly 쿠키에 토큰을 저장하는 것이 더 안전합니다.
정리
| 항목 | 세션 기반 | 토큰 기반 |
|---|---|---|
| 상태 저장 | 서버(세션 저장소) | 클라이언트(토큰) |
| 수평 확장 | 세션 공유 인프라 필요 | 추가 인프라 불필요 |
| 강제 로그아웃 | 세션 삭제로 즉시 가능 | 블랙리스트 등 추가 구현 필요 |
| 보안 위협 | CSRF (쿠키 자동 전송) | XSS (토큰 탈취) |
| 적합한 환경 | 서버 렌더링 MVC | REST API, SPA, 모바일 |