블로그 이름 뭐하지
[Spring] 쿠키와 세션, JWT 본문
쿠키와 세션, JWT는 사용자의 인증, 인가를 위해 필요한 개념이다.
인증과 인가
- 인증(Authentication)
해당 유저가 실제 유저인지 인증
(ex. 로그인, 지문인식 등) - 인가(Authorization)
해당 유저가 특정 리소스에 접근이 가능한지 권한을 확인
(ex. 관리자 권한, 회원/비회원 다른 권한 등)
웹 어플리케이션 인증의 특수성

일반적으로 서버-클라이언트 구조로 되어있고, Http 프로토콜을 이용해 통신한다.
해당 통신은 비연결성 무상태로 이루어져있다.
- 비연결성(Connetcionless)
서버와 클라이언트가 연결되어 있지 않다.
리소스 절약을 위해 채팅이나 게임이 아닌 이상, 실제로 둘은 연결되어 있지 않다.
서버는 하나의 요청에 하나의 응답을 내고 연결을 끊는다. - 무상태(Stateless)
서버가 클라이언트의 상태를 저장하지 않는다.
기존의 상태를 저장하는 것도 서버의 비용을 증가시키므로, 기존의 상태가 없다고 가정하는 프로토콜로 구현되어있다.
서버는 클라이언트가 직전에 어떤 요청을 보냈는지 알지 못한다.
통신이 연결되어 있지 않는 상태에서 우리는 어떻게 인증과 인가 정보를 처리하고, 정보를 불러와야 할까.
그에 대한 해결 방법을 아래에 정리한다.
인증 방식
1) 쿠키-세션 방식의 인증
인증과 관련된 약간의 정보를 서버가 저장하여 로그인을 유지시키는 개념이다.

1) 사용자가 로그인 요청을 보내면 서버가 DB에서 정보가 일치하는지 확인한다.
2) 서버의 정보와 요청이 일치하면 세션 저장소에서 유저의 정보와 상관없는 난수 Session Id를 발급한다.
3) 서버는 로그인 요청의 응답으로 Session Id를 반환한다
4) 클라이언트는 Session Id를 쿠키라는 저장소에 보관하고, 요청마다 Http Header에 쿠키를 담아 보낸다
5) 클라이언트의 요청에 쿠키가 있다면 서버는 세션저장소에서 쿠키를 검증한다.
6) 검증이 완료되면 유저에 대한 정보 등 응답을 내어준다.
2) JWT 기반 인증
JWT(Json Web Token)란 인증에 필요한 정보들을 암호화 시킨 것을 의미한다.
쿠키 세션 방식과 유사하게 JWT 토큰을 Http Header에 실어 서버가 클라이언트를 식별한다.

1) 사용자가 로그인 요청을 보내면 서버가 DB에서 정보가 일치하는지 확인한다.
2) 서버의 정보와 요청이 일치하면 유저의 정보를 JWT로 암호화하여 내보낸다
3) 서버는 로그인 요청의 응답으로 JWT를 반환한다
4) 클라이언트는 JWT를 저장소에 보관하고, 요청마다 Http Header에 담아 보낸다
5) 클라이언트의 요청에 토큰이 있다면 서버는 토큰을 검증한다.
6) 검증이 완료되면 유저에 대한 정보 등 응답을 내어준다.
쿠키와 세션
쿠키와 세션 모두 Http에 상태 정보를 유지(Stateful)하기 위해 사용된다
쿠키
클라이언트에 저장될 목적으로 생성한 정보를 담은 파일
브라우저 > F12(개발자도구) > Application > Storage > Cookies 에 도메인 별로 저장되어 있다.

package com.sparta.springauth.auth;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@RestController
@RequestMapping("/api")
public class AuthController {
public static final String AUTHORIZATION_HEADER = "Authorization";
@GetMapping("/create-cookie")
public String createCookie(HttpServletResponse res) {
addCookie("Robbie Auth", res);
return "createCookie";
}
//★쿠키 읽기
@GetMapping("/get-cookie")
// @CookieValue(): Cookie의 Name정보를 전달하여 value를 가져온다
public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) {
System.out.println("value = " + value);
return "getCookie : " + value;
}
//★쿠키 생성
public static void addCookie(String cookieValue, HttpServletResponse res) {
try {
cookieValue = URLEncoder.encode(cookieValue, "utf-8").replaceAll("\\+", "%20");
// Cookie Value 에는 공백이 불가능해서 encoding 진행
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, cookieValue);
// Name-Value : 쿠키를 구별하는 데 사용되는 키 - 쿠키의 값
cookie.setPath("/"); // 쿠키가 사용되는 경로 저장
cookie.setMaxAge(30 * 60); // 쿠키 만료기간 저장
// Response 객체에 Cookie 추가하여 브라우저로 반환
// 반환된 쿠키는 브라우저의 쿠키 저장소로 저장
res.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e.getMessage());
}
}
}
세션
서버에서 클라이언트 별로 Session ID를 부여하고 필요한 정보를 클라이언트 별로 서버에 저장한다.
서버에서 생성한 Session ID는 클라이언트의 쿠키 값으로 저장되어 클라이언트의 식별에 사용된다.
//★세션 생성
@GetMapping("/create-session")
public String createSession(HttpServletRequest req) {
// Servlet에서는 Session ID를 간편하게 만들 수 있는 HttpSession을 제공한다.
// 세션이 존재할 경우 세션 반환, 없을 경우 새로운 세션을 생성한 후 반환
HttpSession session = req.getSession(true);
// 세션에 저장될 정보 Name - Value 를 추가합니다.
// 반환된 세션은 Cookie 저장소에 'JSESSIONID'라는 Name으로 Value에 저장된다
session.setAttribute(AUTHORIZATION_HEADER, "Robbie Auth");
return "createSession";
}
//★세션 읽기
@GetMapping("/get-session")
public String getSession(HttpServletRequest req) {
// 세션이 존재할 경우 세션 반환, 없을 경우 null 반환
HttpSession session = req.getSession(false);
String value = (String) session.getAttribute(AUTHORIZATION_HEADER);
// 가져온 세션에 저장된 Value 를 Name 을 사용하여 가져옵니다.
System.out.println("value = " + value);
return "getSession : " + value;
}
쿠키와 세션의 비교
쿠키(Cookie) | 세션(Session) | |
저장 위치 | 클라이언트 (웹 브라우저) | 웹 서버 |
사용 예 | 사이트 팝업의 '오늘 다시보지 않기' 정보 저장 | 로그인 정보 저장 |
만료 시점 | 쿠키 저장 시에 만료일시 설정 가능 (브라우저 종료 시에도 유지 가능) |
- 브라우저 종료 시 - 클라이언트 로그아웃 시 - 서버에서 설정한 유지기간까지 |
용량 제한 | 브라우저 별로 다름(크롬 기준) -하나의 도메인 당 180개 -하나의 쿠키당 4KB |
개수 제한은 없으나, 세션 저장소 크기 이상 저장 불가 |
보안 | 취약 (클라이언트에서 쿠키 정보를 쉽게 변경, 삭제, 가로채기 당할 수 있다) |
비교적 안전 (서버에 저장되므로 상대적으로 안전하다) |
JWT
JSON 포맷을 사용해 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token
일반적으로 쿠키를 통해 JWT를 저장한다.
JWT 사용 이유

서버가 1대 인 경우 서버 하나가 모든 Client의 정보를 소유한다


서버의 대용량 트래픽 처리를 위해 2대 이상의 서버를 운영 시, 서버마다 각각 다른 Client의 정보를 가지게 된다.
이 경우 자신의 정보가 없는 서버에 API 요청을 하게 될 우려가 있으므로
Sticky Session을 이용해 Client마다 요청할 서버를 고정하거나,
세션 저장소를 이용해 모든 세션을 한번에 저장하여, 모든 서버에서 모든 Client의 요청을 처리할 수 있게 한다.

JWT를 사용하면 Client의 정보를 서버가 아닌 클라이언트에 저장하게 되며,
모든 서버에서 동일한 Secret Key를 사용하여 해당 정보를 암호화하고, 검증하는 것이 가능하다.
JWT 장점 | JWT 단점 |
- 동시 접속자가 많을 때, 서버 측 부하를 낮춘다 - Client, Server가 다른 도메인을 사용(OAuth 로그인 등)할 때 유용하다 |
- 구현의 복잡도가 증가한다 - JWT에 담는 내용이 많을 수록 네트워크 비용이 증가한다 - 기생성된 JWT를 일부만 만료시킬 수 없다 - Secret Key가 유출되면 JWT를 조작할 수 있다. |
JWT 사용 흐름
▶ Client가 로그인 성공 시
1) 서버에서 Client의 정보를 JWT로 암호화한다
2) 서버에서 쿠키를 생성해 JWT를 담아 Client 응답에 전달한다
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
cookie.setPath("/");
// Response 객체에 Cookie 추가
res.addCookie(cookie);
▶ Client가 JWT를 통해 인증 시
1) 서버에서 API 요청 때마다 쿠키에 포함된 JWT를 찾아 사용한다
2) Client가 전달한 JWT를 위조검증하고, 유효기간이 지나지 않았는지 확인한다
3) 검증이 성공하면 JWT에서 사용자 정보를 가져와 확인한다
// HttpServletRequest 에서 Cookie Value : JWT 가져오기
public String getTokenFromRequest(HttpServletRequest req) {
Cookie[] cookies = req.getCookies();
if(cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
// 쿠키에서 JWT 토큰 꺼내기
// 쿠키에 담긴 정보가 여러개일 수 있으니 그중 JWT가 담긴 쿠키의 이름과 동일한지 확인
try {
return URLDecoder.decode(cookie.getValue(), "UTF-8");
// Encode 되어 넘어간 Value 다시 Decode
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
}
return null;
}
JWT 구조
JWT는 누구나 평문으로 복호화가 가능하나 Secret Key가 없으면 수정이 불가하다(Read Only)
보안에 취약하므로, HTTPS로 토큰이 노출되지 않도록 하고,
인증정보만 담아 만료시간을 짧게 하여 보안을 유지한다
▼ JWT에 담긴 내용을 해석할 수 있는 링크
JWT.IO
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
jwt.io

JWT 다루기
1) 설정
//build.gradle 에 dependency 추가
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
//application.properties에 secret key 추가
jwt.secret.key={대충 자기가 들고 있는 secret key}
2) JwtUtil 만들기
Util 클래스는 특정 매개변수(파라미터)에 대한 작업을 수행하는 메서드들이 존재하는 클래스로,
다른 객체에 의존하지 않고 하나의 모듈로서 동작하는 클래스이다.
JwtUtil은 JWT 관련 기능의 작업을 수행하는 클래스라고 할 수 있다.
JWT 관련 기능
- JWT 생성
- 생성된 JWT를 Cookie에 저장
- Cookie에 들어있던 JWT 토큰을 SubString
- JWT 검증
- JWT에서 사용자 정보 가져오기
package com.sparta.springauth.jwt;
import com.sparta.springauth.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
@Component
public class JwtUtil {
// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자 >> Bearer >> JWT나 OAuth에 대한 토큰을 사용한다는 표시
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간
private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
// Base64로 Encode 한 SecretKey를 Properties에 작성해두고 @Value를 통해 가져온다
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
//암호화 알고리즘 : HS256
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 로그 설정
public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
//★토큰 생성
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username) // 사용자 식별자값(ID)
.claim(AUTHORIZATION_KEY, role) // 사용자 권한(key 값을 통해 value 확인)
.setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간(ms 기준)
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘으로 암호화
.compact();
}
//★JWT Cookie 에 저장
public void addJwtToCookie(String token, HttpServletResponse res) {
try {
// Cookie Value 에는 공백이 불가능해서 encoding 진행
token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20");
//쿠키에 JWT 토큰 추가
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
cookie.setPath("/");
// Response 객체에 Cookie 추가
res.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
logger.error(e.getMessage());
}
}
//★JWT 토큰 substring
public String substringToken(String tokenValue) {
//StringUtil.hasText로 공백, null을 확인하고 startsWith로 토큰 시작값이 Bearer인지 확인
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
//맞다면 순수한 JWT를 반환하기 위해 Bearer(공백포함 7자)을 제거
return tokenValue.substring(7);
}
logger.error("Not Found Token");
throw new NullPointerException("Not Found Token");
}
//★토큰 검증
public boolean validateToken(String token) {
try {
// Jwts.parserBuilder() 를 이용해 JWT를 파싱
// JWT가 위변조되지 않았는지 SecretKey를 통해 확인
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException | SignatureException e) {
logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
logger.error("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
//★토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
// JWT 구조 중 Payload 부분에 토큰의 정보가 있다
// 이 정보의 한 조각을 claim이라고 부르며 key-value의 한쌍으로 이루어져있다
// 토큰에는 여러 개의 클레임을 넣을 수 있다
// Jwts.parserBuilder()와 secretkey로 claim을 가져와 담겨있는 사용자의 정보를 사용한다
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
}
//JWT 테스트
@GetMapping("/create-jwt")
public String createJwt(HttpServletResponse res) {
// Jwt 생성
String token = jwtUtil.createToken("Robbie", UserRoleEnum.USER);
// Jwt 쿠키 저장
jwtUtil.addJwtToCookie(token, res);
return "createJwt : " + token;
}
@GetMapping("/get-jwt")
public String getJwt(@CookieValue(JwtUtil.AUTHORIZATION_HEADER) String tokenValue) {
// JWT 토큰 substring
String token = jwtUtil.substringToken(tokenValue);
// 토큰 검증
if(!jwtUtil.validateToken(token)){
throw new IllegalArgumentException("Token Error");
}
// 토큰에서 사용자 정보 가져오기
Claims info = jwtUtil.getUserInfoFromToken(token);
// 사용자 username
String username = info.getSubject();
System.out.println("username = " + username);
// 사용자 권한
String authority = (String) info.get(JwtUtil.AUTHORIZATION_KEY);
System.out.println("authority = " + authority);
return "getJwt : " + username + ", " + authority;
}
'Spring' 카테고리의 다른 글
[Spring] Spring Security (0) | 2024.11.14 |
---|---|
[Spring] 필터(Filter) (0) | 2024.11.14 |
[Spring] Bean (0) | 2024.11.13 |
[Spring] JPA (0) | 2024.11.13 |
[Spring] IOC(제어의 역전)와 DI(의존성 주입) (0) | 2024.10.10 |