- 왜 JWT를 썼나 (세션 vs JWT)
: JWT를 공부하고 싶은 마음이 있었지만 세션에 비해 JWT에 대한 장점을 말해보자면
세션은 서버에 수가 늘어나면 늘어난 서버에서 세션 저장을 정리해야 하고 꼬일수 있는 단점이 있고
JWT는 서버의 수가 늘어나도 JWT 토큰 한개로 처리 가능하기 때문에 JWT를 사용하는게 좋다고 생각한다.
- SecurityConfig 설정과 이유
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/register").permitAll()
.requestMatchers("/api/auth/register", "/api/auth/login",
"/api/auth/refresh", "/api/auth/logout").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/member/**").hasRole("MEMBER")
.requestMatchers("/admin/**").hasAnyRole("TRAINER", "MASTER")
.requestMatchers("/master/**").hasRole("MASTER")
.requestMatchers("/api/admin/**").hasAnyRole("TRAINER", "MASTER")
.requestMatchers("/api/master/**").hasRole("MASTER")
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(rateLimitFilter, JwtFilter.class); // Rate Limit 필터를 JWT 필터보다 앞에 등록
return http.build();
}
========================================================================================================================
.csrf(csrf -> csrf.disable()) 설명
**CSRF 공격**은 "사용자 모르게 요청을 위조하는 것" 이다.
- 브라우저는 요청을 할때 쿠키를 자동으로 포함해서 요청한다.
이때 해커가 몰래 돈을 자신들의 계좌로 보내도록 요청 위조를 할 수 있는것이다.
요청을 받은 서버는 쿠키가 있으니 정상 요청으로 인식하게 된다.
- 하지만 서버에서 페이지를 내려줄 때 CSRF 토큰을 같이 내려주고
브라우저가 요청을 보낼 때 form 형식이든 헤더든 CSRF 토큰을 보내주면
해커는 CSRF 토큰을 모르기 때문에 위조 요청에 토큰을 넣을 수 없고
서버는 CSRF 토큰 인증 후 해당 요청을 처리하게 된다.
=> 나는 JWT 토큰을 헤더에 Authorization 로 보내기 때문에 불필요로 처리했다.
Thymeleaf 페이지는 모두 GET 전용이고 변경 요청은 전부 JS fetch → /api
========================================================================================================================
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) 설명
이 코드는 세션을 사용하지 않는다는 코드다.
JWT 토큰을 사용하면 세션을 사용하지 않아도 된다.
JWT를 사용하지 않으면 로그인 시 서버가 세션을 생성하고
세션ID를 쿠키에 담아 사용자에게 내려준다.
이후 요청마다 브라우저가 쿠키를 자동으로 포함해서 보내고
서버는 세션ID로 저장된 사용자 정보를 찾아서 인증한다.
========================================================================================================================
.requestMatchers("/member/**").hasRole("MEMBER")
.requestMatchers("/admin/**").hasAnyRole("TRAINER", "MASTER") 설명
이 코드는 requestMatchers("~") 에서 ~의 API로 요청이 올때
hasRole("MEMBER")는 MEMBER 권한을 가진 사용자만 요청을 받아들이고
hasAnyRole("TRAINER", "MASTER")는 TRAINER, MASTER 둘중 하나의 권한을 가진 사용자만 요청을 받아들인다는 뜻이다.
- JwtFilter 흐름
public class JwtFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = null;
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
token = header.substring(7);
} else if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if ("accessToken".equals(cookie.getName())) {
token = cookie.getValue();
break;
}
}
}
if (token != null && jwtProvider.validateToken(token)) {
Claims claims = jwtProvider.parseToken(token);
String username = claims.getSubject();
String role = claims.get("role", String.class);
request.setAttribute("username", username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
username, null,
List.of(new SimpleGrantedAuthority("ROLE_" + role))
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
코드 흐름
1. header에 Authorization를 추출하고 Bearer나 쿠키에서 토큰 추출 한다.
2. jwtProvider.validateToken(token)에서 토큰 확인 후
username과 role을 SecurityContextHolder에 담는다.
SecurityContextHolder란 출입 명부 같은것이다.
컨트롤러에서 지금 누가 요청을 한것인지 판단할 때 SecurityContextHolder의 username과 role로 확인할 수 있다.
3. JWT토큰 안에 password는 넣지 않는다.
그 이유는 JWT토큰이 암호화 된것이 아니라 인코딩 된것이기 때문이다.
토큰만 탈취되면 비밀번호가 노출되는 위험 때문에 식별에 필요한 최소한의 정보만 넣어야 한다.
- JwtProvider 토큰 생성·검증
public class JwtProvider {
private final Key key;
private final long expirationMs;
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
1. validateToken이 parseToken을 호출한다.
2. parseToken 안에서 위조·만료·형식 세 가지를 동시에 검사한다.
3. 통과하면 true, JwtException 발생하면 false를 리턴한다.
4. parseToken 내
.parseClaimsJws(token) 에서 세가지 검사를 한다.
위조 확인, 만료 확인, 형식 확인
- 전체 흐름 한눈에 보기
요청 들어옴
↓
RateLimitFilter (과도한 요청 차단)
↓
JwtFilter (토큰 꺼내서 검증 → SecurityContextHolder에 등록)
↓
SecurityConfig (등록된 인증 정보 보고 경로별 권한 확인)
↓
컨트롤러