📢 여러 디바이스에서 동시 로그인이 가능한 경우, accesstoken 과 refreshtoken 값이 디바이스 별로 달라야 한다.
[ 다중 디바이스 동시 로그인과 JWT TOKEN (1) ] 에서 이미 클라이언트의 정보를 읽어 JWT 토큰값을 달리 주는 법을 고민했다. 다만, 처음 로그인할 때 클라이언트에서 http헤더에 string값을 넣는 것 보다 더 좋은 방법이 없을까를 고민해 보았다.
아래는 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값에 추가하여 다중기기 로그인을 구현하였다.
@PostMapping("/login")
public ResponseEntity<MemberTokenResponseDto> login(@RequestBody @Validated MemberLoginRequestDto memberLoginRequestDto, HttpServletRequest request) {
MemberTokenResponseDto response = memberService.login(memberLoginRequestDto, request);
return ResponseEntity.ok(response);
}
- 웹브라우저 사용자인 클라이언트로부터 서버로 요청이 들어오면
서버에서는 HttpServletRequest를 생성하며, 요청정보에 있는 패스로 매핑된 서블릿에게 전달- 전달받은 내용들을 파라미터로 Get과 Post 형식으로 클라이언트에게 전달
- http프로토콜의 request정보를 서블릿에게 전달하기 위해 사용
- 헤더정보, 파라미터, 쿠키, URI, URL 등의 정보를 읽어 들이는 메소드 포함
- Body의 Stream을 읽어 들이는 메소드 포함
@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;
}
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 ] 에서 확인한다.