본문 바로가기

Spring

[Spring] 쿠키 / 세션 / 토큰

웹은 기본적으로 Stateless하기 때문에 사용자의 상태를 서버가 기억하려면 별도 저장소 필요하다. 이를 위한 방식으로 쿠키와 세션, 토큰이 있다.

 

쿠키 (Cookie)

  • 웹 브라우저(클라이언트 측)에 저장되는 데이터
  • 사용자의 방문 기록, 로그인 상태 유지, 개인 맞춤 설정 등을 저장하여 HTTP의 Stateless 특성을 극복
  • 클라이언트 측에 저장되므로, 서버에 요청을 보낼 때마다 포함되어 전송
  • 만료 날짜를 설정할 수 있으며, 세션 쿠키: 브라우저 종료 시 삭제
                                                  영속 쿠키: 지정된 기간 동안 유지
  • 주의할 점: 클라이언트에 저장되므로 민감한 정보는 직접 담지 않도록 주의 (예: 비밀번호 X)
// 쿠키에 값 저장
Cookie cookie = new Cookie("userId", "abc123");
cookie.setMaxAge(60 * 60); // 1시간 유지
httpServletResponse.addCookie(cookie);

// 쿠키에서 값 꺼내기
Cookie[] cookies = httpServletRequest.getCookies();
if (cookies != null) {
    for (Cookie c : cookies) {
        if (c.getName().equals("userId")) {
            String userId = c.getValue();
        }
    }
}

 

세션 (Session)

  • 사용자와 서버 간의 상태를 유지하기 위한 방법
  • 로그인 정보, 사용자 활동 등을 서버 측에서 관리
  • 서버 측에서 저장되므로, 세션 ID가 쿠키에 저장되어 클라이언트와 연결
  • 브라우저를 닫거나 일정 시간이 지나면 세션 만료
  • 서버에 저장하므로 보안에 강하지만, 사용자가 많아질수록 서버 메모리 부담이 커질 수 있음
// 세션에 값 저장
HttpSession session = httpServletRequest.getSession();
session.setAttribute("loginUser", user);

// 세션에서 값 꺼내기
User user = (User) session.getAttribute("loginUser");

 

토큰 (Token)

  • 인증 시 사용자 정보를 담은 고유한 문자열
  • 서버가 아닌 클라이언트 측에 저장되어 서버 부담을 줄일 수 있다.
  • 다양한 클라이언트(웹, 앱 등)에서 사용 가능하여 확장성이 뛰어나다.
  • 토큰 자체에 인증 정보와 고유한 서명이 포함되어 있어 위조 방지 가능

 

+) JWT (JSON Web Token)

  • 인증에 필요한 정보들을 암호화시킨 JSON 형태의 Token
    → JSON 데이터 포맷을 사용하여 정보를 효율적으로 저장하고 암호화로 서버의 보안성을 높인다.
  • Spring Security에서 JWT, OAuth 등을 공식적으로 지원
    → 이를 사용하여 보안 결함의 위험을 줄이고, 일관성 있는 인증/인가 처리가 가능
  • Servlet Filter로도 JWT를 직접 구현할 수도 있지만, Spring Security를 활용하면 더 안전하고 구조적인 설정 가능
  • 주의할 점: JWT는 Base64로 인코딩되어 쉽게 복호화 할 수 있으므로 민감한 정보는 직접 담지 않도록 주의
JWT 구조: XXXXXX.YYYYYY.ZZZZZZ
                 (Header).(Payload).(Signature)

- Header: 토큰 타입(JWT), 해싱 알고리즘 정보 (ex: HS256)
- Payload: 사용자 정보, 권한 등 (Base64 인코딩된 JSON)
- Signature: 비밀 키를 사용한 서명 → 위조 방지

 

@Slf4j(topic = "JwtFilter")
@RequiredArgsConstructor
public class JwtFilter implements Filter {

    private final JwtUtil jwtUtil;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String requestURI = httpRequest.getRequestURI();
        String jwt = null;

        String authorizationHeader = httpRequest.getHeader("Authorization");

        // 로그인 요청은 필터 패스
        if (requestURI.equals("/api/login")) {
            chain.doFilter(request, response);
            return;
        }

        // 토큰 유무 확인
        if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT 토큰 필요");
            return;
        }

        jwt = authorizationHeader.substring(7);

        // 토큰 검증
        if (!jwtUtil.validateToken(jwt)) {
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "유효하지 않은 토큰");
            return;
        }

        // 권한 확인 (관리자 API)
        if (requestURI.startsWith("/api/admin") && !jwtUtil.hasRole(jwt, "ADMIN")) {
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "접근 권한 없음");
            return;
        }

        // 권한 확인 (사용자 API)
        if (requestURI.startsWith("/api/user") && !jwtUtil.hasRole(jwt, "USER")) {
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "접근 권한 없음");
            return;
        }

        // 통과
        chain.doFilter(request, response);
    }
}
@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {

    public static final String BEARER_PREFIX = "Bearer ";
    private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

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

    @PostConstruct
    public void init() {
        key = Keys.hmacShaKeyFor(Base64.getDecoder().decode(secretKey));
    }

    // 사용자 이름 추출
    public String extractUsername(String token) {
        return extractAllClaims(token).getSubject();
    }

    // 권한 추출
    public String extractRoles(String token) {
        return extractAllClaims(token).get("auth", String.class);
    }

    // 특정 역할 포함 여부
    public boolean hasRole(String token, String role) {
        return extractRoles(token).contains(role);
    }

    // 토큰 생성
    public String generateToken(String username, UserRoleEnum userRole) {
        Date now = new Date();
        return BEARER_PREFIX +
            Jwts.builder()
                .setSubject(username)
                .claim("auth", userRole)
                .setExpiration(new Date(now.getTime() + TOKEN_TIME))
                .setIssuedAt(now)
                .signWith(key, signatureAlgorithm)
                .compact();
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            log.error("JWT 오류", e);
            return false;
        }
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
    }
}

'Spring' 카테고리의 다른 글

[Spring] H2 In-Memory DB를 활용한 Repository 테스트  (2) 2025.06.04
[Spring] JPA 시작하기  (0) 2025.06.04
[Spring] Validation  (1) 2025.05.16
[Spring] Spring Bean 등록과 의존성 주입  (0) 2025.05.15
[Spring] 예외처리  (1) 2025.05.14