웹은 기본적으로 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 |