
해결 방법
1. Sticky Session: Client 마다 요청 Server 고정
2. 세션 저장소 생성하여 모든 세션을 저장
Session storage가 모든 Client의 정보를 저장하고 있어 모든 서버에서 모든 클라이언트 처리 가능
3. JWT 사용

- 로그인 정보를 Server 에 저장하지 않고, Client 에 로그인 정보를 JWT 로 암호화하여 저장 → JWT 통해 인증/인가
- 이때 모든 서버에서 동일한 Secret Key 소유
> Secret Key : 로그인 정보를 암호화, 받아온 JWT 위조 검증 (복호화)
세션 저장소 사용 시 세션 저장소에 접근하는 부담이 있다.
Secret Key는 외부 서버에 저장되는 것이 아니라 부담이 적다.
JWT의 길이가 길어짐 > HTTP 프로토콜이 무거워짐 > 네트워크 비용 증가
JWT는 클라이언트의 쿠키에 저장됨
서버 입장에서 쿠키를 임의로 만료시킬 수 없음
대신 JWT나 쿠키의 만료 기한을 부여할 수 있음
비밀번호 등의 민감한 데이터를 담지 말아야 한다
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
cookie.setPath("/");
// Response 객체에 Cookie 추가
res.addCookie(cookie);
// 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)) {
// 인증 헤더와 동일한 이름의 쿠키를 찾아
try {
return URLDecoder.decode(cookie.getValue(), "UTF-8");
// Encode 되어 넘어간 Value 다시 Decode
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
}
return null;
}
쿠키에 담긴 정보가 여러 개일 수 있기 때문에 그 중 이름이 JWT가 담긴 쿠키의 이름과 동일한지 확인하여 JWT를 가져온다.
Server
ex) GET /api/products : JWT를 보낸 사용자의 관심상품 목록 조회
JWT 는 누구나 평문으로 복호화 가능합니다.
하지만 Secret Key 가 없으면 JWT 수정 불가능
→ JWT 는 Read only 데이터
아래의 모든 데이터는 JSON 형식으로 저장되어 있다
{
"alg": "HS256", # 알고리즘
"typ": "JWT" # 타입
}
발급일자 등의 다양한 데이터를 넣을 수 있다.
{
"sub": "1234567890",
"username": "카즈하",
"admin": true
}
Secret Key를 넣어서 조작함, 암호화 관련한 정보 양식
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
- JWT 생성
- 생성된 JWT를 Cookie에 저장
- Cookie에 들어있던 JWT 토큰을 Substring
- JWT 검증
- JWT에서 사용자 정보 가져오기
@Component
public class JwtUtil {
// Header KEY 값 - Response 객체 Header에 바로 넣는 방법 / Token에 담아서 넣는 방법이 있음
// Cookie를 만들 때는 Cookie의 Name 값이 된다.
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY (Admin, ... )
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자 / 이후에 오는 것은 Token이라고 알려주는 일종의 규칙
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간
private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey (application.properties에서 가져오는 법)
private String secretKey; // 여기에 위의 secretKey 담아준다
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 로그 설정
public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");
@PostConstruct // 한 번만 받아오면 되는 값을 설정할 때마다 새로 요청하는 실수를 방지하기 위해 사용됨
// 생성자 호출 뒤에 이 코드가 호출됨
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey); // secretKey Decoding
key = Keys.hmacShaKeyFor(bytes);
}
// 1. JWT 토큰 생성
// 아래의 모든 값을 넣을 필요는 없다
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username) // 사용자 식별자값(ID) // PK 등의 다른 값을 넣어도 됨
.claim(AUTHORIZATION_KEY, role) // 사용자 권한
.setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘 (KEY, 알고리즘을 넣어서 암호화 시킴)
.compact();
}
// 2. 생성된 JWT를 Cookie에 저장
public void addJwtToCookie(String token, HttpServletResponse res) {
try {
token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value(인코딩한 토큰)
cookie.setPath("/");
// Response 객체에 Cookie 추가
res.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
logger.error(e.getMessage());
}
}
// 3. Cookie에 들어있던 JWT 토큰을 Substring
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) { // 공백이나 null이 아니고, bearer로 시작하는지
return tokenValue.substring(7); // "bearer " = 7자 / 순수한 token 값만 return
}
logger.error("Not Found Token");
throw new NullPointerException("Not Found Token");
}
// 4. JWT 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); // 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;
}
// 5. JWT에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) { // JWT는 Claim 기반 Web Token
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
}
Bearer : 참고 링크
Logging : Application이 동작을 하는 동안에 프로젝트의 상태 / 동작 정보를 시간 순으로 기록하는 것.
public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");Encode / Decode 확인 : https://jwt.io/
- 양방향 암호 알고리즘
- 암호화: 평문 → (암호화 알고리즘) → 암호문
- 복호화: 암호문 → (암호화 알고리즘) → 평문
- 단방향 암호 알고리즘
- 암호화: 평문 → (암호화 알고리즘) → 암호문
- 복호화: 불가 (
암호문 → (암호화 알고리즘) → 평문)
이때 사용자는 암호화된 패스워드를 알 필요는 없으며, 아래 함수에서 암호화된 비밀번호와 비교해서 일치여부를 판단한다.
// 사용예시
// 비밀번호 확인
if(!passwordEncoder.matches("사용자가 입력한 비밀번호", "저장된 비밀번호")) {
throw new IllegalAccessError("비밀번호가 일치하지 않습니다.");
}
.png?id=953bd8c9-2dab-41eb-b115-f93805ff043b&table=block&spaceId=83c75a39-3aba-4ba4-a792-7aefe4b07895&width=1920&userId=&cache=v2)
Web 애플리케이션에서 관리되는 영역으로 Client로 부터 오는 요청과 응답에 대해 최초/최종 단계의 위치
이를 통해 요청과 응답의 정보를 변경하거나 부가적인 기능을 추가할 수 있습니다.
주로 범용적으로 처리해야 하는 작업들, 예를들어 로깅 및 보안 처리에 활용합니다.
Filter은 한 개만 존재하는 게 아니라 여러 개가 Chain 형식으로 묶여서 처리될 수 있다.
필터 구현 부분 강의 자료 참고하기
http.csrf((csrf) -> csrf.disable());
DispatcherServlet을 통과하게 되고 이후에 각 요청을 담당하는 Controller 로 분배됩니다.Filter 입니다.

상세 처리 과정 보면서 구현하자