728x90
쿠키, 세션, 토큰이란?
1. 쿠키 (Cookie)
- 쿠키는 웹 브라우저에 저장되는 작은 데이터 조각으로, 사용자의 정보나 상태를 추적하는 데 사용됩니다.
- 예를 들어, 사용자가 로그인할 때 서버는 로그인 정보를 쿠키로 브라우저에 저장하고, 그 후 사용자 요청이 있을 때마다 이 쿠키를 사용하여 사용자를 인증할 수 있습니다.
- 특징:
- 클라이언트 측에 저장됩니다.
- 만료 시간이나 특정 정보(예: 사용자 ID, 로그인 상태)를 설정할 수 있습니다.
- 보통 HTTP 요청 시 자동으로 서버에 전송됩니다.
- 보안: HTTPOnly와 Secure 속성을 사용하여 보안성을 강화할 수 있습니다.
2. 세션 (Session)
- 세션은 서버 측에서 사용자의 상태 정보를 저장하는 방식입니다. 사용자가 로그인하면, 서버는 세션을 생성하여 고유한 세션 ID를 클라이언트(웹 브라우저)에 전달합니다.
- 클라이언트는 이후 요청 시 이 세션 ID를 서버에 전송하여 서버가 세션 정보를 기반으로 사용자를 식별합니다.
- 특징:
- 서버 측에 저장된 데이터로 사용자의 상태를 추적합니다.
- 서버가 클라이언트에게 세션 ID를 발급하고, 이 ID는 보통 쿠키를 통해 클라이언트에 저장됩니다.
- 보안: 서버에서 세션 데이터를 관리하기 때문에 쿠키보다 보안성이 높습니다. 하지만 세션이 서버 측에서 관리되므로 서버의 부하가 늘어날 수 있습니다.
3. 토큰 (Token)
- 토큰은 서버와 클라이언트 간에 정보를 교환할 때 사용되는 보안 인증 수단입니다. 일반적으로 JWT(Json Web Token) 형식으로 사용되며, 사용자 인증 및 권한 부여에 사용됩니다.
- 토큰은 서버에서 생성되어 클라이언트에 전달되며, 이후 클라이언트는 이 토큰을 요청에 첨부하여 인증을 받습니다.
- 특징:
- 주로 Stateless 방식으로 동작하여 서버에 상태 정보를 저장하지 않습니다.
- 클라이언트 측에서 토큰을 저장하고 요청할 때마다 서버로 전달합니다.
- 세션 기반 인증 방식보다 더 효율적이고 확장성이 좋습니다.
쿠키 세션을 사용하지 않고 토큰을 사용하는 이점
- Stateless (무상태성)
- 토큰 기반 인증은 서버 측에 상태를 저장하지 않기 때문에 Stateless 방식을 따릅니다. 즉, 서버는 클라이언트의 상태를 기억하지 않아도 됩니다. 이로 인해 서버가 더 확장 가능하고, 여러 서버에 분산 처리할 때 유리합니다.
- 서버 부하 감소
- 세션은 서버 측에서 데이터를 관리하므로 서버의 부하가 증가할 수 있습니다. 반면, 토큰은 클라이언트 측에서 관리되므로 서버의 저장소에 부담을 주지 않습니다.
- 스케일링 용이
- 토큰은 클라이언트에서 독립적으로 작동하므로, 여러 서버 간에 세션 정보를 공유할 필요가 없습니다. 이는 서버 확장 시 매우 유리합니다.
- 다양한 클라이언트 지원
- 토큰은 브라우저뿐만 아니라 모바일 애플리케이션이나 API와 같은 다양한 클라이언트에서도 사용할 수 있습니다. 반면, 세션은 브라우저에 의존적입니다.
- Cross-Domain 인증 지원
- 토큰은 도메인 간 인증을 쉽게 처리할 수 있어, 여러 도메인에 걸쳐 인증을 처리해야 하는 경우 유용합니다. 쿠키나 세션은 주로 동일 도메인 내에서 사용됩니다.
AccessToken이란?
- Access Token은 클라이언트가 서버에 요청을 보낼 때, 자신의 인증을 증명하기 위해 사용하는 짧은 유효기간을 가진 토큰입니다.
- 사용자가 로그인한 후 서버는 AccessToken을 생성하고 이를 클라이언트에 전달합니다. 클라이언트는 이후 서버에 요청할 때 이 토큰을 포함시켜 인증을 받습니다.
- 특징:
- 유효 기간이 짧아 자주 갱신해야 합니다.
- 서버는 AccessToken을 통해 사용자의 인증 정보를 확인하고, 해당 사용자가 권한을 가지고 있는지 판단합니다.
- 예시: JWT(JSON Web Token)는 대표적인 AccessToken 형식입니다.
RefreshToken이란?
- Refresh Token은 AccessToken의 유효 기간이 만료되었을 때, 새로운 AccessToken을 발급받을 수 있게 해주는 토큰입니다. 즉, AccessToken을 갱신하는 데 사용됩니다.
- 사용자가 로그인하면 서버는 AccessToken과 함께 RefreshToken을 클라이언트에 전달합니다. AccessToken이 만료되면 클라이언트는 RefreshToken을 서버에 보내 새로운 AccessToken을 발급받습니다.
- 특징:
- AccessToken보다 유효 기간이 길고 상대적으로 보안이 강화된 방식으로 저장됩니다.
- 주로 서버와의 상호작용에서만 사용되며, 클라이언트의 민감한 데이터를 포함하지 않습니다.
- RefreshToken을 이용한 인증은 세션을 유지하면서 AccessToken을 새롭게 갱신하는 방식입니다.
- 단점: RefreshToken 자체가 유출될 경우 보안에 위협이 될 수 있으므로, RefreshToken을 안전하게 저장하고 관리하는 것이 중요합니다.
AccessToken과 RefreshToken의 관계
- AccessToken은 짧은 유효 기간을 가지며, 사용자가 새로운 요청을 할 때마다 인증을 제공합니다.
- RefreshToken은 AccessToken의 유효 기간이 만료되면, 새로운 AccessToken을 요청하는 데 사용됩니다.
- 즉, AccessToken은 주로 요청을 인증하는 데 사용되며, RefreshToken은 새로운 AccessToken을 발급받기 위한 키로 사용됩니다.
스프링 실습 정리
1. 의존성 설치
실습을 위해 jjwt 라이브러리를 의존성으로 추가합니다. 아래 의존성을 build.gradle 또는 pom.xml에 추가해야 합니다.
// Gradle 예시
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
2. application.yaml 설정
Spring Boot 설정 파일인 application.yaml을 사용하여 토큰 관련 설정을 추가합니다.
spring:
application:
name: security-token
token:
secret:
key: "SpringBootJWTTokenSecretKeyTest123123321321" # Secret Key
access:
expiration: 60 # Access Token 만료 시간 (60초)
refresh:
expiration: 120 # Refresh Token 만료 시간 (120초)
3. JwtTokenProvider 클래스
JWT 토큰을 생성하고 검증하는 JwtTokenProvider 클래스를 작성합니다. 이 클래스는 accessToken과 refreshToken을 생성하고 검증할 수 있는 메서드를 제공합니다.
@Component
public class JwtTokenProvider {
@Value("${token.secret.key}")
private String SECRET_KEY;
@Value("${token.access.expiration}")
private long ACCESS_TOKEN_EXPIRATION;
@Value("${token.refresh.expiration}")
private long REFRESH_TOKEN_EXPIRATION; // 2분
// Access Token 생성
public String createAccessToken(String userId) {
System.out.println("AccessToken create");
return createToken(userId, ACCESS_TOKEN_EXPIRATION);
}
// Refresh Token 생성
public String createRefreshToken(String userId) {
System.out.println("RefreshToken create");
return createToken(userId, REFRESH_TOKEN_EXPIRATION);
}
// 토큰 생성 로직
private String createToken(String userId, long expiration) {
return Jwts.builder()
.setSubject(userId)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) // 만료 시간 (밀리초로 변환)
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
// 토큰에서 사용자 ID 추출
public String getUserIdFromToken(String token) {
return Jwts.parserBuilder() // parser() 대신 parserBuilder() 사용
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
// 토큰 유효성 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder() // parser() 대신 parserBuilder() 사용
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token);
return true;
} catch (ExpiredJwtException e) {
// 만료된 토큰을 별도로 처리할 수 있음
return false;
} catch (JwtException | IllegalArgumentException e) {
// 다른 예외 처리
return false;
}
}
// 쿠키에서 특정 이름의 값 가져오기
public String getTokenFromCookie(HttpServletRequest request, String cookieName) {
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if (cookie.getName().equals(cookieName)) {
return cookie.getValue();
}
}
}
return null;
}
}
4. 로그인 요청 처리 (AuthController)
로그인 요청을 처리하는 AuthController에서는 사용자가 로그인하면 AccessToken과 RefreshToken을 생성하고, 이를 클라이언트에게 쿠키로 전달합니다.
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final JwtTokenProvider jwtTokenProvider;
@PostMapping("/login")
public ResponseEntity<Map<String, String>> login(
@RequestBody Map<String, String> loginRequest,
HttpServletResponse response) {
String userId = loginRequest.get("userId");
String password = loginRequest.get("password");
// 예제: 간단한 유저 검증
if ("testUser".equals(userId) && "password123".equals(password)) {
String accessToken = jwtTokenProvider.createAccessToken(userId);
String refreshToken = jwtTokenProvider.createRefreshToken(userId);
// 쿠키 설정
Cookie accessTokenCookie = new Cookie("accessToken", accessToken);
accessTokenCookie.setHttpOnly(true);
accessTokenCookie.setPath("/");
accessTokenCookie.setMaxAge(1 * 60); // 1분
Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken);
refreshTokenCookie.setHttpOnly(true);
refreshTokenCookie.setPath("/");
refreshTokenCookie.setMaxAge(2 * 60); // 2분
response.addCookie(accessTokenCookie);
response.addCookie(refreshTokenCookie);
// 응답 데이터
Map<String, String> tokens = new HashMap<>();
tokens.put("message", "로그인 성공");
return ResponseEntity.ok(tokens);
} else {
return ResponseEntity.status(401).body(Map.of("message", "로그인 실패"));
}
}
}
5. 유저 정보 가져오기 (UserController)
유저 정보 요청 시, accessToken 또는 refreshToken을 이용하여 인증 후 유저 정보를 반환합니다.
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserController {
private final JwtTokenProvider jwtTokenProvider;
@GetMapping("/info")
public ResponseEntity<?> getUserInfo(HttpServletRequest request, HttpServletResponse response) {
// 쿠키에서 토큰 가져오기
String accessToken = jwtTokenProvider.getTokenFromCookie(request, "accessToken");
String refreshToken = jwtTokenProvider.getTokenFromCookie(request, "refreshToken");
// 액세스 토큰 검증
if (accessToken != null && jwtTokenProvider.validateToken(accessToken)) {
String userId = jwtTokenProvider.getUserIdFromToken(accessToken);
// 유저 정보 반환
Map<String, String> userInfo = new HashMap<>();
userInfo.put("userId", userId);
userInfo.put("name", "Test User"); // 예제 데이터
userInfo.put("email", "user@example.com");
System.out.println("accessToken : 유저 정보");
return ResponseEntity.ok(userInfo);
}
// 액세스 토큰이 유효하지 않을 경우, 리프레시 토큰 검증
if (refreshToken != null && jwtTokenProvider.validateToken(refreshToken)) {
String userId = jwtTokenProvider.getUserIdFromToken(refreshToken);
// 새로운 액세스 토큰 생성
String newAccessToken = jwtTokenProvider.createAccessToken(userId);
// 새 액세스 토큰을 쿠키로 전달
Cookie accessTokenCookie = new Cookie("accessToken", newAccessToken);
accessTokenCookie.setHttpOnly(true);
accessTokenCookie.setPath("/");
accessTokenCookie.setMaxAge(15 * 60); // 15분
response.addCookie(accessTokenCookie);
// 유저 정보 반환
Map<String, String> userInfo = new HashMap<>();
userInfo.put("userId", userId);
userInfo.put("name", "Test User"); // 예제 데이터
userInfo.put("email", "user@example.com");
System.out.println("refreshToken : 유저 정보");
return ResponseEntity.ok(userInfo);
}
// 둘 다 유효하지 않은 경우
return ResponseEntity.status(401).body("로그인하세요.");
}
}
728x90
'Spring Boot' 카테고리의 다른 글
[Spring Boot] Lombok과 직렬화/역직렬화 (0) | 2024.11.25 |
---|---|
[Spring Boot] 간단한 실시간 웹소켓 채팅 구현하기 (0) | 2024.11.22 |
[Spring Boot] 스프링 시큐리티 설정 (5) | 2024.11.09 |
[Spring Boot] @Configuration @Bean 사용법 (3) | 2024.11.08 |
[Spring Boot] Snake Case JSON 변환 전체 설정하기 (0) | 2024.11.01 |