[Project] 2. Access, Refresh Token, Redis (로그아웃)

Hayoon·2022년 8월 28일
0

Spring 정리

목록 보기
5/11

앞선 [Project] 1장에서 JWT토큰에 대해서 설명하였다. 단순히 JWT토큰만 발급받으면 로그인/아웃이 되는 구조로 생각을 했었지만, 그리 간단하지 않았다.
Access, Refresh 토큰을 통해 로그인 상태일 경우 서비스 Api를 접근할 때 유효성검사를 통해 회원은 검증을 받아야 한다.

Access Token을 사용하는 이유

서버가 액세스 토큰을 클라이언트에게 주면 클라이언트는 매 요청시 Access Token을 서버로 보내주어 로그인 상태을 알려준다.이러한 방식은 HTTP의 무상태 특성을 보완하기 위한 한 가지 방법이지만 Access Token을 주는 방식은 전달 과정에서 탈취 당할 우려가 있어 보안에 문제가 있다.이를 해결하기 위해 토큰에 만료기간을 주어, 만약 탈취를 당하더라도 시간이 지나면 토큰을 사용할 수 없게 만들 수 있다. 하지만 이는 로그인 상태가 주기적으로 풀린다는 뜻이고 사용자에게 큰 불편을 줄 것이다.

  1. 로그인하면 access 토큰과 refresh 토큰을 발급 받는다. refresh 토큰은 redis에 저장된다.
  2. 요청을 보낼 때마다 헤더(X-AUTH-TOKEN)에 액세스 토큰을 담아서 보낸다.
  3. Access 토큰이 만료(expired)되었으면, Access 토큰과 Refresh 토큰을 함께 보내서 토큰 재발급을 요청한다.
  4. 기간만 만료된 유효한 access 토큰이고, redis에 저장된 refresh 토큰과 같으면서 유효한 refresh 토큰이면, 1번 과정처럼 aceess 토큰과 refresh 토큰을 재발급 받는다.
  5. 유효하지않은 refresh 토큰이라면, 재로그인 요청을 받는다.
//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;
    }

Login Api Controller

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);
    }

access 토큰 재발급

토큰 재발급
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

profile
Junior Developer

0개의 댓글