개요
- jwt 토큰방식의 인증/인가를 도입
- 토큰 하나로 모든 서비스를 처리하게 된다면 해당 토큰을 탈취당하는 보안 이슈 발생 우려
- jwt 토큰방식의 보안성 강화
접근방법
- 토큰에 클라이언트의 ip주소를 담고 검증과정에서 ip주소가 다를 경우 에러를 반환
- RefreshToken 사용
ip주소가 담긴 토큰이 탈취된다면 더 심각한 보안 이슈가 발생될 가능성이 있으므로 많이 쓰이는 RefreshToken을 사용
또한, 모바일의 경우 ip 주소의 변경이 자주 일어날 수 있으므로 그때마다 에러를 반환하게 된다면 유저가 불편함을 겪기 때문에 ip 주소를 담지 않기로 함
또한 RefreshToken도 유저에게 발급하고 관리하게 된다면 마찬가지로 탈취될 가능성이 있다고 우려되어, RefreshToken은 유저에게 제공하지 않고 Redis에 저장하여 따로 관리함으로써 보안성 강화
해결과정
- 최초 로그인 시 accessToken을 발급해서 유저에게 제공하고, 같은 정보로 refreshToken을 발급해 redis에 저장
- redis에는 "key : {accessToken}, value : {refreshToken}, expire : {refreshToken의 만료시간}"으로 저장
- accessToken의 만료시간은 30분, refreshToken의 만료시간은 6시간으로 refreshToken의 만료시간을 더 길게 설정
- 유저가 accessToken을 주고받으며 활동을 하다가 만료가 되면 redis에 refreshToken이 존재하는지 확인
- 존재한다면 newAccessToken을 발급한 뒤 기존의 redis 값은 삭제
- "key : {newAccessToken}, value : {refreshToken}, expire : {refreshToken의 남은 만료시간}"으로 다시 저장
- refreshToken의 만료시간이 다 돼서 존재하지 않는다면 유저에게 재로그인을 요청
refreshToken을 활용한 accessToken 재발급
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
String accessToken = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0).substring(7);
validBlackToken(accessToken);
if (tokenProvider.validateToken(accessToken)) {
// accessToken이 문제 없으면 Header에 userId 담기
return chain.filter(exchange);
} else if(!tokenProvider.validateToken(accessToken)) {
boolean isExist = tokenProvider.isExistRefreshToken(accessToken);
if (isExist) {
// redis에 accessToken으로 저장된 refreshToken 가져오기
String refreshToken = (String) redisTemplate.opsForValue().get(accessToken);
// refreshToken 검증
tokenProvider.validateToken(refreshToken);
// refreshToken의 정보로 newAccessToken 발급
String newAccessToken = tokenProvider.createAccessToken(refreshToken);
log.info("newAccessToken -> {}", newAccessToken);
// refreshToken의 기존 만료시간으로 다시 redis에 저장
Long oldExpiration = redisTemplate.getExpire(accessToken);
redisTemplate.opsForValue().set(newAccessToken, refreshToken, oldExpiration*1000, TimeUnit.MILLISECONDS);
// 기존의 accessToken으로 저장된 데이터 삭제
redisTemplate.delete(accessToken);
Long userId = tokenProvider.getUserId(newAccessToken);
request.mutate().header("Auth", "true").build();
request.mutate().header("userId", String.valueOf(userId)).build();
tokenProvider.setHeaderAccessToken(response, newAccessToken);
return chain.filter(exchange);
}
}
}
return onError(exchange, "권한없음 : 토큰이 필요합니다.", HttpStatus.UNAUTHORIZED);
결론
- 만약 accessToken을 탈취해 간 사람이 원래 주인보다 먼저 재발급 요청을 한다면??
- 토큰 안에 들어가는 정보는 최소한으로만 줘야 할지 vs 닉네임, 프로필 이미지, 이메일 등등 유저의 정보를 다 담을지
- 보안상의 이유로는 전자가 맞는 듯하나, 전자일 경우 user-service에서 유저의 다른 정보를 가져오는 로직이 필요
'멋진 개발자 > 트러블슈팅' 카테고리의 다른 글
트러블 슈팅 - 준비된 재고는 200개인데 240명이 구매를 성공한 건에 대하여 (0) | 2024.03.15 |
---|---|
트러블 슈팅 - api-gateway에서 jwt 인증/인가 구현 (0) | 2024.03.03 |
트러블 슈팅 - JavaMailSender 객체로 이메일 인증하기 (0) | 2024.03.03 |