🔒 JWT 인증/인가

2025. 5. 1. 22:18·[내일배움캠프-Sparta]/Spring 6기
반응형

🧭 학습 목표

  1. JWT(Json Web Token)의 구조와 원리를 이해한다
  2. JWT를 Spring Security에서 적용하는 방법을 학습한다
  3. 쿠키(Cookie)를 통해 JWT를 안전하게 관리하는 방법을 습득한다
  4. JWT + Cookie 기반 인증을 적용할 수 있다
jwt 구조 보러가기

직접적 으로 담기는 정보는 payload에 담긴다.

1️⃣ 인증 방식 개요

토큰 기반 인증은 서버가 상태를 유지하지 않고 클라이언트가 JWT를 보관하여 요청마다 함께 전송

2️⃣ JWT란?

JWT는 JSON 기반의 토큰 포맷으로, HEADER / PAYLOAD / SIGNATURE 세 가지 부분으로 구성

SIGNATURE는 HMAC SHA256으로 서명된 값

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

PAYLOAD: {
  "sub": "userId",
  "role": "USER",
  "exp": 1714069142
}

3️⃣ JWT 사용 방식 

🔐 인증 흐름

  1. 로그인 시 사용자 정보를 검증하여 JWT 생성
  2. JWT를 응답에 담아 클라이언트에 전달 (보통 Cookie 또는 Header)
  3. 클라이언트는 요청 시 JWT를 전송하고 서버는 토큰을 검증하여 인증 처리

4️⃣ Cookie에 JWT 저장하는 이유

LocalStorage는 XSS공격에 취약하며 클라이언트의 JavaScript로 접근할 수 있음. 반면 HttpOnly옵션이 설정된 쿠키는 JavaScript로 접근할 수 없기 때문에 XSS에 강함. 다만 CSRF 공격에 대한 대비가 필요.

✅ Cookie 권장 설정

  • HttpOnly : true
  • Secure : true ( HTTPS 환경에서만 전송 )
  • SameSite : Strict 또는 Lax
  • 만료 시간 설정 필요 ( Access vs Refresh Token 전략에 따라 다르게 적용 )

5️⃣ JWT + Cookie 구조

❗ 구조

  1.  로그인 요청 시 Access Token ( 짧은 만료 시간 )과 Refresh Token ( 긴 만료 시간 )을 발급함.
  2. Access Token은 HttpOnly 쿠키로 클라이언트에 전달하고, Refresh Token은 쿠키 또는 DB에 저장함.
  3. 이후 요청 시 쿠키를 통해 Access Token이 자동 전송되며, 만료된 경우 Refresh Token으로 Access Token을 재발급함.

6️⃣ 코드 해석 ⭐

✅ JWT 생성 메소드

public String createToken(Long userId, String email, UserRole userRole) {
    Date now = new Date();
    return "Bearer " +
        Jwts.builder()
            .setSubject(String.valueOf(userId))
            .claim("email", email)
            .claim("userRole", userRole)
            .setIssuedAt(now)
            .setExpiration(new Date(now.getTime() + TOKEN_TIME))
            .signWith(key, SignatureAlgorithm.HS256)
            .compact();
}

코드 라인별 해석

public String createToken(Long userId, String email, UserRole userRole)

  • 사용자 ID, 이메일, 권한 정보를 입력받아 JWT 토큰 문자열을 반환하는 메서드입니다.
  • 매개변수는 해당 정보를 담고 싶다 라는 뜻이며 자율적으로 바뀔수 있다.

Date now = new Date();

  • 현재 시간 기준으로 토큰 발급 시각 및 만료 시각을 지정하기 위한 기준 시간 객체입니다.

return "Bearer " +

  • 반환되는 토큰 앞에 "Bearer " 접두사를 붙여, HTTP Authorization 헤더에서 사용하는 형식에 맞춥니다. ( prefix )

Jwts.builder()

  • JWT를 구성할 수 있는 빌더 객체를 생성합니다.

.setSubject(String.valueOf(userId))

  • 토큰의 subject 필드에 사용자 식별자(ID)를 문자열로 지정합니다. 주요 인증 기준 필드입니다.

.claim("email", email) 

  • setSubject()와 claim()은 암호화 하고자 하는 내용이 들어감 ⭐
  • Payload 영역에 이메일 정보를 커스텀 클레임으로 추가합니다.

.claim("userRole", userRole)

  • 사용자 역할(ADMIN, USER 등)을 추가하여 권한 확인에 사용됩니다.

.setIssuedAt(now)

  • iat 필드로 저장될 토큰 발급 시각을 지정합니다.

.setExpiration(new Date(now.getTime() + TOKEN_TIME))

  • exp 필드로 저장될 토큰 만료 시각을 설정합니다. 현재 시간 + 60분입니다.

.signWith(key, SignatureAlgorithm.HS256)

  • HMAC SHA256 알고리즘을 사용해 지정된 비밀 키로 서명합니다. JWT 위변조를 방지합니다.

.compact();

  • 구성한 Header, Payload, Signature를 합쳐 최종 JWT 문자열을 생성

JWT의 전반적인 구조(Subject, Claims, Expiration 등)를 구성하고, 클라이언트에 전달할 서명된 토큰을 생성하는 핵심 로직

✅ JWT 토큰 추출 및 파싱 메소드

public String substringToken(String tokenValue) {
    if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)){
        return tokenValue.substring(7);
    }
    throw new ServerException("Not Found Token");
}

public Claims extractClaims(String token) {
    return Jwts.parserBuilder()
            .setSigningKey(key)
            .build()
            .parseClaimsJws(token)
            .getBody();
}

코드 라인별 해석 (substringToken)

StringUtils.hasText(tokenValue)

  • 전달받은 문자열이 null이 아니고 공백이 아닌지를 검사합니다.

tokenValue.startsWith(BEARER_PREFIX)

  • "Bearer "로 시작하는지 확인하여 JWT 형식인지 검증합니다.

tokenValue.substring(7)

  • "Bearer " 이후의 실제 토큰 값을 추출합니다. (Bearer 는 7자)

throw new ServerException("Not Found Token")

  • 조건을 만족하지 않으면 사용자 정의 예외를 발생시켜 토큰이 없음을 알립니다.

코드 라인별 해석 (extractClaims)

Jwts.parserBuilder()

  • JWT 문자열을 해석하기 위한 파서 빌더를 생성합니다.

.setSigningKey(key)

  • JWT 생성 시 사용한 서명 키를 지정하여 서명 유효성 검사를 수행합니다.

.build().parseClaimsJws(token)

  • 실제 JWT 문자열을 파싱하고 유효성 검사를 수행합니다.

.getBody()

  • 파싱된 JWT의 Payload(Claims)를 반환합니다. 사용자 정보, 발급 시간, 만료 시간 등이 포함

✅ Filter 설정 클래스

@Configuration
@RequiredArgsConstructor
public class FilterConfig {

    private final JwtUtil jwtUtil;

    @Bean
    public FilterRegistrationBean<JwtFilter> jwtFilter() {
        FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new JwtFilter(jwtUtil));
        registrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴을 지정합니다.

        return registrationBean;
    }
}

코드 라인별 해석 (FilterConfig)

@Configuration

  • 해당 클래스가 스프링 설정 클래스임을 명시합니다. Bean 등록 등의 작업을 수행할 수 있습니다.

@RequiredArgsConstructor

  • final 키워드가 붙은 필드를 자동으로 생성자 주입합니다. 여기서는 JwtUtil이 주입됩니다.

private final JwtUtil jwtUtil;

  • JWT 검증 및 파싱 로직을 담고 있는 유틸 클래스를 필터에 주입하기 위해 선언합니다.

@Bean

  • 해당 메서드를 통해 반환된 객체를 Spring 컨테이너가 Bean으로 관리하도록 지정합니다.

public FilterRegistrationBean<JwtFilter> jwtFilter()

  • JWT 필터를 등록하는 메서드입니다. 필터 등록용 Spring 클래스인 FilterRegistrationBean을 반환합니다.

new FilterRegistrationBean<>();

  • 새 필터 등록 객체를 생성합니다.

registrationBean.setFilter(new JwtFilter(jwtUtil));

  • JWT 유틸을 주입하여 JwtFilter 인스턴스를 생성하고, 이를 필터로 등록합니다.

registrationBean.addUrlPatterns("/*");

  • 이 필터가 모든 요청 경로에 대해 작동하도록 설정합니다. 필요 시 특정 경로만 지정할 수도 있습니다.

return registrationBean;

  • 최종적으로 구성된 필터 등록 객체를 반환합니다. Spring이 이를 Bean으로 등록하고 필터 체인에 포함시킵니다.

이 클래스는 JWT 검증을 위한 필터(JwtFilter)를 전역으로 등록하기 위한 설정을 담당합니다. Spring Boot 내에서 인증 관련 필터를 적용할 때 주로 사용

✅ JWT 필터 전체 코드

@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String url = request.getRequestURI();

        if (url.startsWith("/auth")) {
            chain.doFilter(request, response);
            return;
        }

        String bearerJwt = request.getHeader("Authorization");

        if (bearerJwt == null) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다.");
            return;
        }

        String jwt = jwtUtil.substringToken(bearerJwt);

        try {
            Claims claims = jwtUtil.extractClaims(jwt);
            if (claims == null) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 JWT 토큰입니다.");
                return;
            }

            UserRole userRole = UserRole.valueOf(claims.get("userRole", String.class));

            request.setAttribute("userId", Long.parseLong(claims.getSubject()));
            request.setAttribute("email", claims.get("email"));
            request.setAttribute("userRole", claims.get("userRole"));

            if (url.startsWith("/admin")) {
                if (!UserRole.ADMIN.equals(userRole)) {
                    response.sendError(HttpServletResponse.SC_FORBIDDEN, "관리자 권한이 없습니다.");
                    return;
                }
                chain.doFilter(request, response);
                return;
            }

            chain.doFilter(request, response);
        } catch (Exception e) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "유효하지 않는 JWT 토큰입니다.");
        }
    }
}

코드 라인별 해석 (JwtFilter)

@Slf4j

  • 로그 출력을 위한 Lombok 어노테이션입니다. 예외 로그 등을 출력할 수 있습니다.

@RequiredArgsConstructor

  • final 필드를 자동 생성자 주입합니다. jwtUtil이 주입됩니다.

extends OncePerRequestFilter

  • Spring Security에서 제공하는 필터로, 요청당 한 번만 실행되도록 보장됩니다.

String url = request.getRequestURI();

  • 현재 요청된 URL 경로를 가져옵니다.

if (url.startsWith("/auth")) { ... }

  • 로그인, 회원가입 등 인증이 필요 없는 경로는 필터를 건너뜁니다.

String bearerJwt = request.getHeader("Authorization");

  • 요청 헤더에서 Authorization 값을 가져옵니다.

if (bearerJwt == null) { ... }

  • JWT가 없는 경우 400 에러를 반환하고 요청을 차단합니다.

String jwt = jwtUtil.substringToken(bearerJwt);

  • "Bearer " 접두사를 제거하고 실제 토큰 문자열만 추출합니다.

Claims claims = jwtUtil.extractClaims(jwt);

  • JWT의 유효성 검사를 수행하고, Payload에서 클레임 정보를 추출합니다.

if (claims == null) { ... }

  • 클레임이 비정상적일 경우 400 에러 반환

UserRole userRole = UserRole.valueOf(...)

  • JWT에 포함된 역할 정보를 열거형으로 변환합니다.

request.setAttribute(...)

  • 사용자 정보를 이후 컨트롤러에서 참조할 수 있도록 request 범위에 저장합니다.

if (url.startsWith("/admin"))

  • 요청 URL이 관리자 페이지라면 ADMIN 권한이 있는지 확인합니다.

if (!UserRole.ADMIN.equals(userRole))

  • 권한이 ADMIN이 아닐 경우 403 에러를 반환합니다.

chain.doFilter(request, response);

  • 필터를 통과했을 경우 다음 필터 또는 컨트롤러로 요청을 전달합니다.

catch (Exception e)

  • JWT 파싱 도중 발생할 수 있는 모든 예외를 잡아 400 에러로 반환합니다.

7️⃣ 보안 고려사항

  • JWT 서명 키는 외부에 노출되지 않도록 환경변수로 관리합니다.
  • Refresh Token은 DB에 저장하고 블랙리스트 관리를 고려합니다.
  • HTTPS 강제 및 CORS 설정을 적절히 구성합니다.
  • Access Token 재발급 시 기존 토큰을 무효화하는 로직을 고려합니다.

✅ 마무리 요약

JWT는 인증 정보를 자체적으로 담고 있으며,  서버가 상태를 유지할 필요가 없음.

Access Token은 인증 요청 시 사용되며 비교적 짧은 만료 시간을 갖습니다.

Refresh Token은 토큰 재발급에 사용되며 긴 만료 시간을 가집니다.

쿠키에 저장할 경우 HttpOnly와 Secure 옵션 설정을 통해 보안을 강화할 수 있습니다.

반응형

'[내일배움캠프-Sparta] > Spring 6기' 카테고리의 다른 글

TIL 50 [ JPA 심화 - ( 테이블 객체 다루는법, JpaRepository 쿼리,페이징처리, JPQL ,QueryDSL, Auditing, 필요한부분 갱신 ) , 트랜잭션 복습 ]  (0) 2025.05.02
🚧 트랜잭션 (스탠다드)  (0) 2025.05.02
TIL 49 [ JPA 심화 - ( MyBatis, ORM, 테이블 객체, Raw JPA 연관관계) ]  (0) 2025.05.01
TIL 48 [ JPA 심화 ( 의존성, DB, SQL, Driver, JDBC ) ]  (0) 2025.04.30
TIL 47 [ Spring 심화 프로젝트 ( D-day ) - 발표/마무리, 트러블 슈팅 ]  (0) 2025.04.29
'[내일배움캠프-Sparta]/Spring 6기' 카테고리의 다른 글
  • TIL 50 [ JPA 심화 - ( 테이블 객체 다루는법, JpaRepository 쿼리,페이징처리, JPQL ,QueryDSL, Auditing, 필요한부분 갱신 ) , 트랜잭션 복습 ]
  • 🚧 트랜잭션 (스탠다드)
  • TIL 49 [ JPA 심화 - ( MyBatis, ORM, 테이블 객체, Raw JPA 연관관계) ]
  • TIL 48 [ JPA 심화 ( 의존성, DB, SQL, Driver, JDBC ) ]
dimenshun
dimenshun
한 소년의 개발 일기
    반응형
  • dimenshun
    Dev Life Notes
    dimenshun
  • 전체
    오늘
    어제
    • 분류 전체보기 (268)
      • CS (23)
        • 자료구조 (0)
        • 알고리즘 (0)
        • 컴퓨터 구조 (8)
        • 네트워크 (6)
        • 운영체제 (3)
        • DB ( + SQLD ) (5)
        • SW공학 (1)
      • 프로그래밍 (3)
        • Java (0)
        • Spring (0)
        • HTML,CSS (3)
        • JavaScript (0)
      • 개발 툴 (7)
        • Git(버전관리) (1)
        • Docker (3)
        • AWS (2)
        • JSP (1)
      • 코딩테스트(Algorithm) (125)
        • 백준 (6)
        • 프로그래머스 (119)
      • [내일배움캠프-Sparta] (110)
        • Spring 6기 (106)
        • KPT 회고 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Til
    db
    spring
    배포
    내일배움캠프
    network
    Testcode
    web
    컴퓨터구조
    네트워크
    docker
    Python
    It
    세션
    알고리즘
    SQLD
    cs
    운영체제
    트랜잭션
    메모리
    개발자
    SQL
    코딩테스트
    Java
    OS
    웹
    KPT
    AWS
    백엔드
    CPU
  • hELLO· Designed By정상우.v4.10.3
dimenshun
🔒 JWT 인증/인가
상단으로

티스토리툴바