앞선 [Project] 1장에서 JWT토큰에 대해서 설명하였다. 단순히 JWT토큰만 발급받으면 로그인/아웃이 되는 구조로 생각을 했었지만, 그리 간단하지 않았다.
Access, Refresh 토큰을 통해 로그인 상태일 경우 서비스 Api를 접근할 때 유효성검사를 통해 회원은 검증을 받아야 한다.
서버가 액세스 토큰을 클라이언트에게 주면 클라이언트는 매 요청시 Access Token을 서버로 보내주어 로그인 상태을 알려준다.이러한 방식은 HTTP의 무상태 특성을 보완하기 위한 한 가지 방법이지만 Access Token을 주는 방식은 전달 과정에서 탈취 당할 우려가 있어 보안에 문제가 있다.이를 해결하기 위해 토큰에 만료기간을 주어, 만약 탈취를 당하더라도 시간이 지나면 토큰을 사용할 수 없게 만들 수 있다. 하지만 이는 로그인 상태가 주기적으로 풀린다는 뜻이고 사용자에게 큰 불편을 줄 것이다.
//JwtTokenProvider
public String createToken(String memberPk, List<String> roles, Long tokenValidTime) {
Claims claims = Jwts.claims().setSubject(memberPk); //JWT payload 에 저장되는 정보단위
claims.put("roles", roles); //<key, Value> 쌍으로 저장
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + tokenValidTime)) //set Expire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과 signature 에 들어갈 secret 값 세팅
.compact();
}
// access token 생성
public String createAccessToken(String account, List<String> userRole) {
Long tokenInvalidTime = 1000L * 60 * 30; // Hayoon 30분
return this.createToken(account, userRole, tokenInvalidTime);
}
// refresh token 생성
public String createRefreshToken(String account, List<String> userRole) {
Long tokenInvalidTime = 1000L * 60 * 60 * 24 ; // Hayoon 1일
String refreshToken = this.createToken(account, userRole, tokenInvalidTime);
redisService.setValues(account, refreshToken, Duration.ofMillis(tokenInvalidTime));
return refreshToken;
}
userLoginDto에 Id, Password를 입력한 responseBody를 받아 DB조회 후 password가 일치할 경우
access, refresh 토큰을 발급한다.
@PostMapping("/api/v5/login")
public LoginRepositoryDto loginV5(@RequestBody @Valid UserLoginDto userLoginDto) {
Member member = memberLoginRepository.findByAccount(userLoginDto.getAccount())
.orElseThrow(() -> new IllegalArgumentException("가입되지 않은 ACCOUNT 입니다."));
if(!passwordEncoder.matches(userLoginDto.getPassword(), member.getPassword()))
throw new IllegalArgumentException("잘못된 비밀번호입니다.");
String accessToken = jwtTokenProvider.createAccessToken(member.getAccount(), member.getRoles());
String refreshToken = jwtTokenProvider.createRefreshToken(member.getAccount(), member.getRoles());
return new LoginRepositoryDto(accessToken, refreshToken);
}
토큰 재발급
1. 전달받은 유저의 아이디로 유저가 존재하는지 확인한다.
2. RefreshToken이 유효한지 체크한다.
3. AccessToken을 발급하여 기존 RefreshToken과 함께 응답한다.
public LoginRepositoryDto reIssueAccessToken(String account, String refreshToken) {
Member member = memberLoginRepository.findByAccount(account)
.orElseThrow(() -> new IllegalStateException("존재하지 않는 유저입니다."));
String accessToken = jwtTokenProvider.createAccessToken(member.getAccount(), member.getRoles());
return new LoginRepositoryDto(accessToken, refreshToken);
}
//토큰의 유효성 + 만료일자 확인
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
ValueOperations<String, String> logoutValueOperations = redisTemplate.opsForValue();
if(logoutValueOperations.get(jwtToken) != null) {
log.info("로그아웃 된 토큰입니다.");
return false;
}
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
access 토큰 만료시간을 체크 후, redis에 (blacklist) + accessToken, 계정, 만료기간을이 담긴 ValueOperation을 만들어 redis에 저장한다. redis에서 유저 refreshtoken 값을 삭제한다.
public void logout(String account, String accessToken) {
long expiredAccessTokenTime = getExpiredTime(accessToken).getTime() - new Date().getTime();
redisService.setValues(blackList + accessToken, account, Duration.ofMillis(expiredAccessTokenTime));
redisService.deleteValues(account); // Redis에서 유저 리프레시 토큰 삭제
}
(redis에 대한 내용과 redis + Spring boot + AWS EC2 연동은 다음 글에서 이어서 하겠습니다.)
출처: https://daco2020.tistory.com/303
https://kukekyakya.tistory.com/entry