inblog logo
|
LifeLog, DevLog
    SpringWeb

    인증/인가, 세션과 JWT + JWT 실습

    개념 정리, JWT 실습 해보기
    KYJTHEYJ's avatar
    KYJTHEYJ
    Jan 22, 2026
    인증/인가, 세션과 JWT + JWT 실습
    Contents
    인증/인가세션과 JWT세션JWT (JSON Web Token)JWT의 구성요소JWT 사용시 필요한 것적용해보기Cookie 설정

    인증/인가

    인증 → 사용자가 누구인지 확인하는 절차

    인가 → 인증된 사용자가 어떠한 리소스나 기능에 접근할 권한이 있는지 확인하는 절차

    세션과 JWT

    인증을 유지하는 방법은 여러가지가 있지만, 지금은 크게 2개가 쓰이는 것 같다

    세션

    정보가 유효하면 인증 정보를 Stateful 로 관리 한다 (서버에 저장)
    클라이언트에게 인증 정보를 보내 활용한다
    전체적인 흐름은 아래와 같다

    로그인 시도 → 서버에서 로그인 정보 검증 (DB 등)

    → 세션 정보를 생성하고 서버에서 저장함

    → 클라이언트에게 쿠키 등으로 전달

    → 클라이언트에서 받은 세션 ID로 요청 → 세션 저장된 정보를 조회

    → 인증 성공

    모든 세션 정보를 서버에 저장해서 관리하므로 클라이언트를 강제로 로그아웃 하는
    등으로 클라이언트를 제어할 수 있다

    세션 ID로는 민감한 정보가 없으므로 쿠키에 저장해도
    클라이언트 측에 정보들이 남지 않는다

    다만 서버에 저장하므로 부하가 있다

    JWT (JSON Web Token)

    정보가 유효하면 암호화된 액세스 토큰을 만들어 클라이언트에게 보내고
    서버는 그 토큰을 저장하지 않는다 (Stateless로 관리)
    그 후 클라이언트는 토큰을 전송하면 서버가 토큰만 검증하여 응답한다
    이 토큰에는 만료시간부터 식별까지 가능한 값들이 담겨 있다
    전체적인 흐름은 아래와 같다

    로그인 시도 → 서버에서 로그인 정보 검증 (DB등)

    → JWT 토큰을 생성하고 저장하지 않는다

    → 클라이언트에게 로컬 스토리지 등으로 전달

    → 클라이언트는 받은 토큰을 전송하여 요청 → 토큰을 검증한다

    → 올바른 토큰이면 인증에 성공

    서버에 저장하지 않으니 서버의 부하가 없고 확장하여 사용해도 무리가 없다

    다만 탈취 당할 경우 만료되기 전까지 식별이 가능한 정보가 담겨 있어 문제가 된다

    💡

    큰 차이는 상태 관리의 방법, 세션은 Stateful, JWT는 Stateless

    JWT의 구성요소

    jwt.iotoken.dev

    JWT는 . 으로 구분된 세 부분으로 구성된다

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

    Header

    토큰의 유형, 서명을 생성할 때 사용할 알고리즘 등의 정보가 담겨 있다

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

    Payload **

    토큰에 담을 실제 정보 (Claim) 가 들어있다
    클레임에는 사용자의 정보, 토큰의 속성 등이 담겨 있다

    이 부분은 B

    {
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true,
      "iat": 1516239022
    }

    💡

    중요한 점은 헤더와 페이로드의 클레임들은
    Base64로 인코딩 되어 있을 뿐
    암호화 되지 않은 부분이므로 민감한 정보를 담으면 안된다

    이 부분이 잘 설계 되어 있어야 탈취에도 대비할 수 있다

    signature **

    JWT의 무결성을 보장하는 가장 중요한 부분

    헤더와 페이로드를 인코딩한 값과 서버만 지닌 비밀 키를
    헤더에 지정된 알고리즘 으로 암호화하여 생성한다

    그럼 서버는 클라이언트로부터 받은 토큰의 헤더, 페이로드를 동일한 비밀 키와
    알고리즘으로 암호화하여 다시 서명 값을 만들고, 이 값이 기존 서명값과 일치하는지
    확인 하는 절차를 한번 더 거쳐 위변조를 판단
    한다

    Refresh, Access Token

    JWT를 사용하면서 유효기간에 딜레마가 생기는데
    유효기간이 짧으면 → 보안은 좋으나 자주 로그인, UX 경험이 하락
    유효기간이 길면 → 보안에 불리, 악용여지 높아짐

    그래서 이 두 토큰을 같이 사용하여 전략을 취한다

    Access Token 은 실제 자원에 접근할 때 사용, 위에서 명시한 JWT (유효기간이 짧음)

    Refresh Token 은 새 Access Token을 발급 받기 위해 사용 (유효기간을 길게 설정)
    평소엔 사용 안하지만 Access Token 만료시 사용
    그런데 이 토큰은 보통 따로 저장을 해서 관리하므로 Stateful 적임

    💡

    하이브리드 적인 방식을 취하고 있다는 것이 결론
    실제로 뭔가 해봐야 이해가 더 갈 것 같다

    JWT 사용시 필요한 것

    • 어떤 암호화 알고리즘을 사용할지

    • 어떤 정보를 넣을 것인지

    • 어떤 암호키로 암호화할지

    • 만료 시간

    적용해보기

    우선 테스트를 위해 Postman을 사용할 것이다

    관련 코드나 개념의 코드 작성과 템플릿은
    https://github.com/jwtk/jjwt
    위 링크의 공식 문서 + 여러 자료 짜집기 + 클로드 검수 or 수동 검수로 진행한다

    테스트 베이스는 기존에 사용한 토이 프로젝트가 있어서 활용하려한다
    간단한 로그인과 조회, 데이터 생성이 가능한 API가 구현된 프로젝트이다

    페이로드는.. 대충 담았다

    토큰 생성하기

    생성의 규칙은 공식 문서에 있는 방법을 활용한다

    • 생성 관련 코드

    @Slf4j
    @Component
    public class JwtUtil {
        // 기본 설정
        public static final String BEARER_PREFIX = "Bearer "; // "이 토큰을 가진 자가 인증의 주체다" 를 명시하는 스킴, 이 스킴 이름을 보고 HTTP가 인증 처리 방식을 결정함
        private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
    
        // 설정한 비밀키를 가져오기
        @Value("${jwt.secret.key}"
        private String secretKeyString;
    
        private SecretKey key;
        private JwtParser parser;
    
        // 서버가 실행시 가장 먼저 생성할 것을 명시 하는 어노테이션 @PostConsturct
        @PostConstruct
        public void init() {
            byte[] bytes = Decoders.BASE64.decode(secretKeyString);
            this.key = Keys.hmacShaKeyFor(bytes); // 설정된 비밀키의 바이트 배열을 전달 해주면 HMAC-SHA 알고리즘을 통해 SecretKey 타입의 비밀키를 만들어 준다
            this.parser = Jwts.parser()
                    .verifyWith(this.key)
                    .build();
        }
    
        // 토큰 생성
        public String generateToken(String adminName, Long adminId) {
            Date now = new Date();
            return BEARER_PREFIX + Jwts.builder()
                    .claim("adminName", adminName)
                    .claim("adminId", adminId)
                    .issuedAt(now)
                    .expiration(new Date(now.getTime() + TOKEN_TIME))
                    .signWith(key, Jwts.SIG.HS256)
                    .compact();
        }
    	
    	....
    
    }

    생성 설정 순서

    1. 먼저 비밀키는 yaml 에 생성해 두었다 (@Value 부분)

    1. Bearer 인증을 사용할 것을 먼저 명시 한다
      표현이 그렇지만 Stateless 인 JWT 의 최종 방어선인 만료 시간을 설정한다
      (BEARER_PREFIX, TOKEN TIME 부분)

    1. 서버가 실행시, 사용할 SecretKey 와 해독에 필요한 Parser를 설정한다
      Parser는 생성된 SecretKey 로 증빙할 것을 verifyWith 으로 명시한다

    1. 토큰을 생성을 위해 Jwt.builder() 를 사용하는데
      클레임 (항목들) 과 생성일자 (issuedAt)
      , 만료 시간 (expiration), 암호화 알고리즘과 비밀키로
      서명한 후 (signWith) String 으로 압축 (compact) 한다

    2. 이렇게 압축된 String (서명된 JWT는 JWS 라고 총칭한다) 이 생성되면 완료

    💡

    Bearer ?

    Bearer 는 이 토큰을 가진 자가 인증의 주체임을 명시하는 스킴
    HTTP 는 키워드에 따라 인증 처리 방식을 결정 한다
    Bearer 로 명시 했으니 HTTP 가 Bearer 인증을 수행한다
    자세한 사항은 더 조사를 해야 겠지면 OAuth 2.0에 관련되어 있다

    기존 토이 프로젝트의 로그인 부를 조금 고쳐서 적용해보았다

    해당 JWS를 갖고 아래 token.dev 에서
    Signing key, yaml에 설정한 비밀키를 입력하면 유효한지 알수 있다
    생성 테스트로 JWS가 올바르게 생성 되었음을 알 수 있다

    생성 완료 후 비밀키와 유효한지 확인 -> verifed

    토큰 검증하기

    토큰 생성이 올바르게 작동하는 것을 확인 하였으니,
    로그인하여 기능을 사용할 때, 토큰에 검증의 단계를 섞어보자

    개발의 인프라 환경에 따라 다르겠지만,
    고민이 되는게 검증은 페이로드엔 민감한 정보를 담지 못하니
    내 환경에선 당연히 DB 에 저장된 값을 가져오는 단계가 필요하다

    토이 프로젝트 구성상 로그인된 사용자의 정보를 가져와야해서
    책임 소지가 불분명해지고 Stateless 가 퇴색되지만
    예시를 들고 적용해 보는 것에 여기선 의의를 둔다

    실무에선 인증서버를 따로 두고 로그인 후 에서 실 서버내에
    사용할 키 값을 따로 주어 활용하는 등 (OAuth2) 의
    방법이 있고, UUID로 처리하는 등의 방법이 있는 것 같은데, 추후 포스팅한다

    @Slf4j
    @Component
    @RequiredArgsConstructor
    public class JwtUtil {
        // 관리자의 정보 조회용
        private final AdminService adminService;
    
        // 기본 설정
        public static final String BEARER_PREFIX = "Bearer "; // "이 토큰을 가진 자가 인증의 주체다" 를 명시하는 스킴, 이 스킴 이름을 보고 HTTP가 인증 처리 방식을 결정함
        private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
    
        // 설정한 비밀키를 가져오기
        @Value("${jwt.secret.key}") // application.yml 에 있는 key 가져옴
        private String secretKeyString;
    
        private SecretKey key;
        private JwtParser parser;
    
        // 서버가 실행시 가장 먼저 생성할 것을 명시 하는 어노테이션 @PostConsturct
        @PostConstruct
        public void init() {
            byte[] bytes = Decoders.BASE64.decode(secretKeyString);
            this.key = Keys.hmacShaKeyFor(bytes); // 설정된 비밀키의 바이트 배열을 전달 해주면 HMAC-SHA 알고리즘을 통해 SecretKey 타입의 비밀키를 만들어 준다
            this.parser = Jwts.parser()
                    .verifyWith(this.key)
                    .build();
        }
    
        // 토큰 생성
        public String generateToken(String adminEmail) {
            Date now = new Date();
            return BEARER_PREFIX + Jwts.builder()
                    .claim("adminEmail", adminEmail)
                    .issuedAt(now)
                    .expiration(new Date(now.getTime() + TOKEN_TIME))
                    .signWith(key, Jwts.SIG.HS256)
                    .compact();
        }
    
        // 토큰 검증
        public boolean validateToken(String token) {
            if (token == null || token.isBlank()) return false; // 토큰이 비었는지 검사
            try {
                parser.parseSignedClaims(token); // 토큰이 서명키로 파싱, 불가시 exception
                return true;
            } catch (JwtException | IllegalArgumentException e) {
                // 예외처리를 상세히 하지 않는 것이 공격자에게 보안적으로 좋다 (단순한 false 구분값 return)
                // 내부 로그엔 모든 JWT 관련 오류인 JwtException 으로 로깅
                log.debug("Invalid JWT: {}", e.toString());
                return false;
            }
        }
    
    ...
    
    }

    검증 설정 순서

    1. 먼저 Header 에 담긴 토큰이 비었는지 검사한다

    2. 서명키로 파싱이 되는지 체크하여 가능 하면 우선 true

    3. 파싱이 안되면 토큰에 문제가 발생한 것, 이 때 예외처리를 친절히 해주지 않아야
      공격자에게 공격 루트를 드러내지 않을 수 있다
      대신 로깅은 잘 해놔야 함은 당연하다

    4. 이제 후속 기능에 접근할 수 있게 처리한다

    생성한 토큰을 보내 검증을 시키고 결과를 받았다

    검증 결과를 받았다, 틀리게 하면 단순히 401 에러코드 반환 처리로 대응하도록 구성

    토큰이 이상하다 -> 401 반환
    로그는 자세히

    Cookie 설정

    이제 발행한 토큰을 클라이언트 브라우저에서 활용하도록 쿠키에도 담아보자

    Cookie cookie = new Cookie("token", token);
    cookie.setHttpOnly(true);   // JS 진입 방지
    cookie.setSecure(false);    // 운영에선 true, secure 만 접근되도록
    cookie.setPath("/");        // 모든 경로에서 사용하도록 설정
    cookie.setMaxAge(3600);     // 쿠키의 만료 시간
    response.addCookie(cookie);
    
    // 같은 코드인데 스프링 프레임워크 5.0 이상 지원할 경우엔
    // ResponseCookie 가 인코딩 처리도 해주므로 사용하는게 좋을 듯       
    ResponseCookie cookie = ResponseCookie.from("token", token)
                    .httpOnly(true)
                    .secure(false)
                    .path("/")
                    .maxAge(3600)
                    .build();
    
    response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
            

    이후엔 필터 에서 토큰에 대해 검사하도록 처리하게 해주면 우선 끝이다
    (현재는 테스트용으로 JSON 반환에서 토큰은 빼야한다)

    Share article
    Contents
    인증/인가세션과 JWT세션JWT (JSON Web Token)JWT의 구성요소JWT 사용시 필요한 것적용해보기Cookie 설정

    LifeLog, DevLog - https://github.com/KYJTHEYJ

    RSS·Powered by Inblog