์ด๋ฒ ์ฅ์์๋ Spring Security์ Stateful ์ธ์ฆ(JSESSIONID)์ ๋ฌด์ํ(Stateless) JWT ๊ตฌ์กฐ๋ก ๋์ฒดํ๋ค.
๊ธฐ์กด JSESSIONID + XSRF-TOKEN ์ฟ ํค ๊ธฐ๋ฐ ์ธ์ฆ์
๋ฐ๋ฉด, JWT(JSON Web Token)์ ์ฌ์ฉํ๋ฉด
โ์ฌ์ฉ์ ์ํ๋ฅผ ํ ํฐ ๋ด๋ถ์ ์ง์ ์ ์ฅํ์ฌ, ์๋ฒ๊ฐ ์ํ๋ฅผ ๊ธฐ์ตํ์ง ์์๋ ๋๋ค.โ
์ฆ, ๋ฐฑ์๋ ์๋ฒ๋ ์์ ํ Statelessํ๊ฒ ๋์ํ๋ฉฐ,
๋ชจ๋ ์์ฒญ์ Authorization ํค๋์ JWT๋ฅผ ํฌํจํด ์ธ์ฆ๋๋ค.
| ๊ตฌ๋ถ | Opaque Token | JWT Token |
|---|---|---|
| ํํ | ๋ฌด์๋ฏธํ ๋๋ค ๋ฌธ์์ด | JSON ๊ตฌ์กฐ(Base64 ์ธ์ฝ๋ฉ) |
| ์ ๋ณด ์ ์ฅ | ์์ (์๋ฒ ์ธ์ ์ ์ ์ฅ) | ํ ํฐ ๋ด๋ถ์ ์ ์ฅ(Self-contained) |
| ๊ฒ์ฆ ๋ฐฉ์ | ์๋ฒ์ ๋งค๋ฒ ๊ฒ์ฆ ์์ฒญ ํ์ | ๊ณต๊ฐ ํค๋ก ๋ก์ปฌ ๊ฒ์ฆ ๊ฐ๋ฅ |
| ์์กด์ฑ | Auth ์๋ฒ ํ์ | ๋ ๋ฆฝ์ ๊ฒ์ฆ ๊ฐ๋ฅ |
| ์ด์์ ํ๊ฒฝ | ํ์๋ง / ๋ด๋ถ API | ๋ง์ดํฌ๋ก์๋น์ค / ๋ถ์ฐ ํ๊ฒฝ |
| ์๋ | ๋๋ฆผ (์๋ฒ ํต์ ํ์) | ๋น ๋ฆ (๋ก์ปฌ ๊ฒ์ฆ ๊ฐ๋ฅ) |

๐ JWT์ ํต์ฌ ์ด์ :
Auth ์๋ฒ์ ๋งค๋ฒ ์์กดํ์ง ์๊ณ , ๊ณต๊ฐํค ๊ฒ์ฆ๋ง์ผ๋ก ํ ํฐ์ ์ ํจ์ฑ์ ํ๋จํ ์ ์๋ค.
โ ์๊ฒฉ ์ฆ๋ช ๋ ธ์ถ ์ต์ํ
๋ก๊ทธ์ธ ์ 1ํ๋ง ์์ด๋/๋น๋ฐ๋ฒํธ ์ ์ก โ ์ดํ ์์ฒญ์ JWT๋ง ์ ์ก
โ ๋คํธ์ํฌ ๋ด ์๊ฒฉ ์ฆ๋ช ๋ ธ์ถ ์ต์ํ
๐ ๋ง๋ฃ ์๊ฐ(Expiration)
ํ ํฐ ์์ฑ ์ ๋ง๋ฃ ์๊ฐ(exp) ์ง์
โ ํ ํฐ ํ์ทจ ์์๋ ์ผ์ ์๊ฐ์ด ์ง๋๋ฉด ์๋ ๋ฌดํจํ
๐ฆ Self-contained ๊ตฌ์กฐ
์ฌ์ฉ์๋ช , ๊ถํ, ๋ฐ๊ธ์ผ์ ๋ฑ์ ํ ํฐ ๋ด๋ถ์ ์ ์ฅ
โ ์๋ฒ/DB์ ๋ณ๋ ์ธ์ ๊ด๋ฆฌ ํ์ ์์
๐ ์ฌ์ฌ์ฉ์ฑ (SSO)
ํ ๋ฒ ๋ก๊ทธ์ธ ์ ๋ฐ๊ธ๋ ํ ํฐ์ ์ฌ๋ฌ ์๋น์ค์์ ์ฌ์ฉ ๊ฐ๋ฅ
โ Single Sign-On(SSO) ๊ตฌํ์ ์ ํฉ
๐ฑ Cross-platform ํธํ์ฑ
์น, ๋ชจ๋ฐ์ผ, IoT ๋ฑ ์ด๋์๋ ๋์ผํ ๋ฐฉ์์ผ๋ก ์ฌ์ฉ ๊ฐ๋ฅ
๐งฑ Stateless (๋ฌด์ํ)
์๋ฒ ๊ฐ ์ธ์ ๋๊ธฐํ ๋ถํ์ โ ์ํ ํ์ฅ(MSA) ํ๊ฒฝ์ ์ต์ ํ
JWT๋ Header.Payload.Signature์ ์ธ ๋ถ๋ถ์ผ๋ก ๊ตฌ์ฑ๋๋ค.
xxxxx.yyyyy.zzzzz
| ๊ตฌ์ฑ์์ | ์ญํ | ์์ |
|---|---|---|
| Header | ๋ฉํ๋ฐ์ดํฐ(์๊ณ ๋ฆฌ์ฆ, ํ์ ) | {"alg": "HS256", "typ": "JWT"} |
| Payload | ์ฌ์ฉ์ ์ ๋ณด(Claim) | {"sub":"user", "role":"admin", "exp":1718794117} |
| Signature | ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ์ฉ ๋์งํธ ์๋ช | HMACSHA256(Base64(header+payload), secret) |

โ๏ธ ๋์งํธ ์๋ช (Signature)
์๋ช ์ ํ ํฐ ๋ณ์กฐ๋ฅผ ๊ฐ์งํ๋ ํต์ฌ ์์.
๋น๋ฐํค(
secret)๋ก ์์ฑ๋๋ฉฐ, ํ ํฐ ์กฐ์ ์ ์๋ช ์ด ๋ถ์ผ์นํ๊ฒ ๋๋ค.

http
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.securityContext(AbstractHttpConfigurer::disable) // JSESSIONID ๋นํ์ฑํ
.cors(cors -> cors.configurationSource(...))
.csrf(csrf -> csrf.disable());
setExposedHeaders(List.of("Authorization"))๐ JWTTokenGeneratorFilter.java
@Component
public class JWTTokenGeneratorFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
String secret = env.getProperty("JWT_SECRET", "default_secret");
SecretKey secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
String jwt = Jwts.builder()
.issuer("EazyBank")
.subject("JWT Token")
.claim("username", auth.getName())
.claim("authorities", auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(",")))
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 8 * 60 * 60 * 1000))
.signWith(secretKey)
.compact();
res.setHeader("Authorization", jwt);
}
chain.doFilter(req, res);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return !request.getServletPath().equals("/user");
}
}
๋ก๊ทธ์ธ ์์ฒญ ์(POST /user)๋ง JWT๋ฅผ ์์ฑํ๊ณ
์ดํ ์์ฒญ์์๋
JWTTokenValidatorFilter๊ฐ ๊ฒ์ฆ์ ๋ด๋นํ๋ค.
๐ JWTTokenValidatorFilter.java
@Component
public class JWTTokenValidatorFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
String jwt = req.getHeader("Authorization");
if (jwt != null) {
try {
String secret = env.getProperty("JWT_SECRET", "default_secret");
SecretKey secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(jwt)
.getPayload();
String username = claims.get("username", String.class);
String authorities = claims.get("authorities", String.class);
Authentication auth = new UsernamePasswordAuthenticationToken(
username, null,
AuthorityUtils.commaSeparatedStringToAuthorityList(authorities));
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (Exception e) {
throw new BadCredentialsException("Invalid Token received");
}
}
chain.doFilter(req, res);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return request.getServletPath().equals("/user");
}
}
๐ ProjectSecurityConfig.java
http
.addFilterAfter(new JWTTokenGeneratorFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(new JWTTokenValidatorFilter(), BasicAuthenticationFilter.class);
.expiration(new Date(System.currentTimeMillis() + 3000)); // 3์ด
3์ด ํ ์์ฒญ ์ ExpiredJwtException ๋ฐ์ โ BadCredentialsException("Invalid Token") ๋ฐํ
๐ก ๊ฐ์ ํฌ์ธํธ:
๋ง๋ฃ ์์ธ์ ๋ณ์กฐ ์์ธ๋ฅผ ๋ณ๋ ์ฒ๋ฆฌํ์ฌ
401 Unauthorized: ๋ง๋ฃ403 Forbidden: ๋ณ์กฐ ๋ก ๊ตฌ๋ถํ๋ฉด ์ค๋ฌด์์ ๋๋ฒ๊น ์ด ์ฌ์์ง๋ค.

๐ UserController.java
@PostMapping("/apiLogin")
public ResponseEntity<LoginResponseDTO> apiLogin(@RequestBody LoginRequestDTO request) {
Authentication authReq = UsernamePasswordAuthenticationToken.unauthenticated(
request.username(), request.password());
Authentication authRes = authenticationManager.authenticate(authReq);
if (authRes != null && authRes.isAuthenticated()) {
String jwt = generateJwt(authRes); // ๋์ผํ Generator ๋ก์ง ์ฌ์ฌ์ฉ
return ResponseEntity.ok()
.header("Authorization", jwt)
.body(new LoginResponseDTO("OK", jwt));
}
throw new BadCredentialsException("Login Failed");
}
| ์์น | ์ญํ | ์ฃผ์ ์ฝ๋ |
|---|---|---|
login.component.ts | ๋ก๊ทธ์ธ ์ฑ๊ณต ์ ํ ํฐ ์ ์ฅ | sessionStorage.setItem('Authorization', token) |
app.request.interceptor.ts | ๋ชจ๋ ์์ฒญ์ JWT ์๋ ์ฒจ๋ถ | req = req.clone({ headers: req.headers.set('Authorization', token) }) |
logout.component.ts | ์ธ์ ์ข ๋ฃ ์ JWT ์ญ์ | sessionStorage.removeItem('Authorization') |
| ์ํฉ | ์์ธ | HTTP ์๋ต | ์ค๋ช |
|---|---|---|---|
| ๋ง๋ฃ(Exp) | ExpiredJwtException | 401 | ํ ํฐ ์ ํจ์๊ฐ ์ด๊ณผ |
| ๋ณ์กฐ(Signature) | SignatureException | 403 | ๋์งํธ ์๋ช ๋ถ์ผ์น |
| ์๋ชป๋ ํ์ | MalformedJwtException | 400 | ํ ํฐ ํ์ฑ ๋ถ๊ฐ |
| ํค ๋ถ์ผ์น | InvalidKeyException | 500 | secret key ์์ |
JWT ๊ธฐ๋ฐ ์ธ์ฆ์
๋ค๋ง,
Secret ๊ด๋ฆฌ, ๋ง๋ฃ ์ฒ๋ฆฌ, Refresh Token ์ ๋ต์ด ๋ฐ๋์ ๋ณํ๋์ด์ผ ํ๋ค.