Kakao OAuth2 + JWT + Redis๋ฅผ ํตํ ์ธ์ฆ ๊ณผ์ ๊ตฌํ (3) - Redis๋ก RefreshToken ๊ด๋ฆฌ
by rlaehddnd0422์ง๋ ํฌ์คํ ์์ JWT๋ฅผ ํ๋ก์ ํธ์ ๋์ ํด ๋ณด์์ต๋๋ค.
๋ก๊ทธ์ธ ์ฑ๊ณต ์ AccessToken๊ณผ RefreshToken์ ํด๋ผ์ด์ธํธ์๊ฒ Json์ผ๋ก ์๋ตํ๋๋ก ๊ตฌํํ๊ณ , ์ถ๊ฐ๋ก JWT ํํฐ๋ฅผ ์์ฑํ์ฌ AccessToken์ ๊ฒ์ฆํ๋๋ก ๊ตฌํํ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ JwtFilter์์ ์ก์ธ์ค ํ ํฐ์ด ์ ํจํ์ง ์์ ๊ฒฝ์ฐ์๋ ํธ๋ค๋ฌ๋ฅผ ์ฌ์ฉํ์ฌ ๋ก๊ทธ์ธ ์์ฒญ ๋ฉ์์ง๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ์๋ต์ผ๋ก ๋ด๋ ค์ฃผ์์๋๋ฐ์.
๋ง์ง๋ง์ผ๋ก ์ ํจ์ฑ๊ณผ ๋ณ๊ฐ๋ก ์ก์ธ์ค ํ ํฐ์ ๊ธฐํ์ด ๋ง๋ฃ๋ ๊ฒฝ์ฐ์ RefreshToken์ ํตํด AccessToken์ ์ฌ๋ฐ๊ธ๋ฐ๋ ๋ก์ง์ ๊ตฌํํด๋ณด๊ฒ ์ต๋๋ค.
์ก์ธ์ค ํ ํฐ์ ์ฌ๋ฐ๊ธํ๋ ๋ฐฉ๋ฒ์ ํฌ๊ฒ 2๊ฐ์ง๊ฐ ์๊ฒ ์ต๋๋ค.
- ์์ฒญ๋ง๋ค Access Token๊ณผ Refresh Token์ ๊ฐ์ด ๋๊ธฐ๊ณ ์ก์ธ์ค ํ ํฐ์ด ๋ง๋ฃ๋ ๊ฒฝ์ฐ ๋ฆฌํ๋ ์ ํ ํฐ์ ๊ฒ์ฆํ์ฌ ์ฌ๋ฐ๊ธ ๋ฐ๊ธฐ
- ์ฌ๋ฐ๊ธ 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 ํธ๋ค๋ฌ๊ฐ ๋์ํ์ฌ ํด๋ผ์ด์ธํธ์๊ฒ ์ฌ๋ก๊ทธ์ธ ์์ฒญ ๋ฉ์์ง๋ฅผ ์๋ต.
<์ฐธ๊ณ ์๋ฃ>
'๐ Backend > Spring Security' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋ธ๋ก๊ทธ์ ์ ๋ณด
Study Repository
rlaehddnd0422