Backend Language/Spring boot(Java)

[Spring boot] Redis๋กœ JWT ๋งŒ๋ฃŒ Logout (Redis BlackList)

chaerlo127 2022. 8. 15. 22:45
728x90

์•ฑ ๋Ÿฐ์นญ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ, ๋กœ๊ทธ์•„์›ƒ api๋ฅผ ์ƒ์„ฑํ–ˆ๋‹ค.

 

JWT๋กœ User์˜ ์ •๋ณด๋ฅผ ์ ‘๊ทผํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—, ๋กœ๊ทธ์•„์›ƒ์„ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” AccessToken์˜ ์‹œ๊ฐ„์„ ๋งŒ๋ฃŒํ•ด์•ผํ–ˆ๋‹ค. 

 

์ƒ์„ฑํ•œ Token์„ ๋งŒ๋ฃŒํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” Redis๋ฅผ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๊ณ , ๋ธ”๋กœ๊ทธ์— ์ž‘์„ฑํ•˜๋ฉฐ ๋ณต์Šตํ•ด๋ณด๊ณ ์ž ํ•œ๋‹ค.

 

1. build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

build.gradle์— ์œ„์™€ ๊ฐ™์€ dependencies ์ถ”๊ฐ€

 

2. RedisRepositoryConfig

@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
public class RedisRepositoryConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    // lettuce
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

 

@Value Annotation์— ์žˆ๋Š” spring.redis.host, port๋Š” application properties์—์„œ ๋”ฐ๋กœ ์ž‘์„ฑํ•ด์ค€๋‹ค.

 

spring.redis.host=localhost
spring.redis.port=6379

 

redis๋ฅผ localhost์—์„œ ์‚ฌ์šฉํ•˜๋ฉฐ, port๋Š” 6379๋ฅผ ์‚ฌ์šฉํ•  ์˜ˆ์ •์ด๋‹ค.

728x90

3. JWTFilter

private final TokenProvider tokenProvider;
private final RedisTemplate redisTemplate;

@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {

        // 1. Request Header ์—์„œ ํ† ํฐ์„ ๊บผ๋ƒ„
        String jwt = resolveToken(request);

        // 2. validateToken ์œผ๋กœ ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
        // ์ •์ƒ ํ† ํฐ์ด๋ฉด ํ•ด๋‹น ํ† ํฐ์œผ๋กœ Authentication ์„ ๊ฐ€์ ธ์™€์„œ SecurityContext ์— ์ €์žฅ
        if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt, request)) {
        
        // String ~ if ๋ฌธ๊นŒ์ง€ ์ƒˆ๋กญ๊ฒŒ ์ถ”๊ฐ€
            String isLogout = (String)redisTemplate.opsForValue().get(jwt);

            if (ObjectUtils.isEmpty(isLogout)) {
                Authentication authentication = tokenProvider.getAuthentication(jwt);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(request, response);
    }

 

String isLogout ๋ถ€ํ„ฐ if๋ฌธ ๊นŒ์ง€ ์ƒˆ๋กญ๊ฒŒ ์ถ”๊ฐ€ํ•˜๊ณ  RedisTemplate ๋ฅผ DI๋กœ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

 

 

4. JwtSecurityConfig

JwtFilter์— DI๋ฅผ ์ถ”๊ฐ€ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— JwtSecurityConfig์—๋„ ์˜์กด์„ฑ ์ถ”๊ฐ€ํ•˜๊ณ , Constructor๋กœ ๋„˜๊ฒจ์ค€๋‹ค.

// ์ง์ ‘ ๋งŒ๋“  TokenProvider ์™€ JwtFilter ๋ฅผ SecurityConfig ์— ์ ์šฉํ•  ๋•Œ ์‚ฌ์šฉ
@RequiredArgsConstructor
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private final TokenProvider tokenProvider;
    private final RedisTemplate redisTemplate;
    // TokenProvider ๋ฅผ ์ฃผ์ž…๋ฐ›์•„์„œ JwtFilter ๋ฅผ ํ†ตํ•ด Security ๋กœ์ง์— ํ•„ํ„ฐ๋ฅผ ๋“ฑ๋ก
    @Override
    public void configure(HttpSecurity http) {
        JwtFilter customFilter = new JwtFilter(tokenProvider, redisTemplate);
        http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

 

 

5. SecurityConfig

private final RedisTemplate redisTemplate;

....
...
..
.


.and()
.apply(new JwtSecurityConfig(tokenProvider, redisTemplate))

 

6. TokenProvider

public Long getExpiration(String accessToken) {
     // accessToken ๋‚จ์€ ์œ ํšจ์‹œ๊ฐ„
     Date expiration = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody().getExpiration();
     // ํ˜„์žฌ ์‹œ๊ฐ„
     Long now = new Date().getTime();
     return (expiration.getTime() - now);
    }

 

getExpiration์„ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค. ํ˜„์žฌ accessToken์˜ ์ ‘๊ทผ ๊ฐ€๋Šฅ ์‹œ๊ฐ„์„ ์—†์• ๊ณ  blacklist๋กœ ์˜ฌ๋ฆฐ๋‹ค. 

 

7. Service

String accessToken = request.getHeader("Authorization").substring(7);

Long expiration = tokenProvider.getExpiration(accessToken);

redisTemplate.opsForValue()
         .set(accessToken, "logout", expiration, TimeUnit.MILLISECONDS);

 

 

getExpiration์„ ํ†ตํ•ด accessToken์˜ ์ ‘๊ทผ ์‹œ๊ฐ„์„ ๋งŒ๋ฃŒํ•˜๊ธฐ ์œ„ํ•ด token์˜ ๊ฐ’์„ ๋ถˆ๋Ÿฌ์˜ค๊ณ , redisTemplate๋กœ ์ ‘๊ทผ์‹œ๊ฐ„์„ ๋งŒ๋ฃŒํ•œ๋‹ค. 

์ด ๋•Œ, service ๋ถ€๋ถ„์—๋Š” redisTemplate DI๋ฅผ ์ž‘์„ฑํ•ด์ค˜์•ผ ํ•œ๋‹ค.

 

8. Redis Window ๋‹ค์šด๋กœ๋“œ

๋‚˜๋Š” ์ˆ˜๋งŽ์€ ๋ธ”๋กœ๊ทธ ๊ธ€์„ ์ฐพ์•„๋ณด๋ฉด์„œ build.gradle์— dependencies์—์„œ๋งŒ ์ ์šฉ์„ ํ•˜๋ฉด redis๊ฐ€ ๋˜๋Š” ์ค„ ์•Œ์•˜์ง€๋งŒ ๋”ฐ๋กœ ๋‹ค์šด๋กœ๋“œ ํ›„ port ์‹คํ–‰์„ ํ•ด์•ผ์ง€ ๊ฐ€๋Šฅํ•œ ๊ฒƒ์ด์—ˆ๋‹ค. 

https://github.com/microsoftarchive/redis/releases

 

Releases · microsoftarchive/redis

Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported: Strings, Lists, Sets, Sorted Sets, Hashes - microsoftarchive/redis

github.com

 

์œ„ ๊นƒํ—ˆ๋ธŒ ์ฃผ์†Œ์—์„œ 

 

 

 

Redis-x64-3.0.504.msi๋ฅผ ๋‹ค์šด๋ฐ›์œผ๋ฉด Redis๋ฅผ ํ™œ์šฉํ•œ ๋กœ๊ทธ์•„์›ƒ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์™„์„ฑ์ด๋‹ค.!!!

 

 

์•ฑ ๋Ÿฐ์นญ์„ ์œ„ํ•ด redis๋ฅผ ๋ฐฐ์› ์ง€๋งŒ, ์™„๋ฒฝํ•˜๊ฒŒ ๋ฐฐ์šฐ์ง€ ๋ชปํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ ์‹œ๊ฐ„์„ ๋‚ด์–ด ๋‹ค์‹œ ๊ณต๋ถ€๋ฅผ ์ง„ํ–‰ํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™๋‹ค.

๋˜ํ•œ redis๋ฅผ ์„œ๋ฒ„ ๋ฐฐํฌ๋ฅผ ์ง„ํ–‰ํ–ˆ์„ ๋•Œ์—๋„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋ฏธ๋ฆฌ ๊ณต๋ถ€๋ฅผ ์ง„ํ–‰ํ•  ์˜ˆ์ •์ด๋‹ค.

 

Spring boot Security๋ฅผ ์ฒ˜์Œ ์ ‘ํ•ด๋ดค์ง€๋งŒ, jwt, redis ๋“ฑ์„ ์ด๋ฒˆ ๊ธฐํšŒ๋ฅผ ํ†ตํ•ด ๋งŽ์ด ์•Œ๊ฒŒ๋œ ๊ฒƒ ๊ฐ™๋‹ค.

 

 

[์ถœ์ฒ˜]

https://wildeveloperetrain.tistory.com/61

https://github.com/microsoftarchive/redis/releases

 

728x90