📙 목차
1. Token
Token (토큰)
- 사용자의 신원을 증명하거나 권한을 부여하기 위해 발급되는 디지털 증명서 같은 문자열
- 주로 인증(Authentication)과 인가(Authorization) 과정에서 사용된다.
- 토큰은 보통 암호화되어 안전하게 정보를 담고 있으며, 만료시간이 설정되어 있다.
- 대표적인 예, JWT(JSON Web Token)
Token 동작 순서
- 서버가 사용자 로그인 후 고유한 토큰을 생성해 클라이언트에게 전달한다.
- 서버가 아닌 클라이언트에 저장되어 서버의 부담을 덜 수 있다.
- 클라이언트는 이후 요청 시 이 토큰을 함께 보내어 서버가 사용자를 식별하고 권한을 확인할 수 있게 한다.
Token 의 단점
- Cookie/Session 방식보다 Token 자체의 데이터 용량이 많다.
- 요청이 많아지면 그만큼 트래픽이 증가한다.
- Payload(전송되는 데이터)는 암호화되지 않아서 중요한 데이터를 담을 수 없다.
- Token을 탈취당하면 대처하기 어려워 만료 시간(30분)을 설정한다.
2. Access Token, Refresh Token
Access Token과 Refresh Token은 주로 사용자 인증(Authentication) 및 **인가(Authorization)**를 다룰 때 사용하는 토큰 기반 인증 시스템의 핵심 개념이다.
보통 JWT(JSON Web Token) 기반 인증 시스템이나 OAuth2.0에서 사용된다.
Access Token
- 사용자의 인증 정보를 담고 있는 토큰이다.
- 클라이언트가 서버에 요청할 때 이 토큰을 HTTP 헤더에 넣어 전송하여, 해당 요청이 인증된 사용자에 의한 것임을 증명한다.
- 짧은 유효 기간을 가진다 (예: 15분~1시간).
- 보통 다음 정보를 포함한다:
- 사용자 ID
- 만료 시간
- 발급자 정보
- 권한 정보
{
"sub": "user123",
"exp": 1715900000,
"role": "USER"
}
Refresh Token
- Access Token이 만료되었을 때, 새로운 Access Token을 재발급받기 위해 사용하는 토큰이다.
- 일반적으로 Access Token보다 더 긴 유효 기간을 가진다 (예: 7일~30일).
- 보안상 Refresh Token은 서버나 안전한 저장소에만 보관되어야 한다.
- Refresh Token도 보통 JWT나 난수(Random String) 형태이다.
Access Token, Refresh Token 인증
- 사용자가 로그인하면:
- Access Token + Refresh Token 발급
- 사용자가 API 호출 시:
- Access Token을 헤더에 담아서 전송
- Access Token이 만료되면:
- Refresh Token을 이용해 새로운 Access Token 재발급 요청
- Refresh Token도 만료되면:
- 다시 로그인 필요
* 보안 팁
구분 | 저장 위치 | 특징 |
Access Token | 메모리 / 쿠키 | 유효기간 짧음, 요청마다 포함 |
Refresh Token | 서버 DB / Secure HttpOnly Cookie | 길게 유지되며, 보안 저장 필수 |
3. JWT(JSON Web Token)
JWT(JSON Web Token)
- 인증에 필요한 정보들을 암호화시킨 JSON 형태의 Token
- JSON 데이터 포맷을 사용하여 정보를 효율적으로 저장하고 암호화로 서버의 보안성을 높였다.
JWT 구조
Header.Payload.Signature세 부분이 점(.)으로 구분된 문자열 형태
- Header(헤더)
- 토큰의 타입과 해싱 알고리즘을 정의한다.
- Payload(페이로드)
- 실제로 인증과 관련된 데이터(Claims)를 담고 있다.
- Claims의 종류
- Registered Claims : 미리 정의된 Claims
- iss(issuer) : 발행자
- exp(expiration time) : 만료시간
- sub(subject) : 제목
- iat(issued At) : 발행 시간
- jti(JWT ID) : 토큰의 고유 식별자
- Public Claims : 사용자가 정의할 수 있는 클레임, 공개용 정보 전달 목적
- Private Claims : 사용자 지정 클레임, 당사자들 간에 정보를 공유하기 위한 목적
- Registered Claims : 미리 정의된 Claims
- Signature(서명): 헤더와 페이로드를 합친 뒤, 비밀 키를 이용해 서명한 값으로 토큰 위변조를 방지한다.
- Header와 Payload를 서버의 Secret Key로 서명하여 암호화 한다.
- 암호화는 Header에서 정의한 알고리즘(alg)을 활용한다.
- 서명을 통해 서버는 Token이 변조되지 않았음을 확인할 수 있다.
XXXXXX.YYYYYY.ZZZZZZ
(Header).(Payload).(Signature)
JWT 인증 과정
- 클라이언트의 로그인 요청
- 로그인에 성공했다면 Header, Payload에 Secret Key를 사용하여 Signature를 만든다.
- 이후 Base64로 Encoding 한다.
- 일반적으로 Cookie에 담아 클라이언트에게 JWT를 발급한다.
- 발급받은 JWT를 저장 후 서버에 요청할 때 Authorization Header에 JWT를 담아 보낸다.
- 서버에서 JWT의 유효성 검사를 통해 통과한다면 인증에 성공하여 요청을 처리해준다.
- JWT 만료, 위변조 여부를 검사한다.
JWT의 유효성 검사
- A의 JWT를 B가 탈취
- B가 탈취한 JWT를 임의로 수정
- B가 수정한 JWT로 Server에 요청
- 서버는 Signature를 사용하여 유효성 검사(Signature 불일치)
- Header, Payload를 서버의 Secret Key값을 이용해 Signature를 다시 만들어 비교한다.
- 임의로 조작된 데이터를 판별할 수 있다.
JWT 장점
- Signature로 서버의 보안성이 증가한다.
- Token 자체가 필요한 정보(유저 및 검증 정보)들을 모두 가지고 있다.
- 서버는 인증 정보와 관련된 별도의 저장소를 사용하지 않는다.
- 서버의 수평 확장성(Scale Out)이 높아진다.
- Cookie가 없는 다른 환경에서도 인증/인가를 적용할 수 있다.
- DB를 조회하지 않아도 된다.
JWT 단점
- Payload는 암호화 된 것이 아니라 민감한 정보를 다루지 못한다.
- Token의 길이가 길어서 트래픽이 증가하면 네트워크에 부하가 증가한다.
- 클라이언트 측에서 Token을 관리하기 때문에 탈취당하면 대처하기 어렵다.
4. JWT 사용 간단 예제
의존성 추가 (build.gradle)
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
JwtUtil 클래스 만들기
@Component
public class JwtUtil {
private final String secret = "my-secret-key-1234567890";
public String createToken(String username) {
return Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 15)) // 15분
.signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(secret.getBytes())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validate(String token) {
try {
extractUsername(token);
return true;
} catch (Exception e) {
return false;
}
}
}
Controller
@RestController
public class AuthController {
private final JwtUtil jwtUtil;
public AuthController(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password) {
if ("user".equals(username) && "pass".equals(password)) {
return jwtUtil.createToken(username);
}
throw new RuntimeException("Invalid login");
}
@GetMapping("/secure")
public String secure(@RequestHeader("Authorization") String authHeader) {
String token = authHeader.replace("Bearer ", "");
if (jwtUtil.validate(token)) {
String username = jwtUtil.extractUsername(token);
return "Hello, " + username;
}
return "Invalid token";
}
}
'Spring > 강의' 카테고리의 다른 글
[📙 숙련 Spring] 3-1. JPA (0) | 2025.05.20 |
---|---|
[📙 숙련 Spring] 2-4. Filter (0) | 2025.05.20 |
[📙 숙련 Spring] 2-2. Cookie, Session (2) | 2025.05.16 |
[📙 숙련 Spring] 2-1. 인증과 인가 (0) | 2025.05.16 |
[📙 숙련 Spring] 1-3. Validation과 Bean Validation (4) | 2025.05.15 |