๐Ÿ“Œ Spring Security - JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ ๊ตฌํ˜„

My Pale Blue Dotยท2025๋…„ 5์›” 19์ผ
0

SPRING BOOT

๋ชฉ๋ก ๋ณด๊ธฐ
35/40
post-thumbnail

๐Ÿ“… ๋‚ ์งœ

2025-05-19

๐Ÿ“ ํ•™์Šต ๋‚ด์šฉ

โœ… ๊ธฐ์กด ๋ฐฉ์‹ vs JWT ๋ฐฉ์‹

๊ตฌ๋ถ„์„ธ์…˜ ๋ฐฉ์‹JWT ๋ฐฉ์‹
์ƒํƒœ ์ €์žฅ์„œ๋ฒ„ ์„ธ์…˜ (Stateful)๋ฌด์ƒํƒœ (Stateless)
์ธ์ฆ ์œ ์ง€์„ธ์…˜ IDAccess Token
์ €์žฅ ์œ„์น˜์„œ๋ฒ„ (๋ฉ”๋ชจ๋ฆฌ, DB)ํด๋ผ์ด์–ธํŠธ (์ฟ ํ‚ค, ํ—ค๋” ๋“ฑ)
ํ™•์žฅ์„ฑ๋‚ฎ์Œ (์„œ๋ฒ„ ๋ถ€ํ•˜โ†‘)๋†’์Œ (๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์ ํ•ฉ)

โœ… ์šฐ๋ฆฌ๋Š” JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ ๋ฐฉ์‹์„ ์„ ํƒํ•˜๊ณ , Access Token์„ ์ฟ ํ‚ค์— ์ €์žฅํ•˜์—ฌ ์ธ์ฆ ํ๋ฆ„์„ ๊ตฌํ˜„


1๏ธโƒฃ JWT๋ž€?

  • JSON Web Token: ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ JSON์œผ๋กœ ์ธ์ฝ”๋”ฉํ•œ ํ›„ ์„œ๋ช…ํ•˜์—ฌ ์ƒ์„ฑํ•˜๋Š” ํ† ํฐ
  • ์„ธ ๋ถ€๋ถ„์œผ๋กœ ๊ตฌ์„ฑ๋จ: Header.Payload.Signature
  • ์ฃผ๋กœ ์ธ์ฆ ์ •๋ณด(์‚ฌ์šฉ์ž๋ช…, ๊ถŒํ•œ ๋“ฑ)๋ฅผ ๋‹ด์•„ ์ „์†ก

2๏ธโƒฃ ์˜์กด์„ฑ ์ถ”๊ฐ€ (build.gradle)

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

3๏ธโƒฃ ์ฃผ์š” ํด๋ž˜์Šค๋ณ„ ์„ค๋ช… ๋ฐ ์ฝ”๋“œ


๐Ÿ”น KeyGenerator.java

[๋น„๋ฐ€ํ‚ค ์ƒ์„ฑ ์œ ํ‹ธ ํด๋ž˜์Šค]

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;
    }
}

๐Ÿ”น JwtProperties.java

[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";
}

์ฃผ๋กœ ํ† ํฐ ์œ ํšจ ์‹œ๊ฐ„๊ณผ ์ฟ ํ‚ค ์ด๋ฆ„ ๋“ฑ ๊ณตํ†ต ์„ค์ •๊ฐ’์„ ์ƒ์ˆ˜๋กœ ์ •์˜


๐Ÿ”น TokenInfo.java

[ํ† ํฐ ๋ฐ˜ํ™˜์šฉ DTO]

@Builder
@Data
@AllArgsConstructor
public class TokenInfo {
    private String grantType;     // ์ผ๋ฐ˜์ ์œผ๋กœ "Bearer"
    private String accessToken;   // JWT ์•ก์„ธ์Šค ํ† ํฐ
    private String refreshToken;  // JWT ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ
}

๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ํด๋ผ์ด์–ธํŠธ์— ๋ฐ˜ํ™˜ํ•  Access/Refresh ํ† ํฐ์„ ๋‹ด๋Š” DTO


๐Ÿ”น JwtTokenProvider.java

[ํ•ต์‹ฌ ์œ ํ‹ธ ํด๋ž˜์Šค] โ€“ ํ† ํฐ ์ƒ์„ฑ, ๊ฒ€์ฆ, ์ธ์ฆ ๊ฐ์ฒด ๋ณ€ํ™˜

@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 ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•ต์‹ฌ ํด๋ž˜์Šค


๐Ÿ”น JwtAuthorizationFilter.java

[์š”์ฒญ ํ•„ํ„ฐ] โ€“ ์š”์ฒญ๋งˆ๋‹ค ์ฟ ํ‚ค์—์„œ 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์—๊ฒŒ ์•Œ๋ฆผ


๐Ÿ”น CustomSuccessHandler.java

[๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ์ฒ˜๋ฆฌ] โ€“ 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);
    }
}

๐Ÿ”ง SecurityConfig ๋“ฑ๋ก ์˜ˆ์‹œ

@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๋ฅผ ์ด์šฉํ•ด ์„œ๋ฒ„๋Š” ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š๊ณ  ์ธ์ฆ ์ฒ˜๋ฆฌ
  • โœ… Access Token์€ ์ฟ ํ‚ค์— ์ €์žฅํ•˜์—ฌ ์ž๋™ ์ „์†ก๋˜๋„๋ก ์„ค์ •
  • โœ… ๋งค ์š”์ฒญ๋งˆ๋‹ค JwtAuthorizationFilter๊ฐ€ ํ† ํฐ์„ ํ™•์ธํ•˜์—ฌ ์ธ์ฆ ์ฒ˜๋ฆฌ
  • โœ… Refresh Token์„ ํ™œ์šฉํ•œ ์žฌ๋ฐœ๊ธ‰์€ ๋ณ„๋„ ๊ตฌํ˜„ ๊ฐ€๋Šฅ

๐Ÿ”— ์ฐธ๊ณ  ์ž๋ฃŒ


โœจ ๋А๋‚€ ์ 

JWT๋ฅผ ์ฒ˜์Œ ์ ‘ํ–ˆ์„ ๋•Œ๋Š” ๊ฐœ๋…์ด ์–ด๋ ค์› ์ง€๋งŒ,

์ง์ ‘ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์ธ์ฆ ํ๋ฆ„, ํ•„ํ„ฐ ์ฒ˜๋ฆฌ, ์ฟ ํ‚ค ์„ค์ •, ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ๊นŒ์ง€ ์ดํ•ดํ•˜๊ฒŒ ๋๋‹ค.

์ถ”ํ›„์— Refresh Token ์žฌ๋ฐœ๊ธ‰, ์‚ฌ์šฉ์ž๋ณ„ ๊ถŒํ•œ ๊ตฌ๋ถ„๊นŒ์ง€ ํ™•์žฅํ•ด๋ณผ ๊ณ„ํš์ด๋‹ค.


๐Ÿ“š ์š”์•ฝ

  • JWT = ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ธ์ฆ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋Š” ๋ฐฉ์‹
  • AccessToken์€ ์ฟ ํ‚ค, ์‚ฌ์šฉ์ž ์ธ์ฆ์€ SecurityContext์— ์„ค์ •
  • ๋กœ๊ทธ์•„์›ƒ/๋งŒ๋ฃŒ ์‹œ ์ฟ ํ‚ค ์‚ญ์ œ๋กœ ์ธ์ฆ ์ƒํƒœ ์ œ๊ฑฐ
  • SecurityConfig์— ๋ชจ๋“  ์š”์†Œ๋ฅผ ์ž˜ ์—ฐ๊ฒฐํ•ด์•ผ ์ •์ƒ ์ž‘๋™

profile
Here, My Pale Blue.๐ŸŒ

0๊ฐœ์˜ ๋Œ“๊ธ€