Redis를 이용한 리프레쉬 토큰 관리 아이디어

Kim Dong Kyun·2023년 7월 27일
1

발단

현재 나는

  1. 유저 요청 시 액세스 토큰(AT)이 만료되었다면 리프레쉬 토큰(RT)를 검증하고, Redis 에도 존재하는 RT를 사용자가 가진 RT와 대조해서 사용자와 매칭되는지 확인 중
  • 사용자 헤더 : AT / RT
  • 레디스 : {사용자 이메일 : RT}
  1. 이 방법의 단점은, 다른 브라우저나 기기에서 요청 + 각각 만료되어 리프레쉬 토큰을 재발급 해야 한다면 기기마다 다르게 발급되는 리프레쉬 토큰을 적절히 검증이 불가능함 (하나의 리프레쉬 토큰으로만 관리하므로)
  • 즉, 크롬에서 로그인 하고
  • 사파리 등에서 다시 로그인 할 시에 레디스에 존재하는 리프레쉬 토큰 값이 "덮어써"지므로, 크롬에서는 적절한 RT를 가지고 있음에도 불구하고 레디스의 밸류와 맞지 않은 상황이 발생함.

그래서?

그래서 생각한건

사용자 이름 : {HashMap}

레디스의 키를

{사용자 이메일 : {deviceId : 리프레쉬 토큰} } 와 같은 식으로 사용하는 것

public class RedisAuthDAO {
    static Map<String, Map<String, String>> userRefreshTokens = new HashMap<>();

    public void storeRefreshToken(String username, String deviceId, String refreshToken) {
        Map<String, String> userTokens = userRefreshTokens.get(username);
        // 사용자의 이름을 키로 하는 맵에서, 그 안에 이중 맵으로 {아이디, 리프레쉬 토큰} 을 저장
        if (userTokens == null) {
            userTokens = new HashMap<>();
            userRefreshTokens.put(username, userTokens);
        }
        userTokens.put(deviceId, refreshToken);
    }

    public String getRefreshToken(String username, String deviceId) {
        Map<String, String> userTokens = userRefreshTokens.get(username);
        return userTokens != null ? userTokens.get(deviceId) : null;
    }

    public void removeRefreshToken(String username, String deviceId) {
        Map<String, String> userTokens = userRefreshTokens.get(username);
        if (userTokens != null) {
            userTokens.remove(deviceId);
        }
    }
}
  • 대충 위와 같은 형식. 레디스템플릿 사용하면 opsForHash 해서 적절히 직렬화해서 넣어야 할듯.

만약

유효기간, 듀레이션이 적절히 적용이 안된다면?

  • Map< String, ArrayList<> > 이런 형식으로 가야할듯.

그런데,

디바이스 아이디를 가져오는 것이 어렵다. 웹에서는 특히 그게 어려움 (앱은 그렇지 않다고 한다)

그렇다면 로컬 스토리지나 쿠키에 사용자를 식별할 디바이스 아이디를 입력해줘야 한다.

어떻게?

클라이언트 측에서 "브라우저의 이름" 을 가져오면, 그 이름과 사용자의 이메일을 붙여서 로컬 스토리지에 저장해주자.

function getBrowserName() {
  let userAgent = navigator.userAgent;

  if (userAgent.indexOf("Firefox") > -1) {
    return "Firefox";
  } else if (userAgent.indexOf("MSIE") > -1 || userAgent.indexOf("Trident") > -1) {
    return "Internet Explorer";
  } else if (userAgent.indexOf("Edge") > -1) {
    return "Edge";
  } else if (userAgent.indexOf("Chrome") > -1) {
    return "Chrome";
  } else if (userAgent.indexOf("Safari") > -1) {
    return "Safari";
  } else {
    return "unknown";
  }
}

let emailAddress = "user@example.com";
let browserName = getBrowserName();
let combinedIdentifier = emailAddress + "-" + browserName;
localStorage.setItem("uniqueIdentifier", combinedIdentifier);

로컬 스토리지는 브라우저마다 별도로 가지므로, 위와 같이 클라이언트측에서 셋 한 후에 "LoginRequestDto" 에서 브라우저의 이름을 가져오면 될 듯 하다. 아니면 헤더에 Browser-name 이라는 키로 브라우저를 식별할 수 있도록 하던가.

그렇다면 백단에서는 아래와 같이 될 것

@RestController
@RequestMapping
public class UserController {
  // ...
  @PostMapping("/login")
  public ResponseEntity<?> login(@RequestBody LoginRequestDto loginRequestDto) {
      // "browserName" 필드를 포함하는 LoginRequest 객체가 있다고 가정
      String browserName = loginRequestDto.getBrowserName();
      // ...
  }
}
@RestController
@RequestMapping
public class UserController {
  // ...
  @PostMapping("/login")
  public ResponseEntity<?> login(HttpServletRequest request) {
      // 클라이언트 측 JavaScript에서 헤더에 "Browser-Name"을 포함하여 전송했다고 가정
      String browserName = request.getHeader("Browser-Name");
      // ...
  }
}

0개의 댓글