1. 인증과 인가
서버 개발에서 가장 기본적인 보안은 인증과 인가이다. 인증(authentication)은 사용자가 누구인지 확인하는 과정이고, 인가(authorization)는 사용자에게 자원에 접근할 수 있는 권한을 부여하는 것이다. 이 둘만 잘 지켜도 기본적인 취약점은 막을 수 있다.
인증과 토큰
로그인은 인증의 한 형태이고 보안을 강화하기 위해 2단계 인증(2FA, Two-Factor Authentication)을 사용하기도 한다. 또 지문 같은 생체 정보를 이용하는 방식도 인증의 한 형태이다.
사용자가 누구인지 확인하는 데 성공하면 서버는 클라이언트에 문자열로 된 토큰을 제공한다. 클라이언트는 해당 토큰을 각 요청마다 보내 자신이 누구닝니 증명한다. 서버는 사용자 인증이 필요한 기능에 대해 매번 아이디와 암호를 입력받지 않고, 토큰을 사용해 사용자를 식별한다.
토큰을 이용해 사용자를 식별하려면 토큰과 사용자간의 매핑 정보를 어딘가에 저장해야 한다. 해당 매핑 정보 저장 위치로는 크게 2가지이다.
서버의 별도 저장소 : 별도 저장소에 토큰과 사용자 식별 정보를 저장한다.
토큰 : 토큰 자체에 사용자 식별자 정보 저장
별도 저장소에 토큰과 사용자 식별자 정보 저장하기
서버는 토큰과 사용자 식별 정보를 DB나 레디스와 같은 별도 저장소에 보관할 수 있다. 로그인에 성공할 경우 서버는 임의의 토큰 문자열을 만든 뒤 외부 저장소에 매핑 정보를 보관한다. 토큰 문자열을 생성할 때는 고유한 값을 생성새 토큰 중복으로 인해 사용자 정보가 잘못 매칭되지 않도록 해야 한다.

외부 저장소에 보관되는 정보는 다음 데이터를 갖는다.
- 토큰
- 사용자 식별자
- 생성 시간
- 최근 사용 시간
- 그 외 유효 시간, 클라이언트 버전 등 추가 데이터
서버는 클라이언트가 전송한 토큰을 이용해서 저장소에 사용자 식별자를 구한다.
토큰 저장 용량에 따른 저장 방식
- 메모리 캐시
- 토큰 데이터는 크기가 크지 않기 때문에 수백만 개의 토큰을 저장해도 용량이 NGB(N 기가 바이트)일 정도로 DB 용량에 큰 부담은 없다. 레디스 같은 메모리 캐시를 사용해도 충분히 저장할 수 있는 크기이다.
- 단점1 : 서버 재시작 시 토큰 데이터 사라짐
- 단점2 : 생성할 수 있는 세션 개수가 메모리 크기에 제한을 받음
- 위 단점을 해소하기 위해 세션 데이터를 외부 저장소에 보관하기도 한다.
- 외부 저장소
- 필요하다면 토큰 데이터를 메모리 캐시가 아닌 DB 같은 저장소에 별도로 저장하면 된다.
- 예시로 스프링 세션은 메모리 대신 DB나 레디스에 세션 데이터를 저장해서 서버 재시작 시에도 세션을 유지할 수 있게 한다. 이 방법은 외부 저장소에 토큰을 저장하면서도 서블릿의 HttpSession을 그대로 사용할 수 있다는 장점이 있다
- 서버 메모리 - 서블릿 세션
- 외부 저장소가 아닌 서버 메모리에 토큰 데이터를 저장할 수도 있는데, 서블릿 세션이 이에 해당한다.
- 톰캣과 같은 서블릿 컨테이너는 메모리에 세션 객체를 저장한다.
- 서블릿 세션은 고유의 세션ID를 생성하는데 이 세션ID가 토큰에 해당한다.
고정 세션
메모리에 토큰 데이터를 저장하는 방식을 사용할 때는 고정 세션(sticky session)이 필요하다. 서버마다 서로 다른 토큰 집합을 저장하고 있기 때문이다. 예시로 클라이언트가 A 서버에 저장된 토큰을 가지고 있는 상태에서 B서버로 요청을 보낸다고 하자. B 서버는 해당 토큰을 갖고 있지 않으므로 클라이언트 요청을 제대로 처리할 수 없다. 따라서 로드 밸런서를 이용해 고정 세션 방식으로 요청 토큰을 처리할 수 있다.

토큰 자체에 사용자 식별 정보 저장하기
토큰 자체에 사용자 식별 정보를 저장할 수도 있는데, 대표적으론 JWT(Json Web Token)를 이요하는 것이다. 사용자가 로그인에 성공하면 사용자 식별값으로 JWT를 생성해서 클라이언트에 토큰으로 응답한다.
- JWT 토큰 생성 및 응답
// 사용자 식별자를 담은 JWT 문자열을 클라이언트에 응답한다.
String token = Jwts.builder()
.subject("userid") // 사용자 식별자
.signWith(key)
.compact();
// 로그인 결과로 토큰 응답
return LoginResponse.of(token);
- 서버에서 JWT 토큰으로 사용자 식별
try {
// 토큰 문자열을 파싱한다
Jws<Claims> jwt = Jwts.parser().verifyWith(key).build().parseSignedClaims(jws);
// 토큰에서 사용자 식별자를 구한다
String userId = jwt.getPayload().getSubject();
} catch (JwtException e) {
// 유효하지 않은 토큰이면 에러 처리
throw new AuthenticationException(e);
}
- 장점
- 토큰이 사용자 식별 정보를 포함하고 있기에 토큰만으로도 사용자 식별 가능
- 서버 구조가 간단하다
- 메모리에 토큰을 저장하지 않기에 수평 확장 가능하다
- 단점
- 서버 - 클라이언트 간 데이터 크기가 증가하므로 네트워크 트래픽이 증가하므로 트래픽 증가에 치명적이다
- 토큰 데이터를 서버에서 제어 불가능하다(서버에서 삭제/변경 불가능)
토큰 송수신
클라이언트는 서버에 토큰을 전송할 때 주로 다음 2가지 방식 중 하나를 사용한다.
- 쿠키 : 쿠키를 사용해서 토큰 전송
- 웹 사이트는 주로 쿠키 방식을 사용한다. 서버 세션도 쿠키를 사용해서 세션ID를 주고 받는다.
- 서버는 사용자가 로그인에 성공하면 토큰 문자열을 값으로 갖는 쿠키를 웹 브라우저에 응답한다. 웹 브라우저(클라이언트)는 서버가 전송한 쿠키를 모든 요청에 함께 전송하므로 토큰을 서버에 전송하기 위해 별도의 자바스크립트 코드를 작성할 필요가 없다.
- 헤더 : 특정 이름을 갖는 헤더를 사용해서 토큰 전송
- 쿠키도 헤더를 통해 전송되지만, 이 경우는 쿠키를 제외한 다른 헤더를 말한다. 많은 앱이 서버와 통신할 때 헤더를 통해 토큰을 전송한다. 헤더 이름은 Token, X-token, Auth, Authorization(OAuth 2.0의 경우) 등 알맞게 정하면 된다. 클라이언트는 토큰을 로컬에 젖아했다가 서버 API 요청을 호출할 때 헤더를 이용해서 토큰을 전송한다.
토큰 보안
보안을 위해 토큰을 사용하는 만큼 토큰 자체의 보안에도 신경을 써야 한다. 서버 보안을 철저히 해도 클라이언트가 보안에 취약하면 토큰이 탈취될 수 있기 때문이다. 토큰을 탈취한 클라이언트는 원래 토큰 소유자처럼 행세할 수 있다.
이처럼 토큰 탈취에 따른 보안 문제를 완화하는 방법은 토큰 유효 시간에 제한을 두는 것이다. 예시로 최초에 토큰을 생성할 때 토큰 유효 시간을 1시간으로 주고 1시간이 지나면 사용자에 대한 접근을 거부하는 식이다.
- 토큰 유효시간 유형
- 토큰 생성 시점을 기준으로 제한 시간 설정하는 방법
- 유효 시간 지정
- 유효 시간 + 클라이언트IP 값으로 값 비교하면 토큰 보안이 향상된다. 토큰 생성할 때 접근한 클라이언트 IP와 실제 토큰을 전송한 클라이언트 IP가 같은지 비교한다. IP가 다를경우 비정상 접근으로 간주하고 요청 처리를 거부하면 된다.
- 마지막 접근 시간을 기준으로 토큰 유효 시간 설정
- 서블릿 세션이 이용하는 방식이다
- 토큰 생성 시점을 기준으로 제한 시간 설정하는 방법
토큰 유효 시간은 어플리케이션 성격에 따라 알맞게 정하면 된다. 일반적인 서비스에서는 토큰 유효 시간이 너무 짧으면 사용하기 불편해 비교적 길게 설정하는 것이 좋고, 관리자 사이트처럼 민감 정보를 조회할 수 있는 서비스는 유효 시간을 길게 잡으면 안된다. 깜박하고 브라우저를 켜 놓고 자리를 비우면 누군가 민감한 고객 정보를 조회할 수 있고, 탈취한 토큰을 이용해 고객 정보를 긴 시간 유출할 수 있기 때문이다.
토큰 무효화
토큰 무효화를 통해 강제 로그아웃 시켜 보안 사고 영향을 줄일 수 있다.
- DB/레디스 같은 외부 저장소에 토큰을 보관한 경우엔 토큰 데이터를 삭제 혹은 유효하지 않은 상태로 변경함으로써 토큰 무효화 가능하다
- 토큰 자체에 데이터를 저장하는 방식은 토큰 데이터가 클라이언트에 저장되므로 토큰 무효화가 쉽지 않아 추가 개발이 필요하다.
토큰 재발급
인증과 인가에서 사용하는 토큰으로 액세스 토큰(access token)과 리프레시 토큰(refresh token)이 있다.
- 액세스 토큰
- 토큰으로 인증된 사용자임을 확인하기 위한 목적으로 사용되는 토큰
- 보통 몇 분에서 몇 시간 내외로 만료 시간을 짧게 지정한다. 만료 시간이 짦으면 사용자의 로그인이 풀려 불편을 줄 수 있기 때문이다.
- 리프레시 토큰
- 액세스 토큰을 발급받아 로그인을 다시 하지 않아도 인증 상태를 유지할 수 있게 해주는 토큰
엑세스 + 리프레시 토큰을 모두 지원하는 시스템은 사용자 로그인 성공 시 만료시간이 짧은 액세스 토큰과 만료 시간이 상대적으로 긴 리프레시 토큰을 함께 발급한다. 이후 토큰이 만료되면 리프레시 토큰을 이용해 새로운 액세스 토큰을 발급해 준다. 이를 통해 사용자는 리프레시 토큰이 만료될 때가지 재로그인 없이 인증 상태를 유지할 수 있다.
인가와 접근 제어 모델
- 인가
- 인증과 토큰은 사용자가 누구인지 그리고 정상적으로 접근하는지 확인하는 역할을 한다면, 인가는 사용자가 요청한 기능을 실행할 권한이 있는지 확인하는 역할을 한다.
- 접근제어
- 접근 제어의 기본은 접근한 사용자를 토큰이나 세션으로 식별하는 것이다.
- API 요청 파라미터로 로그인한 사용자의 식별자를 받으면 안 된다.
- 접근제어 모델 : 사용자가 접근할 수 있는 기능/자원을 관리하기 위한 모델
- 1) 역할별 권한 부여 방식, 2) 사용자별 권한 부여 방식, 3) 속성 기반 접근제어가 있다.
1) 역할 기반 접근 제어(Role-Based Access Control, RBAC)
역할별로 실행 가능한 기능 집합을 할당하고, 사용자에게 역할을 부여한다. 사용자는 역할에 허용된 기능을 실행할 수 있는 권한을 가진다.
장점으로는 권한을 체계적으로 관리할 수 있다.
사용자에게 권한을 일일이 부여할 필요 없이 역할만 부여하면 되므로 권한 관리가 쉬워진다.
역할의 설계와 관리에 신경 써야 한다. 역할을 무분별하게 정의하면 중복된 기능을 가진 유사한 역할이 계속 생기기 쉽다.

2) 사용자별 권한 부여 방식
역할이 아닌 사용자별로 허용 가능 기능을 부여하는 방법도 있다. 이 방법은 시스템 규모가 작거나 역할을 나누기 애매할 때 적합하다. 역할별 권한 부여보다 구현이 단순하기 때문에 개발 시간이나 우선순위 등을 고려해 사용자별 권한 방식을 선택하기도 한다.

3) 속성 기반 접근 제어(Attribute-Based Access Control, ABAC)
사용자의 속성에 기반해 접근을 제어하는 것을 의미한다. 예시로는 사용자 IP 주소에 따라 특정 기능의 접근을 허용/제한할 수 있다.
속성을 활용하면 보다 정교한 접근 제어가 가능하지만, 그만큼 구현이 복잡해지고, 사용할 속성과 규칙을 정의하는 데도 많은 시간이 소요된다.
=> 시스템의 접근 제어 요구 수준과 상황에 따라 역할기반, 속성 기반, 사용자별 방법을 조합해서 사용할 수도 있다. 접근 제어가 정교해질수록 복잡해지므로, 실제로 필요한 수준까지만 접근 제어 모델을 설계하는 것을 권장한다.
'기타 > 책' 카테고리의 다른 글
| [주니어 백엔드 개발자가 반드시 알아야 할 실무지식] 8.3 보안 기타 (0) | 2026.04.25 |
|---|---|
| [주니어 백엔드 개발자가 반드시 알아야 할 실무지식] 8.2 데이터 암호화 (1) | 2026.04.25 |
| [주니어 백엔드 개발자가 반드시 알아야 할 실무지식] 7. IO 병목 (0) | 2026.04.14 |
| [주니어 백엔드 개발자가 반드시 알아야 할 실무지식] 6.2 동시성 (0) | 2026.04.13 |
| [주니어 백엔드 개발자가 반드시 알아야 할 실무지식] 6.1 동시성 (0) | 2026.04.06 |