# Kakao OAuth2 + JWT + Redis๋ฅผ ํ†ตํ•œ ์ธ์ฆ ๊ณผ์ • ๊ตฌํ˜„ (3) - Redis๋กœ RefreshToken ๊ด€๋ฆฌ
Study Repository

Kakao OAuth2 + JWT + Redis๋ฅผ ํ†ตํ•œ ์ธ์ฆ ๊ณผ์ • ๊ตฌํ˜„ (3) - Redis๋กœ RefreshToken ๊ด€๋ฆฌ

by rlaehddnd0422

์ง€๋‚œ ํฌ์ŠคํŒ…์—์„œ JWT๋ฅผ ํ”„๋กœ์ ํŠธ์— ๋„์ž…ํ•ด ๋ณด์•˜์Šต๋‹ˆ๋‹ค.

๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ AccessToken๊ณผ RefreshToken์„ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ Json์œผ๋กœ ์‘๋‹ตํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜๊ณ , ์ถ”๊ฐ€๋กœ JWT ํ•„ํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ AccessToken์„ ๊ฒ€์ฆํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  JwtFilter์—์„œ ์•ก์„ธ์Šค ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋Š” ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋กœ๊ทธ์ธ ์š”์ฒญ ๋ฉ”์‹œ์ง€๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์‘๋‹ต์œผ๋กœ ๋‚ด๋ ค์ฃผ์—ˆ์—ˆ๋Š”๋ฐ์š”.

๋งˆ์ง€๋ง‰์œผ๋กœ ์œ ํšจ์„ฑ๊ณผ ๋ณ„๊ฐœ๋กœ ์•ก์„ธ์Šค ํ† ํฐ์˜ ๊ธฐํ•œ์ด ๋งŒ๋ฃŒ๋œ ๊ฒฝ์šฐ์— RefreshToken์„ ํ†ตํ•ด AccessToken์„ ์žฌ๋ฐœ๊ธ‰๋ฐ›๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์•ก์„ธ์Šค ํ† ํฐ์„ ์žฌ๋ฐœ๊ธ‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ํฌ๊ฒŒ 2๊ฐ€์ง€๊ฐ€ ์žˆ๊ฒ ์Šต๋‹ˆ๋‹ค.

  1. ์š”์ฒญ๋งˆ๋‹ค Access Token๊ณผ Refresh Token์„ ๊ฐ™์ด ๋„˜๊ธฐ๊ณ  ์•ก์„ธ์Šค ํ† ํฐ์ด ๋งŒ๋ฃŒ๋œ ๊ฒฝ์šฐ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ๊ฒ€์ฆํ•˜์—ฌ ์žฌ๋ฐœ๊ธ‰ ๋ฐ›๊ธฐ
  2. ์žฌ๋ฐœ๊ธ‰ API๋ฅผ ๋งŒ๋“ค๊ณ  ์„œ๋ฒ„์—์„œ Access Token์ด ๋งŒ๋ฃŒ๋˜์—ˆ๋‹ค๊ณ  ์‘๋‹ตํ•˜๋ฉด Refresh Token์œผ๋กœ ์š”์ฒญํ•˜์—ฌ ์žฌ๋ฐœ๊ธ‰ ๋ฐ›๊ธฐ.

์ €๋Š” 1๋ฒˆ ๋ฐฉ๋ฒ•์œผ๋กœ ์ง„ํ–‰ํ•˜์˜€๋Š”๋ฐ,

์šฐ์„  RefreshToken์ด ์œ ํšจํ•œ ์ง€ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋กœ๊ทธ์ธ ์‹œ ๋ฐœ๊ธ‰๋ฐ›์€ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ์–ด๋”˜๊ฐ€์— ์ €์žฅํ•ด๋‘์–ด์•ผ ํ•˜๋Š”๋ฐ, ์ €๋Š” ์†๋„๊ฐ€ ๋น ๋ฅด๊ณ  ๋ณ„๋„์˜ ์ฟผ๋ฆฌ๋ฌธ์ด ํ•„์š”ํ•˜์ง€ ์•Š์€ Redis๋ฅผ ์ฑ„ํƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

0. ์„ค์ • ์ •๋ณด

application.yml

redis:
  host: localhost
  port: 6379
  • port๋Š” ๊ธฐ๋ณธ redis port์ธ 6379๋กœ ์ง€์ •ํ•ด์ค๋‹ˆ๋‹ค.

 

build.gradle์— ์˜์กด์„ฑ ์ถ”๊ฐ€

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

 

 

RedisConfig

@EnableRedisRepositories
@Configuration
public class RedisConfig {

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

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisPort);
    }
}
  • @EnableRedisRepositories๋กœ  ๋ ˆ๋””์Šค ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์ด์šฉํ•œ๋‹ค๊ณ  ๋ช…์‹œ
  • @Configuration for ์ˆ˜๋™ ๋นˆ ๋“ฑ๋ก
  • @Bean์œผ๋กœ yml์— ๋“ฑ๋กํ•œ host, ํฌํŠธ๋ฒˆํ˜ธ๋กœ Redis ํด๋Ÿฌ์Šคํ„ฐ๋ฅผ ๋นˆ์œผ๋กœ ๋“ฑ๋ก

Redis์— ๋“ฑ๋กํ•  RefreshToken Entity

@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
@RedisHash(value = "refresh", timeToLive = 604800)
public class RefreshToken {

    private String id;
    private Collection<? extends GrantedAuthority> authorities;

    @Indexed
    private String refreshToken;

    public String getAuthority() {
        return authorities.stream()
                .map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
                .collect(Collectors.toList())
                .get(0)
                .getAuthority();
    }
}
  • @RedisHash๋Š” Spring Data Redis ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค๋ฅผ Redis ํ•ด์‹œ ํ˜•์‹์˜ ๋ฐ์ดํ„ฐ๋กœ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค.
    • ์ด ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ช…์‹œํ•˜๋ฉด ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค๊ฐ€ redis ํ•ด์‹œ๋กœ ๋งคํ•‘๋˜๊ณ , Java ๊ฐ์ฒด์™€ Redis ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™˜ํ•˜๊ณ  ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • @Indexed ์–ด๋…ธํ…Œ์ด์…˜์„ ํ•„๋“œ์— ๋ถ™์ด๊ฒŒ ๋˜๋ฉด ํ•„๋“œ ๊ฐ’์œผ๋กœ๋„ ๋ฐ์ดํ„ฐ๋ฅผ ๋น ๋ฅด๊ฒŒ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

RefreshTokenRepository

public interface RefreshTokenRedisRepository extends CrudRepository<RefreshToken, Long> {

    RefreshToken findByRefreshToken(String refreshToken);
}
  • JPA์ฒ˜๋Ÿผ Redis์™€์˜ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ๋ ˆ๋””์Šค ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋Š” CrudRepository๋ฅผ ์ƒ์†๋ฐ›์•„ ๊ธฐ๋ณธ์ ์ธ ๋ฉ”์†Œ๋“œ๋“ค์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. RefreshToken ์—”ํ‹ฐํ‹ฐ์—๋Š” PK๊ฐ€ ๋”ฐ๋กœ ์ง€์ •๋˜์–ด ์žˆ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— Long์œผ๋กœ ์ง€์ •ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

 


๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ•ธ๋“ค๋Ÿฌ ๋ฆฌํŽ™ํ† ๋ง 

@Transactional
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                    Authentication authentication) throws IOException {
    OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
    KakaoUserInfo kakaoUserInfo = new KakaoUserInfo(oAuth2User.getAttributes());

    Member member = memberRepository.findByEmail(kakaoUserInfo.getEmail())
            .orElseThrow(MemberNotFoundException::new);

    TokenDto tokenDto = tokenProvider.createToken(member.getEmail(), member.getRole().name());

    saveRefreshTokenOnRedis(member, tokenDto);
    String redirectURI = String.format(REDIRECT_URI, tokenDto.getAccessToken(), tokenDto.getRefreshToken());
    getRedirectStrategy().sendRedirect(request, response, redirectURI);
}

 

private void saveRefreshTokenOnRedis(Member member, TokenDto tokenDto) {
    List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
    simpleGrantedAuthorities.add(new SimpleGrantedAuthority(member.getRole().name()));
    refreshTokenRedisRepository.save(RefreshToken.builder()
            .id(member.getEmail())
            .authorities(simpleGrantedAuthorities)
            .refreshToken(tokenDto.getRefreshToken())
            .build());
}

 

  • ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ๋ฐœ๊ธ‰๋ฐ›์€ refreshToken - ํšŒ์› ์ด๋ฉ”์ผ์„ refreshToken ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์— ์ €์žฅํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

TokenProvider์— at/rt ์žฌ๋ฐœ๊ธ‰ ๋ฉ”์†Œ๋“œ ์ถ”๊ฐ€

@Transactional
public TokenDto reIssueAccessToken(String refreshToken) {
    RefreshToken findToken = refreshTokenRedisRepository.findByRefreshToken(refreshToken);

    TokenDto tokenDto = createToken(findToken.getId(), findToken.getAuthority());
    refreshTokenRedisRepository.save(RefreshToken.builder()
            .id(findToken.getId())
            .authorities(findToken.getAuthorities())
            .refreshToken(tokenDto.getRefreshToken())
            .build());

    return tokenDto;
}
  • refreshToken ๊ฐ’์„ ๋ฐ›์•„์„œ refreshToken์ด ์œ ํšจํ•˜๊ณ  ๋งŒ๋ฃŒ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ, redis์—์„œ ์ผ์น˜ํ•˜๋Š” ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ์ฐพ์•„ ์ƒˆ๋กœ accessToken๊ณผ refreshToken์„ ๋ฐœ๊ธ‰๋ฐ›๊ณ  Redis์—๋„ ์ƒˆ๋กœ ๋ฐœ๊ธ‰๋ฐ›์€ refreshToken์„ ์—…๋ฐ์ดํŠธ ํ•ด์ค๋‹ˆ๋‹ค.

JwtFilter ์ˆ˜์ •

@Override
protected void doFilterInternal(HttpServletRequest request,
                                HttpServletResponse response,
                                FilterChain filterChain) throws ServletException, IOException {
    if (isRequestPassURI(request, response, filterChain)) {
        return;
    }

    String accessToken = getTokenFromHeader(request, ACCESS_HEADER);

    if (validateExpire(accessToken) && validate(accessToken)) {
        SecurityContextHolder.getContext().setAuthentication(tokenProvider.getAuthentication(accessToken));
    }

    if (!validateExpire(accessToken) && validate(accessToken)) {
        String refreshToken = getTokenFromHeader(request, REFRESH_HEADER);
        if (validate(refreshToken) && validateExpire(refreshToken)) {
            // accessToken, refreshToken ์žฌ๋ฐœ๊ธ‰
            TokenDto tokenDto = tokenProvider.reIssueAccessToken(refreshToken);
            SecurityContextHolder.getContext()
                    .setAuthentication(tokenProvider.getAuthentication(tokenDto.getAccessToken()));

            redirectReissueURI(request, response, tokenDto);
        }
    }
    
    filterChain.doFilter(request, response);
}
  • JWT ํ•„ํ„ฐ์—์„œ ์•ก์„ธ์Šคํ† ํฐ ์œ ํšจํ•˜์ง€๋งŒ ๋งŒ๋ฃŒ๋œ ๊ฒฝ์šฐ, ์š”์ฒญ ํ—ค๋”์˜ refreshToken ๊ฐ’์„ ๋ฐ›์•„ ๊ฐ’์ด ์œ ํšจํ•˜๊ณ  ๋งŒ๋ฃŒ๋˜์ง€ ์•Š์•˜๋Š”์ง€ ๊ฒ€์‚ฌํ•˜๊ณ  tokenProvider์—๊ฒŒ ์žฌ๋ฐœ๊ธ‰์„ ์œ„์ž„ํ•ฉ๋‹ˆ๋‹ค.
    • refreshToken์ด ์œ ํšจํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋œ ๊ฒฝ์šฐ, ๋˜๋Š” accessToken์ด ์œ ํšจํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋œ ๊ฒฝ์šฐ์—๋Š” doFilter๋กœ ํƒ€๊ณ  ๋“ค์–ด๊ฐ€ JwtAccessDeined ํ•ธ๋“ค๋Ÿฌ์—์„œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋กœ ์‘๋‹ตํ•˜๋„๋ก ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.
private static void redirectReissueURI(HttpServletRequest request, HttpServletResponse response, TokenDto tokenDto)
        throws IOException {
    HttpSession session = request.getSession();
    session.setAttribute("accessToken", tokenDto.getAccessToken());
    session.setAttribute("refreshToken", tokenDto.getRefreshToken());
    response.sendRedirect("/api/sign/reissue");
}
  • ์žฌ๋ฐœ๊ธ‰์„ ํ†ตํ•ด ํ† ํฐ์„ ๋ฐ›์•„ Authentication ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด ์‹œํ๋ฆฌํ‹ฐ ์ปจํ…์ŠคํŠธ์— ๋ณด๊ด€ํ•˜๊ณ , ๋ฐœ๊ธ‰๋ฐ›์€ ํ† ํฐ๋“ค์€ ์„ธ์…˜์— ์ €์žฅํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ redirectํ•˜์—ฌ ์•Œ๋ ค์ค๋‹ˆ๋‹ค.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/sign")
public class SignController {

    /**
     * ์•ก์„ธ์Šค ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ API
     */
    @GetMapping("/reissue")
    public ResponseEntity reissueToken(HttpServletRequest request) {
        HttpSession session = request.getSession();
        String accessToken = (String) session.getAttribute("accessToken");
        String refreshToken = (String) session.getAttribute("refreshToken");

        return new ResponseEntity(new TokenDto(accessToken, refreshToken), HttpStatus.OK);
    }
  • redirect๋œ URI๋Š” ํ•ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ์™€ ๋งคํ•‘๋˜์–ด ํด๋ผ์ด์–ธํŠธ๋Š” Json ํ˜•ํƒœ๋กœ ์ƒˆ๋กœ ๋ฐœ๊ธ‰๋ฐ›์€ ์•ก์„ธ์Šคํ† ํฐ, ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์˜ ์ •๋ณด๋ฅผ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” ์ด์ œ ์ด ๊ฐ’๋“ค์„ ํ—ค๋”์— ์„ค์ •ํ•ด์„œ ์„œ๋ฒ„์— ์š”์ฒญํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

1) ์•ก์„ธ์Šค ํ† ํฐ ์œ ํšจ, ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์œ ํšจ -> ์ธ์ฆ ํ†ต๊ณผ

 

2) ์•ก์„ธ์Šค ํ† ํฐ ๋งŒ๋ฃŒ, ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์œ ํšจ -> ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ํ†ตํ•ด ์•ก์„ธ์Šค ํ† ํฐ๊ณผ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์žฌ๋ฐœ๊ธ‰

 

3) ์•ก์„ธ์Šค ํ† ํฐ ์œ ํšจ, ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๋งŒ๋ฃŒ -> ๋กœ๊ทธ์ธ ์‹œ ์•ก์„ธ์Šค ํ† ํฐ๋ณด๋‹ค ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์˜ ๋งŒ๋ฃŒ๊ธฐ๊ฐ„์ด ๊ธธ ๋ฟ๋”๋Ÿฌ ์•ก์„ธ์Šค ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์‹œ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ๋„ ํ•จ๊ป˜ ์žฌ๋ฐœ๊ธ‰ ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์•ก์„ธ์Šค ํ† ํฐ์ด ์œ ํšจํ•˜๊ณ , ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜๋Š” ๊ฒฝ์šฐ๋Š” ์ ˆ๋Œ€ ๋ฐœ์ƒํ•˜์ง€ X

 

4) ์•ก์„ธ์Šค ํ† ํฐ ๋งŒ๋ฃŒ, ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๋งŒ๋ฃŒ -> JwtAccessDenied ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ๋™์ž‘ํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์žฌ๋กœ๊ทธ์ธ ์š”์ฒญ ๋ฉ”์‹œ์ง€๋ฅผ ์‘๋‹ต.

 


<์ฐธ๊ณ  ์ž๋ฃŒ>

 

[SpringBoot] @RedisHash๋ฅผ ์ด์šฉํ•œ Spring Data Redis Repository ์ ์šฉ๊ธฐ

Redis๋ž€? Key, Value ๊ตฌ์กฐ์˜ ๋น„์ •ํ˜• ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ์˜คํ”ˆ ์†Œ์Šค ๊ธฐ๋ฐ˜์˜ ๋น„๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค ๊ด€๋ฆฌ ์‹œ์Šคํ…œ (DBMS)์ด๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ์บ์‹œ, ๋ฉ”์„ธ์ง€ ๋ธŒ๋กœ์ปค๋กœ ์‚ฌ์šฉ๋˜๋ฉฐ ์ธ๋ฉ”๋ชจ๋ฆฌ ๋ฐ

mingyum119.tistory.com

 

 

Redis๋ฅผ ํ†ตํ•œ JWT Refresh Token ๊ด€๋ฆฌ

๊นƒํ—ˆ๋ธŒ ์ €์žฅ์†Œ GitHub - solchan98/Playground: ๐Ÿ› ๊ฐœ๋ฐœ ๊ณต๋ถ€ ๋†€์ดํ„ฐ ๐Ÿ› ๐Ÿ› ๊ฐœ๋ฐœ ๊ณต๋ถ€ ๋†€์ดํ„ฐ ๐Ÿ›. Contribute to solchan98/Playground development by creating an account on GitHub. github.com +2022-09-28 ์ƒˆ๋กœ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

sol-devlog.tistory.com

 

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

Study Repository

rlaehddnd0422

ํ™œ๋™ํ•˜๊ธฐ