이전 시간에는 member가 login하면
accessToken, refreshToken을 발급해줬다.
https://velog.io/@dlsrjsdl6505/%ED%98%BC%EC%9E%90-%ED%95%98%EB%8A%94-Spring-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-6-JWT-accessToken-refreshToken
이번 포스팅에는 또 다른 내용, redis를 프로젝트에 적용시켰다.
내가 발급한 refreshToken을 클라이언트가 다시 돌려줬을때,
돌려 받은 토큰을 검증하는 일이 필요한데,
해당 작업은 클라이언트가 내 서버를 사용하는 동안 여러번 일어나므로
MySQL보다 In-memory DB인 Redis에 저장해놓고 확인하는게
훨씬 더 빠르다.
바로 아래 사진의 빨간색 동그라미에서의 작업이다.
그래서 redis 설치부터,
저번에 작성한 코드를 토대로 발급받은 토큰을 redis에 저장시키고,
그 토큰을 다시 꺼내오는것까지의 과정을 코드를 작성했다.
결과 소스 코드 : https://github.com/ingeon2/soloSpringProject
redis는 설치하는 방법이 두가지가 있다.
도커에서 설치하거나,
릴리즈를 직접 깃허브에서 다운로드 받아서 설치하거나
https://github.com/microsoftarchive/redis/releases
이미 도커는 설치되어 있어서
나는 첫번째 방법을 사용했다.
git bash 콘솔에 들어가서 redis 경로를 만들어 준 후, (위)
docker-compose.yml을 아래와 같이 작성했다.
docker-compose.yml의 내용은,
도커로부터 레디스 공식 이미지를 불러와,
6379 포트로 실행하라는 내용으로 작성했다.
이후 docker-compose를 docker-compose up 명령어를 사용해 빌드시켜 redis를 설치했다.(위)
설치 후
이렇게 도커 데스크톱을 보면 도커상에서 redis가 설치되었고,
도커상에서 실행할 수 있는 것을 확인할 수 있다.
이제 redis 설치가 끝났으니, Spring과 연동시켜줘야 한다.
가장 먼저 의존성을 추가한다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis' //redis
spring:
redis:
host: localhost
port: 6379
여기까지 작성하면,
방금 설치한 redis와 Spring의 연결은 되었다.
이제 redis사용을 위한 코드를 작성해야 한다.
아래와 같이 RedisService 클래스를 작성하였다. 물론 이해가 편하도록 주석도 달았다.
@Service
@Transactional
public class RedisService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// refreshToken을 Redis에 저장
public void saveRefreshTokenToRedis(String refreshToken, long memberId) {
//키 생성
String redisKey = "refreshToken:" + String.valueOf(memberId);
//키와 밸류를 redis에 저장
redisTemplate.opsForValue().set(redisKey, refreshToken);
//만료시간 설정해주기 (10분 만료로 설정)
Duration expiration = Duration.ofMinutes(10);
redisTemplate.expire(redisKey, expiration);
}
// 사용자 ID를 통해 Redis에서 refreshToken을 가져와서 확인
//클라이언트가 보내준 refreshToken과 Redis에 저장된 refreshToken이 일치하다면,
//클라이언트 하고싶은거 다 하도록 허용해도 된다!
public String getRefreshTokenFromRedis(long memberId) {
String redisKey = "refreshToken:" + String.valueOf(memberId);
return redisTemplate.opsForValue().get(redisKey);
}
}
그다음, 바로 위 클래스에서,
Redis에 저장하는 매서드인 saveRefreshTokenToRedis를
클라이언트의 인증 정보를 이용해 인증에 성공할 경우 호출되는
JwtAuthenticationFilter의
successfulAuthentication 매서드에 추가해줬다.
(어딘지 모르겠으면 이전 포스팅에 있습니다!)
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws ServletException, IOException {
//authResult.getPrincipal()로 Member 엔티티 클래스의 객체를 얻음
Member member = (Member) authResult.getPrincipal();
//토큰 생성
String accessToken = delegateAccessToken(member);
String refreshToken = delegateRefreshToken(member);
//토큰들을 헤더에 담음
response.setHeader("Authorization", "Bearer " + accessToken);
response.setHeader("Refresh", refreshToken);
============================여기추가용=========================================
//refreshToken만 redis 저장
redisService.saveRefreshTokenToRedis(refreshToken, member.getMemberId());
============================여기추가용=========================================
//MemberAuthenticationSuccessHandler의 onAuthenticationSuccess() 메서드를 호출하기위해
this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult);
}
여기까지 작성한 후,
순서대로
멤버 생성, 로그인을 실행하면
아래의 사진과 같이
refreshToken이 잘 저장된것을 확인할 수 있었다.
(key = refreshToken1, value = 아래 엄청 긴 영어값)
바로 위 사진의 redis-cli에서 사용한 명령어는
keys * -> (모든 키 보여주세요) 와
get refreshToken1 -> (refreshToken1 을 키로 가진 value를 보여주세요)
명령어를 사용했다.
(잘 보면, redis는 HashMap 자료구조의 key-value 쌍으로 저장이 된다는 것을 알 수 있다.)
이게 바로 redis의 특징(다른 자료구조 형태로 저장)이다.
그럼 이제, 저장까지 마쳤으니
해당 저장된 토큰을 가져와서
클라이언트가 나에게 준 토큰과 비교할 수 있어야 한다.
(비교한 후 일치해야 권한을 내주고 말고를 결정할 수 있으니)
해당 내용은 위에서 적어준 RedisService 클래스의
getRefreshTokenFromRedis 매서드를 통해 구현했고,
해당 매서드는 RedisController에서 GetMapping으로 사용한다.
@RestController
@Slf4j
@Validated
@Transactional
@RequiredArgsConstructor
@RequestMapping("/token")
public class RedisController {
@Autowired
private final RedisService redisService;
@GetMapping("/refreshToken/{member-id}") //memberId를 입력받아
public ResponseEntity getRefreshToken(@PathVariable("member-id") @Positive long memberId) {
//memberId를 변수로 받아 토큰을 받아온다
String refreshTokenFromRedis = redisService.getRefreshTokenFromRedis(memberId);
//ResponseEntity로 감싸서 보내주면 된다!
SingleResponseDto<String> response = new SingleResponseDto<>(refreshTokenFromRedis);
return ResponseEntity.ok(response);
}
}
리프레시 토큰을 응답해 준다.
이제, 사진에 있는 redis에서 꺼내온 토큰이
기존에 내가 발급해서 클라이언트에게 건네주었고,
컨네준 토큰을 클라이언트가 돌려주고 유효한지를 확인한 후,
나머지 로직을 작성하면 되겠다.
해당 로직은, JwtVerificationFilter 클래스에서 아래와 같이 작성해줬다.
private boolean verifyRefreshToken(Map<String, Object> claims, RedisService redisService) {
// 클라이언트가 전달한 refreshToken
String clientRefreshToken = (String) claims.get("refreshToken");
// redis에 저장해놓았던 기존의 토큰
long memberId = (Long) claims.get("memberId");
String refreshTokenFromRedis = redisService.getRefreshTokenFromRedis(memberId);
if(refreshTokenFromRedis.equals(null) || !clientRefreshToken.equals(refreshTokenFromRedis)) return false;
return true;
}
위는 검증 로직이고, 아래는 doFilterInternal매서드, 검증 매서드이다
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ServletException, IOException {
try {
Map<String, Object> claims = verifyJws(request);
if(verifyRefreshToken(claims, redisService)) { //받은 토큰이 유효하다면 Spring context로 가자!
setAuthenticationToContext(claims);
}
else {
throw new Exception("선생님 토큰이 만료되셨습니다");
}
} catch (ExpiredJwtException ee) {
request.setAttribute("exception", ee);
} catch (Exception e) {
request.setAttribute("exception", e);
}
filterChain.doFilter(request, response);
}
진짜 백문이 불여일타가 맞다.
redis가 뭐 해쉬맵으로 저장하네 뭐가 어쩌네.. 해도
들으면 그냥 들을 뿐이다.
직접 코드를 작성해보면, set 등을 이용해서 저장시키고,
일반 DB와 다른 자료구조를 이용하는구나 가 느껴진다.
그리고 사용법 또한 아 이런식으로 코드를 작성해서 사용하는거구나!
이러이러한 이점이 있어서 사용하는거구나! 를 더 체감할 수 있다.
물론 다른 고수들이 보기엔 귀여울 수 있지만,
정말 뿌듯하다.
이제 다음엔, 내가 작성한 코드들을 문서화 해줄 수 있는
Spring Restdocs 혹은 swagger을 이용해서
내 자취를 문서화를 통해 남기고 싶다.
훌륭한 글이네요. 감사합니다.