다중 디바이스 동시 로그인과 JWT TOKEN (2)

sun1·2023년 9월 7일
0

CS

목록 보기
2/17

📢 여러 디바이스에서 동시 로그인이 가능한 경우, accesstoken 과 refreshtoken 값이 디바이스 별로 달라야 한다.

[ 다중 디바이스 동시 로그인과 JWT TOKEN (1) ] 에서 이미 클라이언트의 정보를 읽어 JWT 토큰값을 달리 주는 법을 고민했다. 다만, 처음 로그인할 때 클라이언트에서 http헤더에 string값을 넣는 것 보다 더 좋은 방법이 없을까를 고민해 보았다.


💡 http 헤더의 Sec-Ch-Ua-Platform을 이용하자

아래는 http 헤더에 대한 문서로 http 헤더 값에 어떤 정보가 담겼는지 자세히 알 수 있다.

https://developer.mozilla.org/ko/docs/Web/HTTP/Headers

그 중 고민했던 건 Sec-Ch-Ua-Platform과 Sec-CH-UA-Mobile이었다.
만약 우리 프로젝트가 모바일과 웹 2종류 였다면 Sec-CH-UA-Mobile 값을 사용했을 것이다. Sec-CH-UA-Mobile는 boolean값으로 desktop 브라우저에서는 0, mobile device 브라우저에서는 1 값이 들어올 것이기 때문이다. 하지만 우리 프로젝트는 유니티, 워치까지 들어가기 때문에 Sec-Ch-Ua-Platform 값을 읽기로 하였다. ( 참고: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA-Platform )

Sec-Ch-Ua-Platform 값은 아래 중에 하나이다.

"Android", "Chrome OS", "Chromium OS", "iOS", "Linux", "macOS", "Windows", or "Unknown".

이 값을 읽어서 토큰의 payload값에 추가하여 다중기기 로그인을 구현하였다.


💡 구현 코드

  • controller
@PostMapping("/login")
    public ResponseEntity<MemberTokenResponseDto> login(@RequestBody @Validated MemberLoginRequestDto memberLoginRequestDto, HttpServletRequest request) {
        MemberTokenResponseDto response = memberService.login(memberLoginRequestDto, request);
        return ResponseEntity.ok(response);
    }

📌 HttpServletRequest

  • 웹브라우저 사용자인 클라이언트로부터 서버로 요청이 들어오면
    서버에서는 HttpServletRequest를 생성하며, 요청정보에 있는 패스로 매핑된 서블릿에게 전달
  • 전달받은 내용들을 파라미터로 Get과 Post 형식으로 클라이언트에게 전달
  • http프로토콜의 request정보를 서블릿에게 전달하기 위해 사용
  • 헤더정보, 파라미터, 쿠키, URI, URL 등의 정보를 읽어 들이는 메소드 포함
  • Body의 Stream을 읽어 들이는 메소드 포함
  • service
@Transactional
    public MemberTokenResponseDto login(MemberLoginRequestDto memberLoginRequestDto, HttpServletRequest request) {
        Member member = findMember("email", memberLoginRequestDto.getEmail());
        if (member.getState().equals(MemberState.RESIGNED.name())) throw new NoSuchMemberException("탈퇴한 사용자입니다.");

        String secChUaPlatform = request.getHeader("Sec-Ch-Ua-Platform");
        log.info(secChUaPlatform);

        UsernamePasswordAuthenticationToken authenticationToken = memberLoginRequestDto.toAuthentication();
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        MemberTokenResponseDto tokenInfo = jwtTokenProvider.generateToken(authentication, member, secChUaPlatform);

        stringRedisTemplate.opsForValue()
                .set("RT:" + tokenInfo.getAccessToken(), tokenInfo.getRefreshToken(), tokenInfo.getRefreshTokenExpirationTime(), TimeUnit.MILLISECONDS);

        return tokenInfo;
    }
  • jwttokenprovider
public MemberTokenResponseDto generateToken(Authentication authentication, Member member, String platform) {
        long now = (new Date()).getTime();
        // Access Token 생성
        Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME);
        String accessToken = Jwts.builder()
                .signWith(key, SignatureAlgorithm.HS256)
                .setHeaderParam("typ","JWT")
                .setSubject(member.getId().toString())
                .claim(EMAIL_KEY, authentication.getName())
                .claim(AUTHORITIES_KEY, member.getRole())
                .claim("platform", platform)
                .setExpiration(accessTokenExpiresIn)
                .compact();

        // Refresh Token 생성
        String refreshToken = Jwts.builder()
                .signWith(key, SignatureAlgorithm.HS256)
                .setHeaderParam("typ","JWT")
                .setSubject(member.getId().toString())
                .claim(EMAIL_KEY, authentication.getName())
                .claim(AUTHORITIES_KEY, member.getRole())
                .claim("platform", platform)
                .setExpiration(new Date(now + REFRESH_TOKEN_EXPIRE_TIME))
                .compact();


        return MemberTokenResponseDto.builder()
                .memberId(member.getId())
                .nickName(member.getNickName())
                .grantType(BEARER_TYPE)
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .refreshTokenExpirationTime(REFRESH_TOKEN_EXPIRE_TIME)
                .build();
    }

🤔 하지만 이방법에도 단점이 있다.

모든 디바이스별로 Sec-Ch-Ua-Platform 값을 분류할 수 있는 것이 아니다. 이와 관련된 프로젝트 Troble과 해결방법을 제시한 경험이 있다.👉 자세한 사항은 [ 1주차 Troble & Troubleshooting ] 에서 확인한다.

0개의 댓글