2025-05-19
๊ตฌ๋ถ | ์ธ์ ๋ฐฉ์ | JWT ๋ฐฉ์ |
---|---|---|
์ํ ์ ์ฅ | ์๋ฒ ์ธ์ (Stateful) | ๋ฌด์ํ (Stateless) |
์ธ์ฆ ์ ์ง | ์ธ์ ID | Access Token |
์ ์ฅ ์์น | ์๋ฒ (๋ฉ๋ชจ๋ฆฌ, DB) | ํด๋ผ์ด์ธํธ (์ฟ ํค, ํค๋ ๋ฑ) |
ํ์ฅ์ฑ | ๋ฎ์ (์๋ฒ ๋ถํโ) | ๋์ (๋ง์ดํฌ๋ก์๋น์ค ์ ํฉ) |
โ ์ฐ๋ฆฌ๋ JWT ๊ธฐ๋ฐ ์ธ์ฆ ๋ฐฉ์์ ์ ํํ๊ณ , Access Token์ ์ฟ ํค์ ์ ์ฅํ์ฌ ์ธ์ฆ ํ๋ฆ์ ๊ตฌํ
Header.Payload.Signature
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
[๋น๋ฐํค ์์ฑ ์ ํธ ํด๋์ค]
JWT ์๋ช ์ ์ํ HMAC SHA-256 ํค๋ฅผ ๋ฌด์์๋ก ์์ฑ
public class KeyGenerator {
public static byte[] getKeygen() {
SecureRandom secureRandom = new SecureRandom();
byte[] keyBytes = new byte[256 / 8]; // 32byte = 256bit
secureRandom.nextBytes(keyBytes);
return keyBytes;
}
}
[JWT ์ค์ ๊ฐ ์์ ์ ์ ํด๋์ค]
public class JwtProperties {
public static final int ACCESS_TOKEN_EXPIRATION_TIME = 1000 * 60; // 1๋ถ
public static final int REFRESH_TOKEN_EXPIRATION_TIME = 1000 * 60 * 5; // 5๋ถ
public static final String ACCESS_TOKEN_COOKIE_NAME = "access-token";
public static final String REFRESH_TOKEN_COOKIE_NAME = "refresh-token";
}
์ฃผ๋ก ํ ํฐ ์ ํจ ์๊ฐ๊ณผ ์ฟ ํค ์ด๋ฆ ๋ฑ ๊ณตํต ์ค์ ๊ฐ์ ์์๋ก ์ ์
[ํ ํฐ ๋ฐํ์ฉ DTO]
@Builder
@Data
@AllArgsConstructor
public class TokenInfo {
private String grantType; // ์ผ๋ฐ์ ์ผ๋ก "Bearer"
private String accessToken; // JWT ์ก์ธ์ค ํ ํฐ
private String refreshToken; // JWT ๋ฆฌํ๋ ์ ํ ํฐ
}
๋ก๊ทธ์ธ ์ฑ๊ณต ์ ํด๋ผ์ด์ธํธ์ ๋ฐํํ Access/Refresh ํ ํฐ์ ๋ด๋ DTO
[ํต์ฌ ์ ํธ ํด๋์ค] โ ํ ํฐ ์์ฑ, ๊ฒ์ฆ, ์ธ์ฆ ๊ฐ์ฒด ๋ณํ
@Slf4j
@Component
public class JwtTokenProvider {
private final Key key;
public JwtTokenProvider() {
byte[] keyBytes = KeyGenerator.getKeygen();
this.key = Keys.hmacShaKeyFor(keyBytes); // HMAC-SHA256์ฉ ํค
}
// โ
ํ ํฐ ์์ฑ
public TokenInfo generateToken(Authentication authentication) {
...
}
// โ
ํ ํฐ ๋ณตํธํ โ Authentication ๋ฐํ
public Authentication getAuthentication(String token) {
...
}
// โ
ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฆ
public boolean validateToken(String token) {
...
}
}
์ธ์ฆ ๊ฐ์ฒด๋ก๋ถํฐ JWT ์์ฑํ๊ณ ,
JWT์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ถ์ถํด Spring Security์ Authentication ๊ฐ์ฒด๋ก ๋ณํํ๋ ํต์ฌ ํด๋์ค
[์์ฒญ ํํฐ] โ ์์ฒญ๋ง๋ค ์ฟ ํค์์ JWT ๊บผ๋ด ์ธ์ฆ ์ฒ๋ฆฌ
public class JwtAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String token = ... // ์ฟ ํค์์ access-token ์ถ์ถ
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
ํด๋ผ์ด์ธํธ ์์ฒญ๋ง๋ค ํ ํฐ์ ์ฝ๊ณ , ์ธ์ฆ ์ ๋ณด๋ฅผ SecurityContext์ ์ ์ฅ
์ ์ ๊ฐ ๋ก๊ทธ์ธ๋ ์ํ์์ Spring Security์๊ฒ ์๋ฆผ
[๋ก๊ทธ์ธ ์ฑ๊ณต ์ ์ฒ๋ฆฌ] โ Access Token ์์ฑ + ์ฟ ํค ์ ์ฅ
@Component
public class CustomSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication);
Cookie cookie = new Cookie(JwtProperties.ACCESS_TOKEN_COOKIE_NAME, tokenInfo.getAccessToken());
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(JwtProperties.ACCESS_TOKEN_EXPIRATION_TIME / 1000); // ์ด ๋จ์
response.addCookie(cookie);
response.sendRedirect(request.getContextPath() + "/");
}
}
๋ก๊ทธ์ธ์ ์ฑ๊ณตํ๋ฉด JWT ํ ํฐ์ ๋ฐ๊ธ โ ์ฟ ํค์ ์ ์ฅ โ ๋ฉ์ธ ํ์ด์ง๋ก ์ด๋
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
Cookie cookie = new Cookie(JwtProperties.ACCESS_TOKEN_COOKIE_NAME, null);
cookie.setMaxAge(0); // ์ฟ ํค ์ฆ์ ๋ง๋ฃ
cookie.setPath("/");
response.addCookie(cookie);
response.setStatus(HttpServletResponse.SC_OK);
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired private CustomSuccessHandler customSuccessHandler;
@Autowired private JwtTokenProvider jwtTokenProvider;
@Autowired private UserRepository userRepository;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.formLogin(form -> form
.loginPage("/login")
.successHandler(customSuccessHandler)
)
.logout(logout -> logout
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
)
.addFilterBefore(
new JwtAuthorizationFilter(userRepository, jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
}
JWT๋ฅผ ์ฒ์ ์ ํ์ ๋๋ ๊ฐ๋ ์ด ์ด๋ ค์ ์ง๋ง,
์ง์ ๊ตฌํํ๋ฉด์ ์ธ์ฆ ํ๋ฆ, ํํฐ ์ฒ๋ฆฌ, ์ฟ ํค ์ค์ , ๋ณด์ ๊ณ ๋ ค์ฌํญ๊น์ง ์ดํดํ๊ฒ ๋๋ค.
์ถํ์ Refresh Token ์ฌ๋ฐ๊ธ, ์ฌ์ฉ์๋ณ ๊ถํ ๊ตฌ๋ถ๊น์ง ํ์ฅํด๋ณผ ๊ณํ์ด๋ค.
์ฟ ํค
, ์ฌ์ฉ์ ์ธ์ฆ์ SecurityContext
์ ์ค์ SecurityConfig
์ ๋ชจ๋ ์์๋ฅผ ์ ์ฐ๊ฒฐํด์ผ ์ ์ ์๋