본문 바로가기
Web/Spring

[Spring/Java] JWT 토큰 사용하는 이유 및 구현 방법 (Feat. 세션/쿠키)

by char_lie 2023. 11. 23.
반응형

프로젝트를 진행하는 과정에서 로그인 토큰 인증 방식을.사용하기 위해 JWT 토큰 인증 방식을 도입했다.

로그인 진행시 필요한 정보(id 등)를 담아서 요청할 필요가 있었다. 이를 원활하게 사용하기 위해 토큰에 인증 정보를 담아 전달하는 방식을 사용하는 JWT를 이용하는 방식이 이후 로직 처리에 있어 편리하게 동작할 것이라고 생각하여 세션과 쿠키 방식이 아닌 토큰을 통한 암화하는 방식을 이용했다.

특히, 정보를 담고 있는 토큰이 있으면 요청, 응답 시마다 DB를 확인할 필요가 없이 토큰에 연결된 저장 정보만으로 보여줄 수있다는 장점와 MSA구조에서는 JWT 사용 시 부하를 감소 시킬 수 있기에 채택했다.


 

Cookie

Cookie란?

클라이언트가 웹 사이트에 접속할 때 그 사이트가 사용하고 있는 서버에서 사용자의 컴퓨터에 저장하는 작은 기록 정보 파일

 

특징

  1. 이름, 값, 만료일, 경로 정보로 구성
  2. 하나의 쿠키의 용량은 4KB로 제한
  3. 클라이언트에 총 300개의 쿠키를 저장할 수 있고, 하나의 도메인 당 20개의 쿠키를 가질 수 있음
  4. 여러 보안 옵션을 사용할 수 있음

동작 순서

  1. 클라이언트가 페이지를 요청
  2. 웹 서버에서 쿠키 생성
  3. 생성한 쿠키에 정보를 담아 HTTP 화면을 돌려줄 때, 같은 클라이언트에 반환
  4. 넘겨 받은 쿠키는 클라이언트에서 갖고 있다가 서버 요청시 쿠키 전송
  5. 동일 사이트 재방문시 클라이언트의 PC에 해당 쿠키가 있는 경우 요청 페이지와 함께 쿠키 전송

 

Session

Session이란?

일정 시간동안 같은 사용자로부터 들어오는 일련의 요구를 하나의 상태로 보고, 그 상태를 일정하게 유지시키는 기술

특징

  1. 웹 서버에 웹컨테이너의 상태를 유지하기 위한 정보를 저장
  2. 저장 데이터 제한이 없음
  3. 각 클라이언트 고유 세션 ID를 부여하여 클라이언트 별 요구에 맞는 서비스 제공 가능

동작 순서

  1. 사용자의 웹사이트 접근
  2. 서버에서 접근한 클라이언트의 요청에 있는 Cookie
  3. 세션ID가 없으면 서버에서 세션ID를 생성해 클라이언트에 반환
  4. 서버에서 클라이언트로 돌려준 세션ID를 쿠키를 이용하여 서버에 저장
  5. 클라이언트는 재접속 시 쿠키에 저장된 세션ID 값을 서버에 전달
JWT(Json Web Token)

JWT란?

인증에 필요한 정보들을 토큰에 담아 암호화시켜 사용하는 것으로 Header, Payload, signature 로 구성
각각의 구성요소가 Header.Payload.signature의 형태로 구성된 토큰으로 만들어짐.

 

JWT 구성 형태

1. Header

{
  "alg": "HS256",
  "typ": "JWT"
}

 

Header에는 보통 토큰의 타입이나 서명 생성에 어떤 알고리즘이 사용되었는지 저장함.

위 예시의 경우 JWT 타입의 HS256 알고리즘으로 암호화

 

2. Payload

{
  "sub": "1", // 토큰 제목(식별 값)
  "aud": "John", // 토큰 대상자
  "iat": 1516239022 // 토큰 발급 시간
}

Payload에는 토큰에 포함될 클레임 정보를 정의함.

표준 스펙상 key의 이름은 3글자로 지정되어있고, 각각에 대해 정의함.

 

3. Signature

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

Header와 Payload를 결합한 문자열에서 서버에서 사용하는 비밀 키로 생성한 서명

 

특징

  • 이미 토큰 자체가 인증된 정보기에 세션 저장소와 같은 별도 저장소가 필수적이지 않음
  • DB에 계쏙 요청을 보내야하는 세션과 달리 요청/응답 시 다시 DB에 접속할 필요가 없음 
  • 보안 이슈로 토큰을 훔칠 경우를 방지하여 => RefreshToken을 도입
  • AccessToken의 만료기간을 짧게하고 RefreshToken을 재발급하는 형태로 만들어 보안이슈 해결 가능

인증 과정

 

코드

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import java.util.Date;

import static com.ssafy.global.common.status.FailCode.UNMATCHED_TOKEN;

@Service
@RequiredArgsConstructor
@Data
public class JwtProvider {

    private MemberRepository memberRepository;

    @Value("${jwt.secretKey}")
    private String secretKey;

    @Value("${jwt.access.expiration}")
    private Long accessTokenExpirationPeriod;

    @Value("${jwt.refresh.expiration}")
    private Long refreshTokenExpirationPeriod;

    @Value("${jwt.access.header}")
    private String accessHeader;

    @Value("${jwt.refresh.header}")
    private String refreshHeader;

    private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
    private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
    private static final String ID_CLAIM = "id";
    private static final String ROLE_CLAIM = "role";
    private static final String BEARER = "Bearer ";

    public TokenMapping createToken(Long id, String role,String name){
        return TokenMapping.builder()
                .accessToken(createAccessToken(id, role))
                .refreshToken(createRefreshToken())
                .userName(name)
                .build();
    }

    public String createAccessToken(Long id, String role){
        Date now = new Date();
        return JWT.create()
                .withSubject(ACCESS_TOKEN_SUBJECT)
                .withExpiresAt(new Date(now.getTime() + accessTokenExpirationPeriod))
                .withClaim(ID_CLAIM, id)
                .withClaim(ROLE_CLAIM, role)
                .sign(Algorithm.HMAC512(secretKey));
    }

    public String createRefreshToken(){
        Date now = new Date();
        return JWT.create()
                .withSubject(REFRESH_TOKEN_SUBJECT)
                .withExpiresAt(new Date(now.getTime() + refreshTokenExpirationPeriod))
                .sign(Algorithm.HMAC512(secretKey));
    }

    public Long getExpiration(String accessToken){
        Date expiration = JWT.require(Algorithm.HMAC512(secretKey)).build().verify(accessToken).getExpiresAt();
        long now = new Date().getTime();
        return expiration.getTime() - now;
    }

    public Long AccessTokenDecoder(String accessToken) {
        accessToken = accessToken.replace("Bearer ", "");
        DecodedJWT jwt = JWT.decode(accessToken);
        Claim IDCLAIM = jwt.getClaim(ID_CLAIM);
        if (!IDCLAIM.isNull()) {
            return IDCLAIM.asLong();
        } else {
            throw new GlobalRuntimeException(UNMATCHED_TOKEN);
        }
    }
}

 

우선 JWT 토큰을 사용하기 위한 의존성 주입 후에 위처럼 코드를 작성해줬다.

각각의 역할은 토큰에 ID값과 Role값을 클레임으로 넣어 AccessToken과 RefreshToken을 만들 수 있게 만들어줬고, 유효 시간 설정을 할 수 있게 만들었다.

또한, 액세스 토큰에 저장된 ID를 꺼내서 쓸 수 있는 함수도 만들어서 사용하였다.

다른 엔티티 값이 필요하면 그것을 넣어 암호화 한 형태로 만들 수 있다.

// application.yml
jwt:
  secretKey: ENC(7i9pXf0/gVyTyZOAh8gDVg==)

  access:
    expiration: 3600000
    header: Authorization

  refresh:
    expiration: 1209600000
    header: Authorization-refresh

필요한 시간, 헤더 값에 대해서는 yml에서 관리할 수 있도록 설정하여 사용하였고, 이를 사용하기 위해서 서비스 단에서 로그인 할경우 발급된 토큰을 반환하도록 구성했다.

프로젝트에 사용할 때는 역할에 따른 분류가 필요해서 토큰에 역할을 넣어 JWT를 생성해서 반환했다.

@Service
public class UserServiceImpl implements UserRepository{
    private final MemberRepository memberRepository;
    private final JwtProvider jwtProvider;

    @Autowired
    public UserServiceImpl(MemberRepository memberRepository, JwtProvider jwtProvider) {
        this.memberRepository = memberRepository;
        this.jwtProvider = jwtProvider;
    }

    @Override
    public TokenMapping login(String email, String password) {
        MemberEntity member = memberRepository.findByEmail(email)
                .orElseThrow(() -> new GlobalRuntimeException(UNSIGNED_USER));

        if (!PasswordEncoder.checkPass(password, member.getPassword())) {
            throw new GlobalRuntimeException(DIFFERENT_PASSWORD);
        }

        if (member.isDeleted()) {
            throw new GlobalRuntimeException(DELETED_USER);
        }
        
        String role = member.getRole().name();
        TokenMapping tokenMapping = jwtProvider.createToken(member.getId(), role, member.getName());
        redisService.saveToken(member.getId().toString(), tokenMapping.getAccessToken());
        redisService.saveToken("refresh_" + email, tokenMapping.getRefreshToken());
        return tokenMapping;
    }

 

이를 통해 최종적으로 JWT를 구현할 수 있었고, 사용할 수 있었다.

반응형

댓글