saulsol/Spring_Code: 스프링 토이 플젝
GitHub - saulsol/Spring_Code: 스프링 토이 플젝
스프링 토이 플젝. Contribute to saulsol/Spring_Code development by creating an account on GitHub.
github.com
오늘은 스프링에서 JWT를 사용했을 때 큰 흐름을 정리해보려고 한다.
JWT(JSON Web Tokens)가 뭐고 왜 쓰는가?
JWT는 쉽게 말해 전자 서명된(Digital Signature) 토큰이다.

전자서명을 이해하기 위해서는 뒤 Signature 부분을 보면 된다.
Signature 부분은 토큰을 발행한 주체(서버) issuer가 발행한 서명.
토큰의 유효성 검사에 사용된다.
즉 JWT에서 전자 서명이란 {헤더}.{페이로드} 와 시크릿 키를 이용해 해시 함수를 돌린, 즉 암호화한 결과 값이다.
- 클라이언트가 로그인 시도
- 유저 정보를 바탕으로 {헤더}.{페이로드} 작성
- 생성된 {헤더}.{페이로드}를 secret 키로 전자 서명 → 결과 X(시그니쳐)
- {헤더}.{페이로드}.X를 Base64 로 인코딩 후 반환
- 클라이언트에게 <토큰> 반환, 유저 인가 활동 요청
- 유저에게 받은 <토큰>을 Base64로 디코딩 → {헤더}.{페이로드}.X
- {헤더}.{페이로드}를 따로 떼어서 secret 키로 전자 서명 → 결과 Y
- 디코딩된 토큰의 마지막 부분 X와 방금 전자 서명한 결과 Y를 비교
- X=Y인 경우 서명이 일치하므로 검증 완료
- 로그인 상태이므로 클라이언트에게 서버 리소스 반환

위와 같은 흐름으로 클라이언트의 인가를 전자 서명을 통해 서버에서 허용한 사용자인지 아닌지를 매 요청마다 확인할 수 있다. → 토큰이 탈취당할 수 있으니까 HTTPS를 사용하는 것이 좋다.
또한 서버의 스케일 아웃에 유리하다. JWT는 사용자의 인증 정보를 갖고 있기에 서버는 이를 검증하기만 하면 된다.
즉 요청이 어떤 서버로 라우팅 되든 동일하게 처리할 수 있다.
이제 스프링 시큐리티와 함께 큰 흐름에 대해 알아보자.
Spring에서의 JWT의 활용 - JWT 생성
@Component
public class JWTUtil {
@Value("${jwt.key}")
private String injectedKey;
public String generateToken(Map<String, Object> valueMap, int min){
SecretKey key = null;
try {
key = Keys.hmacShaKeyFor(injectedKey.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return Jwts.builder()
.setHeader(Map.of("typ", "JWT"))
.setClaims(valueMap)
.setIssuedAt(Date.from(ZonedDateTime.now().toInstant()))
.setExpiration(Date.from(ZonedDateTime.now().plusMinutes(min).toInstant()))
.signWith(key)
.compact();
}
}
전자서명에 사용할 키를 만들기 위해 HMAC-SHA 알고리즘을 사용하였다.
그리고 Jwts.builder().signWith(key) 이 부분에서 사용하였다.
Spring에서의 JWT의 활용 - 어느 시점에 JWT를 줘야 할까, accessToken, refreshToken
당연히 사용자가 로그인(인증)에 성공했을 때 JWT 토큰을 줘야 한다.
근데 accessToken이라는 JWT와 refreshToken이라는 JWT 두 개를 클라이언트에게 줬다.
@Log4j2
@Component
@RequiredArgsConstructor
public class APILoginSuccessHandler implements AuthenticationSuccessHandler {
private final JWTUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
MemberDTO memberDTO = (MemberDTO) authentication.getPrincipal();
Map<String, Object> claims = memberDTO.getClaims();
claims.put("accessToken", jwtUtil.generateToken(claims, 10)); // 10분
claims.put("refreshToken", jwtUtil.generateToken(claims, 60 * 24)); // 하루
Gson gson = new Gson();
String jsonStr = gson.toJson(claims);
response.setContentType("application/json; charset=UTF-8");
PrintWriter printWriter = response.getWriter();
printWriter.println(jsonStr);
printWriter.close();
}
}
왜 두 개를 줬을까?
accessToken은 일반적으로 짧은 유효시간을 지정해서 탈취당하더라도 위험을 줄일 수 있도록 구성한다.
그 때문에 일반적으로 accessToken 이 만료되면 사용자는 refreshToken을 활용해서 새로운 accessToken을 발급받을 수 있는 기능을 같이 사용하는 경우가 많다.
비유하자면 클라이언트에게 accessToken 같은 경우 "입장권" 같은 느낌이고 refreshToken 같은 경우 "연장권" 같은 경우이다.
다음 장에서는 accessToken, refreshToken를 소지한 클라이언트와 서버의 시나리오를 공부하고 그에 맞는 코드를 제작해 보자.
'Spring' 카테고리의 다른 글
| 스프링의 선언적 트랜잭션(@Transactional) - 왜 사용하는가 (5) | 2025.01.08 |
|---|---|
| SpringBoot, JWT - 2 (9) | 2024.12.25 |