Spring Security 세션 관리 설정
.sessionManagement(session -> session
.maximumSessions(1) // 동시 접속 허용 개수 1개
.maxSessionsPreventsLogin(false) // 이전 로그인 유지 여부
)
▪️maximumSessions(N)
▫️한 사용자가 동시에 로그인할 수 있는 세션 개수를 N개로 제한
▫️같은 계정으로 여러 개의 브라우저에서 로그인하려고 하면, 이전 로그인 세션이 끊길 수 있음
▪️maxSessionsPreventsLogin(true/false)
▫️false → 나중에 로그인한 사람이 접속 가능, 먼저 로그인한 사람은 자동 로그아웃됨.
▫️true → 먼저 로그인한 사람이 계속 유지, 나중에 로그인한 사람은 차단됨.
CSRF 토큰
웹사이트 보안 도구 ▶️ 진짜 사용자가 맞는지 확인하는 보안 코드
❔CSRF (사이트 간 요청 위조, Cross-Site Request Forgery) 공격이란?
사용자가 모르는 사이에 해커가 사용자의 계정을 이용해 원하지 않는 요청을 보내는 공격
1️⃣은행 사이트에 로그인한 상태
2️⃣해커가 가짜 링크를 보냄
3️⃣그 링크를 클릭하면 너도 모르게 해커의 코드가 실행됨.
4️⃣내 계정에서 해커가 미리 설정한 계좌로 돈이 이체됨
CSRF 토큰 사용 예시
- 사이트에 로그인하면, 서버가 특별한 "CSRF 토큰"을 만들어서 줌.
- 어떤 중요한 요청을 보낼 때, CSRF 토큰을 같이 보내야 함.
- 서버는 내가 보낸 CSRF 토큰이 맞는지 확인
- 맞으면 → 요청을 실행함 ✅
- 틀리면 → 요청을 거부함 ❌ (해커가 시도한 거라 판단함)
📌 스프링 부트 기본 설정
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf() // CSRF 보호 활성화 (기본값)
.and()
.authorizeRequests()
.anyRequest().authenticated();
return http.build();
}
}
📌CSRF 검사 비활성화
http.csrf().disable();
JWT(JSON Web Token)
세션(Session)의 단점
세션은 서버가 클라이언트의 상태(로그인 정보 등)를 저장하는 방식인데
1️⃣서버가 여러개 일 경우
처음 요청한 서버(A)에 세션을 저장하면, 다음 요청이 서버 (B)로 가면 세션 정보가 없음
이를 해결하려면 세션을 모든 서버가 공유해야한다
2️⃣데이터 베이스 사용
여러 서버가 같은 DB에 접속해서 세션을 저장하고 공유한다
하지만 추가적인 DB연결 비용 발생(트래픽이 많아지면 성능저하)
서버가 많아질 수록 한개의 DB가 과부화
DB를 여러개 두면 어떤 데이터를 어디다 저장할지 문제, 같은 데이터를 가진 DB 두개?
등의 문제가 있다
이것을 해결하기 위해 세션 대신 JWT 사용
JWT를 사용하면 서버가 데이터를 저장할 필요 없이,
클라이언트(브라우저, 앱)에서 정보를 가지고 있음
JWT란
JSON 데이터를 안전하게 주고받기 위한 토큰(Token)
로그인 정보나 사용자 정보를 암호화해서 저장하는 보안 토큰
📌 JWT를 사용하면?
서버에서 로그인 정보를 저장할 필요 없음
클라이언트(브라우저, 앱)가 직접 JWT를 가지고 있음
JWT를 서버에 보내서 "내가 인증된 사용자" 라고 증명 가능
JWT 구성 (Header.Payload.Signature)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTYiLCJpYXQiOjE2NzAxMjQ4MDB9.V2WqXJDz0_lGpXzI4Q41gFBSU0wSleVUyJQIlXDO1M4
//점(.)으로 Header, Payload, Signature 세 부분으로 나뉘어 있다
1. Header (헤더) | 어떤 암호화 방식(알고리즘)을 사용할지 설정 | { "alg": "HS256", "typ": "JWT" } |
2. Payload (페이로드, 데이터) | **사용자 정보(아이디, 권한 등)**가 들어있음 | { "userId": "123456", "role": "admin" } |
3. Signature (서명) | 변조 방지용 보안 서명 | 서버의 비밀키로 생성 |
▪️Payload: 실제로 전송할 데이터가 담긴 부분 클레임(claim)이라 불리는 정보들이 포함
▫️Registered claims: 특정한 정보를 제공하기 위해 사전에 정의된 클레임들로, 예 를 들어 발행자(issuer), 만료 시간(expiration time), 주체(subject) 등이 있다.
▫️Public claims: 사용자 정의 클레임으로 충돌을 피하기 위해 URI 형식으로 작성
▫️Private claims: 애플리케이션에서 사용할 수 있는 클레임
▪️Signature: Header와 Payload의 내용을 인코딩하고, 비밀 키(secret key)를 사용하여 서명. 이 서명은 메시지가 변경되지 않았음을 확인하는 데 사용
JWT 동작 과정
로그인 시 JWT가 만들어지고, 이후 요청마다 JWT를 사용
1️⃣ 클라이언트(브라우저, 앱)에서 로그인 요청
{ "username": "testUser", "password": "1234" }
- 사용자가 아이디/비밀번호 입력하고 로그인 버튼 누름
2️⃣ 서버가 로그인 정보를 확인하고 JWT를 발급
- 로그인 성공하면 서버가 JWT를 생성해서 클라이언트에 전달함
- 이 JWT는 클라이언트의 저장소 (일반적으로는 로컬 스토리지나 쿠키)에 저장
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
3️⃣ 클라이언트가 이후 요청마다 JWT를 보냄
- 클라이언트는 Authorization 헤더에 JWT를 담아서 서버에 요청함
GET /user/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
- 서버는 JWT를 확인하고, 유효하면 요청을 처리함
4️⃣ 서버가 JWT를 검증해서 응답
- 서버는 비밀키(Secret Key) 를 이용해서 JWT가 변조되지 않았는지 확인함.
- 검증이 통과되면, 사용자 정보가 포함된 응답을 보냄.
{
"userId": "123456",
"name": "Test User",
"role": "admin"
}
JWT는 한 번 로그인하면 이후에는 매번 로그인할 필요 없이, 이 토큰만 보내서 인증을 받을 수 있음
장점 | 단점 |
서버에서 로그인 정보를 저장할 필요 없음 (세션 관리 불필요) | 토큰이 길어서 네트워크 트래픽이 증가할 수 있음 |
REST API에 적합 (서버가 상태를 유지할 필요 없음) | 보안 키(Secret Key)가 유출되면 위험 |
사용자 정보(Payload)에 추가 데이터를 넣을 수 있음 | 토큰이 변조될 위험이 있음 (서명 검증 필요) |
보안강화 방법
1️⃣ 비밀키(Secret Key) 보안 유지
- JWT를 만들 때 사용하는 비밀키(Secret Key) 가 노출되면, 해커가 가짜 JWT를 만들 수 있다
- 비밀키는 환경변수(.env) 같은 안전한 곳에 저장해야 함.
2️⃣ JWT 만료 시간(Expiration) 설정
- 무제한 JWT는 위험 만료 시간(exp)을 설정해야 함.
- 보통 15분 ~ 1시간 정도로 설정하고, Refresh Token을 따로 두는 게 좋음.
3️⃣ HTTPS 사용 (쿠키 보안)
- JWT를 HTTP 헤더에 넣어서 보내기 때문에, HTTPS가 아니면 토큰이 쉽게 탈취될 수 있음
- Secure & HttpOnly 옵션을 설정하면 더 안전함.
4️⃣ Refresh Token 사용
- Access Token (짧게 유지) + Refresh Token (길게 유지) 조합을 사용하면 더 안전함
- Access Token이 만료되면, Refresh Token으로 새로운 Access Token을 발급받음.
액세스 토큰(Access Token)
- 클라이언트(사용자)가 서버에 요청할 때 "인증된 사용자" 라고 증명하는 토큰
- 클라이언트가 가지고 있으므로 탈취되었을 때 조치할 수 없음
- 만료 시간이 짧음 (보통 15분 ~ 1시간 정도)
리프레시 토큰(Refresh Token)
- 액세스 토큰이 만료되었을 때, 새로운 액세스 토큰을 받기 위한 토큰
- 서버에서만 저장 (보통 데이터베이스에 저장)
- 탈취됐을 때 서버에서 삭제 하면 됨
- 만료 시간이 길음 (보통 7일 ~ 30일 이상)
액세스 토큰(Access Token)
= "입장권" 🎟
리프레시 토큰(Refresh Token)
= "재발급 카드" 🔄
JWT만 사용할 경우의 문제점
- 액세스 토큰을 오래 유지하면 보안 위험이 커짐 (토큰 탈취 시 위험).
- 액세스 토큰을 짧게 설정하면 사용자 경험이 나빠짐 (자주 로그아웃됨).
해결 방법 → 액세스 토큰 + 리프레시 토큰 사용
- 로그인 시 액세스 토큰과 리프레시 토큰을 발급
- 클라이언트는 API 요청 시 액세스 토큰을 사용
- 액세스 토큰이 만료되면 리프레시 토큰으로 새로운 액세스 토큰을 요청
- 리프레시 토큰도 만료되면 다시 로그인해야 함
액세스 토큰이 만료되었을 때
- 사용자가 다시 요청을 보냄
- 액세스 토큰이 만료됨 → "로그아웃되었나요?" ❌
- 리프레시 토큰을 이용해 새 액세스 토큰 요청
- POST /auth/refresh { "refreshToken": "리프레시 토큰 값" }
- 서버는 리프레시 토큰을 확인하고, 새로운 액세스 토큰 발급
- { "accessToken": "새로운 액세스 토큰 값" }
- 이제 다시 API 요청 가능
JWT 생성, 검증
build.gradle
// jwt & json
// jwts
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
//gson - json 메시지를 다루기 위한 라이브러리
implementation 'com.google.code.gson:gson'
application.yml
jwt:
secretKey: 12345678901234567890123456789012
refreshKey: 12345678901234567890123456789012
1️⃣ 비밀키(Secret Key) 만들기
String secret = "abcdefghijklmnopqrstuvwxzy123456"; // 비밀키 (이걸로 암호화함)
byte[] bytes = secret.getBytes(StandardCharsets.UTF_8); // 비밀키를 바이트 배열로 변환
SecretKey secretKey = Keys.hmacShaKeyFor(bytes); // 암호화 키 생성
JWT는 암호화해서 만들기 때문에, 비밀키(Secret Key) 가 필요 이 키가 유출되면 보안에 취약하니까, 절대 노출되면 안 됨!
2️⃣ JWT(토큰) 만들기
String token = Jwts.builder()
.setIssuer("jun-app") // 누가 만든 토큰인지 ("jun-app"이 만듦)
.setSubject("jun123") // 사용자 ID (이 토큰은 "jun123"에 대한 것!)
.setExpiration(new Date(System.currentTimeMillis() + 35000)) // 35초 후 만료됨
.claim("role", "ADMIN") // role(권한) 정보를 추가 ("ADMIN")
.signWith(secretKey) // 비밀키로 서명 (위조 방지)
.compact();
token 에 JWT 문자열이 저장
3️⃣ JWT(토큰) 검증하고 정보 읽기
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey) // 비밀키로 서명 확인 (변조 방지)
.build()
.parseClaimsJws(token) // 토큰을 해석(디코딩)함
.getBody(); // JWT의 정보(클레임)를 가져옴
4️⃣ JWT 정보 출력
System.out.println(claims.getExpiration()); // 만료 시간 출력
System.out.println(claims.getSubject()); // 사용자 ID 출력
System.out.println(claims.getAudience()); // (여기서는 값 없음)
JwtTokenizer (JWTUtil)
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.List;
@Component
public class JwtTokenizer {
private final byte[] accessSecret; // 액세스 토큰 암호화 키
private final byte[] refreshSecret; // 리프레시 토큰 암호화 키
// 액세스 토큰 만료 시간 (30분)
public static final Long ACCESS_TOKEN_EXPIRES_COUNT = 30 * 60 * 1000L;
// 리프레시 토큰 만료 시간 (7일)
public static final Long REFRESH_TOKEN_EXPIRES_COUNT = 7 * 24 * 60 * 60 * 1000L;
// 생성자: application.yml에서 비밀키를 가져와서 설정
public JwtTokenizer(@Value("${jwt.secretKey}") String accessSecret,
@Value("${jwt.refreshKey}") String refreshSecret) {
this.accessSecret = accessSecret.getBytes(StandardCharsets.UTF_8); // 문자열 비밀키를 바이트 배열로 변환
this.refreshSecret = refreshSecret.getBytes(StandardCharsets.UTF_8); // 리프레시 토큰용 비밀키도 동일하게 변환
}
// ✅ JWT(액세스 토큰 & 리프레시 토큰) 생성 메서드
private String createToken(Long id, String email, String name, String username,
List<String> roles, Long expire, byte[] secretKey) {
Claims claims = Jwts.claims().setSubject(email); // 토큰의 subject(주제)를 email로 설정
// JWT에 포함할 사용자 정보 (클레임)
claims.put("userId", id); // 사용자 ID
claims.put("username", username); // 사용자 이름
claims.put("name", name); // 사용자 실제 이름
claims.put("roles", roles); // 사용자 역할(권한)
return Jwts.builder()
.setClaims(claims) // 위에서 설정한 사용자 정보(클레임) 추가
.setIssuedAt(new Date()) // 토큰 발급 시간 설정
.setExpiration(new Date(System.currentTimeMillis() + expire)) // 토큰 만료 시간 설정
.signWith(SignatureAlgorithm.HS256, secretKey) // HMAC-SHA256 알고리즘으로 서명
.signWith(getSigningKey(secretKey)) // 비밀키로 서명 (위 코드와 중복이므로 하나만 사용해야 함)
.compact(); // 최종적으로 JWT 문자열 반환
}
// ✅ HMAC-SHA 서명 키 생성 메서드
private static Key getSigningKey(byte[] secretKey) {
return Keys.hmacShaKeyFor(secretKey); // 비밀키를 HMAC-SHA 알고리즘용 Key로 변환
}
// ✅ 액세스 토큰 생성 메서드 (30분 유효)
public String createAccessToken(Long id, String email, String name, String username, List<String> roles) {
return createToken(id, email, name, username, roles, ACCESS_TOKEN_EXPIRES_COUNT, accessSecret);
}
// ✅ 리프레시 토큰 생성 메서드 (7일 유효)
public String createRefreshToken(Long id, String email, String name, String username, List<String> roles) {
return createToken(id, email, name, username, roles, REFRESH_TOKEN_EXPIRES_COUNT, refreshSecret);
}
}
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
// ✅ 1. JWT 비밀키 설정 (테스트용)
String accessSecret = "abcdefghijklmnopqrstuvwxyz123456"; // 32바이트 이상이어야 함!
String refreshSecret = "123456abcdefghijklmnopqrstuvwxyz"; // 32바이트 이상이어야 함!
// ✅ 2. JwtTokenizer 객체 생성
JwtTokenizer jwtTokenizer = new JwtTokenizer(accessSecret, refreshSecret);
// ✅ 3. 테스트 유저 정보
Long userId = 1L;
String email = "test@example.com";
String name = "John Doe";
String username = "johndoe";
List<String> roles = Arrays.asList("ROLE_USER", "ROLE_ADMIN");
// ✅ 4. JWT 토큰 생성
String accessToken = jwtTokenizer.createAccessToken(userId, email, name, username, roles);
String refreshToken = jwtTokenizer.createRefreshToken(userId, email, name, username, roles);
// ✅ 5. 생성된 토큰 출력
System.out.println("액세스 토큰 (30분 유효):\\n" + accessToken);
System.out.println("\\n리프레시 토큰 (7일 유효):\\n" + refreshToken);
}
쿠키 vs 세션 vs JWT 비교
구분 | 쿠키 (Cookie) | 세션 (Session) | JWT (JSON Web Token) |
저장 위치 | 클라이언트(브라우저) | 서버(메모리, DB 등) | 클라이언트(쿠키, 로컬 스토리지 등) |
인증 방식 | 쿠키에 인증 정보 저장 | 서버에서 세션 ID 관리 | 토큰 자체에 인증 정보 포함 |
서버 부담 | 적음 (Stateless) | 많음 (Stateful) | 적음 (Stateless) |
확장성 | 낮음 (서버 의존) | 낮음 (서버 세션 관리 필요) | 높음 (다중 서버, Microservices에 적합) |
보안 취약점 | XSS(쿠키 탈취), CSRF | 세션 ID 탈취 | 토큰 탈취 시 무효화 어려움 |
탈취 시 위험도 | 중간 (쿠키 보안 설정 가능) | 낮음 (서버에서 무효화 가능) | 높음 (만료 전까지 사용 가능) |
무효화 가능 여부 | 쿠키 삭제 가능 | 가능 (서버에서 세션 삭제) | 불가능 (Blacklist 시스템 필요) |
만료 관리 | 브라우저 설정에 따름 | 서버에서 설정 | 토큰에 만료 시간 포함 (Refresh Token 필요) |
사용 예시 | 간단한 로그인 유지 | 웹사이트 로그인 관리 | API 인증, 모바일 앱, 마이크로서비스 |
➕JWT는 탈취되면 막기 어려우니 만료 시간을 짧게 설정하고 리프레시 토큰 설정하기
➕ 쿠키는 토큰 크기가 작다 (간단한 데이터) , 보안 취약 제일 큼 , 사용자가 쿠키 값을 바꿀 수 있음, 쿠키 값 검증 불가
쿠키 값을 진짜 사용자가 맞는지 검증할 방법이 없다
'TIL' 카테고리의 다른 글
250321 TIL (0) | 2025.03.21 |
---|---|
250317 TIL (0) | 2025.03.17 |
250304 TIL (0) | 2025.03.04 |
250228 TIL (0) | 2025.02.28 |
250227 TIL (0) | 2025.02.27 |