로컬 스토리지
로컬 스토리지는 웹 스토리지 객체로
브라우저 내에 { key : value } 형태로 오리진에 종속되어 저장되는 데이터이다.
오리진

해당 데이터가 저장되는 위치.
로컬 스토리지는 특정 웹사이트의 도메인을 기반으로 구분된다.
도메인을 가진 웹사이트에서 저장한 로컬 스토리지 데이터는 해당 도메인을 오리진으로 가진다.
오리진은 url의 프로토콜, 호스트(도메인), 포트로 구성된다.
로컬 스토리지의 특징

하나의 키에 하나의 값만 저장된다.
데이터는 사용자가 수동으로 삭제하지 않는 한 로컬 저장소에 저장된다.
최대 저장용량은 5MB 이다.
로그인을 유지하기 위한 값 등으로 사용되며 로컬 스토리지 데이터는 자동으로 서버에 전송되지 않는다.
로컬 스토리지 사용법 (자바스크립트)
로컬 스토리지에 데이터 저장하기
localStorage 객체를 사용한다. 데이터는 key-value 쌍으로 저장되며
모든 값은 문자열로 저장된다.
객체나 배열을 저장하기 위해서는 JSON 문자열로 변환하여 저장해야한다.
// 데이터 저장
localStorage.setItem('key', 'value');
username에 Alice라는 값을 저장할 수도 있다.
localStorage.setItem('username', 'Alice');
로컬 스토리지에서 데이터 가져오기
getItem 메서드를 사용해 저장된 데이터를 가져온다.
// 데이터 가져오기
const username = localStorage.getItem('username');
console.log(username); // 출력: 'Alice'
로컬 스토리지에서 데이터 제거
removeItem 메서드를 사용해 특정 키의 데이터를 제거한다.
// 데이터 제거
localStorage.removeItem('username');
로컬 스토리지 전체 데이터 제거
clear 메서드를 사용해 로컬 스토리지에 저장된 모든 데이터 제거
// 모든 데이터 제거
localStorage.clear();

도메인
데이터가 저장되는 위치를 식별하는데 사용되는 주소이다.
웹 사이트의 주소를 나타내며 DNS(Domain Name System) 를 통해 IP 주소로 해석된다.
쿼리 스트링
HTTP 요청에서 데이터를 전달하는 방법이다.
URL의 끝에 ? 문자로 시작하며 여러 개의 파라미터를 & 문자로 구분하여 나타낸다.
key value형태로 데이터를 전달하며
"https://www.example.com/search?q=java&page=1"에서 q=java&page=1은 쿼리 스트링이며
여기서 q=java와 page=1은 각각 파라미터이다.
파라미터
key, value로 구성되어 있다.
주로 GET 요청에서 사용되며 서버로 데이터를 전달할 때 사용된다.
예를 들어 "https://www.example.com/search?fruit=apple" 에서
fruit=apple은 파라미터이며 fruit 와 apple 은 key, value 이다.
key value
로컬 스토리지에서 데이터는 key와 value의 쌍으로 저장된다.
key는 데이터를 식별하며 value는 해당 키에 연결된 데이터이다.
자바스크립트에서는 키와 값을 명시하여 데이터를 저장할 수 있다.
// 로컬 스토리지에 데이터 저장
localStorage.setItem('username', 'Alice');
localStorage.setItem('email', 'alice@example.com');
// 데이터 가져오기
const username = localStorage.getItem('username'); // 'Alice'
const email = localStorage.getItem('email'); // 'alice@example.com'
// 데이터 제거
localStorage.removeItem('email');
로컬스토리지 캐싱해보기
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>로컬 스토리지 예제</title>
</head>
<body>
<h2>입력값 저장하기</h2>
<input type="text" id="inputText" placeholder="텍스트를 입력하세요">
<button onclick="saveToLocalStorage()">저장</button>
<script>
function saveToLocalStorage() {
// 입력된 텍스트 가져오기
const inputText = document.getElementById('inputText').value;
// 로컬 스토리지에 저장
localStorage.setItem('userText', inputText);
alert('입력된 텍스트가 로컬 스토리지에 저장되었습니다.');
}
</script>
</body>
</html>
성공!

세션 스토리지
웹 스토리지 객체로 로컬 스토리지와 유사하게
{ key : value } 형태로 오리진에 종속되어 저장된다.
하지만 동일한 오리진이라도 각 탭마다 독립적으로 저장된다.
다른 탭에서 세션 스토리지에 저장된 데이터에 접근할 수 없다.
특징
하나의 키에 하나의 값만 저장된다.
최대 저장용량은 5MB이다.
사용자가 브라우저에서 탭을 닫으면 데이터는 만료된다.
메서드
setItem(key, value)
주어진 키와 값을 세션 스토리지에 저장한다.
// 세션 스토리지에 데이터 저장하기
sessionStorage.setItem('username', 'john_doe');
getItem(key)
주어진 키(key)에 해당하는 값을 세션 스토리지에서 가져온다.
// 세션 스토리지에서 데이터 가져오기
const username = sessionStorage.getItem('username');
console.log('Username:', username); // 출력: Username: john_doe
removeItem(key)
주어진 키(key)에 해당하는 값을 세션 스토리지에서 제거
// 세션 스토리지에서 데이터 제거하기
sessionStorage.removeItem('username');
clear()
세션 스토리지에 저장된 모든 데이터 제거
// 세션 스토리지의 모든 데이터 제거하기
sessionStorage.clear();
쿠키
브라우저에 저장된 데이터 조각이다.
클라이언트에서 먼저 설정할 수도 있고 서버에서도 먼저 설정할 수 있으나
서버에서 먼저 설정하여 쿠키를 만든다.

HTTP 헤더를 통해 클라이언트 혹은 서버가 HTTP 요청 또는 응답 할 때 추가 정보를 전달할 수 있다.
서버에서 응답 헤더로 Set-Cookie로 설정해서 쿠키를 보내면
클라이언트에서 요청 헤더 Cookie에 설정되어 자동으로 서버에 전달하고, 브라우저에도 저장된다.
특징
서버, 클라이언트 둘 다 조작이 가능하지만 서버에서 설정한다.
최대 4Kb의 저장용량을 가진다.
세션 쿠키
Expires, Max-Age 속성을 지정하지 않았다.
세션 쿠키는 브라우저가 종료되면 쿠키도 사라진다.
영구 쿠키
Expires, Max-Age 속성을 지정했다.
영구 쿠키는 특정날짜 또는 일정기간이 지나면 삭제되기 만든 쿠키이다.
브라우저를 닫을 때 만료된다.
쿠키를 설정할때 사용되는 HTTP 헤더
Set-Cookie 헤더
쿠키를 클라이언트에게 설정할 때 사용하는 헤더.
예제
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Date;
public class CookieExample {
public static void main(String[] args) throws IOException {
int port = 8000;
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
System.out.println("Server running at http://localhost:" + port + "/");
server.createContext("/", new MyHandler());
server.setExecutor(null); // creates a default executor
server.start();
}
static class MyHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
// Set a cookie
String cookieName = "username";
String cookieValue = "john_doe";
String cookiePath = "/";
int cookieMaxAge = 3600; // seconds, 1 hour
// Create cookie header
String cookie = cookieName + "=" + cookieValue
+ "; Path=" + cookiePath
+ "; Max-Age=" + cookieMaxAge
+ "; HttpOnly";
// Add cookie to response headers
exchange.getResponseHeaders().add("Set-Cookie", cookie);
// Send response
String response = "Cookie has been set!";
exchange.sendResponseHeaders(200, response.getBytes().length);
exchange.getResponseBody().write(response.getBytes());
exchange.getResponseBody().close();
}
}
}
코드 설명
1. HttpServer을 사용하여 서버를 생성하고 / 경로에 대한 요청을 처리할 MyHandler 클래스를 등록한다.
2. Myhandler 클래스에서 handle 메서드를 오버라이드하여 쿠키를 설정한다.
3. cookieName, cookieValue: 쿠키의 이름과 값
cookiePath: 쿠키의 유효 경로
cookieMaxAge: 쿠키의 유효 기간을 초 단위로 지정. 1시간으로 설정
4. 쿠키 정보를 기반으로 Set-Cookie 헤더 생성
5. exchange.getResponseHeaders().add("Set-Cookie", cookie)를 통해 응답 헤더에 쿠키 추가
6. 'sendResponseHeader' 메서드를 사용하여 HTTP 응답 코드와 응답 본문의 길이 설정
7. getResponseBody().write 메서드를 사용하여 응답 데이터 전송
쿠키의 보안
Secure 속성
HTTP 연결에서만 쿠키가 전송되도록 하는 속성.
이는 중간자 공격을 방지하고 쿠키의 안정성을 보장한다.
HttpOnly 속성 사용
HttpOnly 속성을 사용하여 JavaScript를 통해 쿠키에 접근할 수 없도록 한다.
이는 XSS 공격을 방지하는 데 도움이 된다.
Domain 속성 사용
Domain 속성을 사용하여 쿠키가 전송될 도메인을 명시적으로 지정한다.
이를 통해 쿠키의 범위를 제한할 수 있다.
Path 속성 사용
쿠키가 전송될 URL 경로를 명시적으로 지정한다.
쿠키의 사용 범위를 제한할 수 있다.
쿠키 값의 검증
서버 측에서 쿠키 값의 유효성을 검증한다.
사용자가 입력한 데이터를 쿠키 값으로 사용할 때는
유효성 검사를 통해 악의적인 데이터 삽을 방지한다.
예제
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Date;
public class SecureCookieExample {
public static void main(String[] args) throws IOException {
int port = 8000;
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
System.out.println("Server running at http://localhost:" + port + "/");
server.createContext("/", new MyHandler());
server.setExecutor(null); // creates a default executor
server.start();
}
static class MyHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
// Set a secure and HttpOnly cookie
String cookieName = "username";
String cookieValue = "john_doe";
String cookiePath = "/";
int cookieMaxAge = 3600; // seconds, 1 hour
// Create cookie header with Secure and HttpOnly attributes
String cookie = cookieName + "=" + cookieValue
+ "; Path=" + cookiePath
+ "; Max-Age=" + cookieMaxAge
+ "; Secure"
+ "; HttpOnly";
// Add cookie to response headers
exchange.getResponseHeaders().add("Set-Cookie", cookie);
// Send response
String response = "Secure cookie has been set!";
exchange.sendResponseHeaders(200, response.getBytes().length);
exchange.getResponseBody().write(response.getBytes());
exchange.getResponseBody().close();
}
}
}
쿠키 허용관련 알림창
서비스 운영시 쿠키를 사용한다면 쿠키허용 관련 알림창을 만들어야 한다.
방문 기록을 추적할 때 쿠키가 사용되기 때문이다.
이는 사용자의 데이터의 간접수집에 해당되며 거기에 해당하는 KISA 지침을 준수해야 한다.
로컬 스토리지, 세션스토리지, 쿠키 비교
공통점
공통점 |
설명 |
저장 위치 |
클라이언트의 브라우저에 저장된다. |
클라이언트 사이드 저장소 |
클라이언트 사이드에서 데이터를 저장한다. |
데이터 전송 |
HTTP 요청 시 서버로 자동으로 전송된다. |
차이점
특성/차이점 |
로컬 스토리지 (Local Storage) |
세션 스토리지 (Session Storage) |
쿠키 (Cookie) |
저장 용량 |
5MB |
5MB |
4KB |
데이터 지속성 |
사용자가 명시적으로 삭제할 때까지 유지 |
브라우저 세션 동안 유지 |
|
(브라우저 종료 시 삭제) |
만료 날짜/시간에 따라 지속 기간이 제한 |
|
|
사용 예시 |
사용자 환경 설정, 오프라인 데이터 저장 |
세션 기간 동안의 로그인 정보 저장 |
로그인 상태 유지, 사용자 설정 유지 등 |
서버로 전송 여부 |
X |
X |
O (서버와 주고받을 때 자동으로 전송) |
보안 |
클라이언트 사이드에서 액세스 가능 |
클라이언트 사이드에서 액세스 가능 |
클라이언트 및 서버 사이에서 액세스 가능 |
로그인
HTTP 요청을 통해 데이터를 주고 받을 때 요청이 끝나면 요청한 사용자의 정보 등을 유지하지 않는 특성이 있다.
로그인은 이전에 로그인한 상태값이 남아있어야 하기 때문에 세션 기반 인증방식 또는 토큰기반인증방식으로 구현된다.

세션: 서버와 클라이언트의 연결이 활성화된 상태
세션ID: 웹 서버 또는 DB에 저장되는 클라이언트에 대한 ID
세션 기반 로그인 프로세스

1. 처음 로그인 > 세션 ID 생성 > 서버에서 세션ID를 쿠키로 설정해 클라이언트에게 전달
2. 클라이언트가 서버에 요청을 보낼 때 해당 세션ID를 쿠키로 담아 전에 로그인했던 아이디지인지 확인
3. 로그인을 유지
단점
1. 사용자의 상태에 관한 데이터를 서버에 저장했을 때 로그인 중인 유저의 수가 증가한다면
서버의 메모리 과부하가 일어날 수 있다.
2. DB 중 RDBMS에 저장한다면 직렬화 및 역직렬화에 관한 오버헤드가 발생한다.
세션 기반 인증방식 실습!
package com.example.demo.controller;
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@Controller
public class LoginController {
// 로그인 폼 제출 시 호출
@PostMapping("/login")
public String login(@RequestParam String username,
@RequestParam String password,
HttpServletRequest request) {
User user = userMapper.findByUsername(username); // 사용자 이름으로 DB에서 사용자 조회
// 사용자가 존재하고 비밀번호가 일치하면
// 세션에 사용자 이름 저장 후 welcome 페이지로 리다이렉트
if (user != null && user.getPassword().equals(password)) {
HttpSession session = request.getSession();
//세션에 사용자 이름 저장
session.setAttribute("username", username);
//welcome 페이지로 리다이렉트
return "redirect:/welcome";
} else {
// 인증 실패 시 로그인 페이지로 리다이렉트
return "redirect:/";
}
}
// 환영 페이지(welcome.html)를 반환
@GetMapping("/welcome")
public String welcome(HttpServletRequest request) {
HttpSession session = request.getSession();
//세션에서 사용자 이름 가져오기
String username = (String) session.getAttribute("username");
// 사용자가 로그인한 경우 welcome 페이지 반환
// 로그인하지 않은 경우 로그인 페이지로 리다이렉트
if (username != null) {
return "welcome";
} else {
// 로그인 페이지로 리다이렉트
return "redirect:/";
}
}
// 로그아웃 처리 메서드
@GetMapping("/logout")
public String logout(HttpServletRequest request) {
HttpSession session = request.getSession();
session.invalidate(); // 세션 무효화
return "redirect:/"; // 로그인 페이지로 리다이렉트
}
}
토큰 기반 로그인 인증방식
상태를 서버에 저장하지 않고 클라이언트 측에 토큰을 저장하여 인증을 수행한다.
이 방식은 RESTful API와 SPA(Single Page Application) 등에서 사용된다.
장점
staless: 서버는 클라이언트의 상태를 유지하지 않아도 된다. 각 요청은 독립적으로 처리된다.
확장성: 토큰은 클라이언트 측에 저장되므로 서버는 추가 저장 공간이나 자원을 할당하지 않는다.
보안 강화: 토큰은 서명되어 있어 변조가 어렵고 HTTPS를 통해 안전하게 전송된다.
주의 사항
토큰 보안: 토큰을 안전하게 보호하기 위해 클라이언트 측에서는 보안을 해야한다.
XSS 공격에 노출될 경우 토큰이 탈취될 수 있다.
토큰 만료: 토큰의 만료 기간을 설정하여 주기적으로 갱신해야 한다.
마료된 토큰을 사용할 경우 보안이 취약해진다.
JWT 토큰
JWT는 정보를 JSON 객체로 안전하게 표현하고 서명하여 검증할 수 있는 방식을 제공한다.

1. 인증 로직 > JWT 토큰 생성 (access 토큰 , refresh 토큰)
2. 사용자가 이후 access 토큰을 HTTP Header - Authorization 또는 HTTP Header-Cookie에 담아
인증이 필요한 서버에 요청해 원하는 컨텐츠를 가져온다.
JWT 구성 요소

헤더: JWT의 유형과 사용하는 알고리즘을 정의한다.
{ "alg": "HS256", "typ": "JWT" }
페이로드: 실제 전송할 정보를 포함한다. 클레임이라고도 불리는 키 값 쌍으로 구성된다.
{ "sub": "user123", "name": "John Doe", "exp": 1626587773 }
sub: 주제(subject) - 토큰의 주인 (유저) 지정
name: 사용자 이름 등 추가 정보를 포함
exp: 만료 시간(expiration) - 토큰의 유효 기간 지정
서명: 헤더와 페이로드를 합친 후 보안 키를 통해 서명한다. 토큰이 변조되지 않았음을 검증한다.
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
장점
자가 포함: JWT는 필요한 모든 정보를 자체적으로 포함하고 있기 때문에 서버에서 상태를 유지할 필요가 없다.
이는 staless 서버 설계에 유리하다.
경량화: 다른 유형의 토큰과 비교했을 때, JSON 형식이 때문에 가볍다.
확장성: JWT는 JSON을 사용한 형식이기 때문에 JSON을 기반으로 쉽게 직렬화, 역지렬화가 가능하다.
보안: 서명을 통해 토큰의 무결성을 검증한다.
단점
크기: 페이로드에 저장되는 정보가 많을 수록 JWT의 크기가 증가한다.
보안: 토큰이 탈취될 경우 토큰에 포함된 정보가 노출될 수 있다.
동작 원리


1. 사용자 로그인 처리
- 사용자가 아이디와 비밀번호로 로그인을 시도한다.
- 로그인이 성공하면, 서버는 사용자를 식별할 수 있는 정보를 기반으로 Access Token과 Refresh Token을 발급한다.
2. Access Token 발급
- Access Token은 사용자가 리소스에 접근할 때 사용된다.
- 일반적으로 JWT(JSON Web Token) 형식으로 발급된다.
- JWT에는 사용자 정보와 만료 시간 등이 포함된다.
- 서명을 통해 토큰의 무결성을 보장한다.
3. Refresh Token 발급
- Refresh Token은 Access Token의 만료 시간이 지나면 새로운 Access Token을 발급받을 수 있는 권한을 부여한다.
- 데이터베이스에 저장되고, 유효기간이 길고 암호화되어 관리된다.
4. 토큰 저장
- 클라이언트는 발급받은 Access Token을 저장하고, 필요할 때마다 이를 사용하여 서버에 요청한다.
- Refresh Token은 안전한 장소에 저장되어, Access Token이 만료될 때 새로운 Access Token을 요청하는 데 사용된다.
5. 토큰 검증
- 서버는 클라이언트가 제공한 Access Token의 유효성을 검증한다.
- JWT의 서명을 통해 토큰의 변조 여부를 확인하고, 만료 시간을 검사하여 유효성을 판단한다.
6. Access Token 갱신
- Access Token의 유효기간이 다가오면, Refresh Token을 사용하여 새로운 Access Token을 발급받는다.
- 이 과정에서 사용자는 로그인을 다시 하지 않아도 된다.
@Controller
public class AuthController {
@Autowired
private UserService userService;
@PostMapping("/login")
@ResponseBody
//ResponseEntity<> 클래스로 HTTP 응답의 상태 코드 명시
public ResponseEntity<?> login(@RequestBody Map<String, String> credentials) {
String username = credentials.get("username");
String password = credentials.get("password");
//사용자명과 비밀번호 검증
if (userService.isValidUser(username, password)) {
// JWT 토큰 생성
String token = generateToken(username);
return ResponseEntity.ok(token);
} else {
//인증 실패시 UNAUTHORIZED(401) 상태 코드 반환
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
@PostMapping("/refresh")
@ResponseBody
public ResponseEntity<?> refresh(@RequestHeader("Authorization") String refreshToken) {
// 리프레시 토큰을 검증하고 새로운 액세스 토큰 생성
String username = validateRefreshToken(refreshToken);
if (username != null) {
//사용자 정보 호출
String email = userService.getEmail(username);
//새로운 액세스 토큰 성성
String newToken = generateToken(username);
return ResponseEntity.ok(newToken);
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
private String generateToken(String username) {
return Jwts.builder() //JWT를 생성하기 위한 빌더 객체 생성
.setSubject(username) //JWT의 클레임(사용자명 생성
.setIssuedAt(new Date()) //토큰의 발생 일시 설정. 현재 시간으로 설정
//토큰의 만료 일시 설정
.setExpiration(new Date(System.currentTimeMillis() + JwtConfig.ACCESS_TOKEN))
//토큰을 서명. HS256 알고리즘과 SECRET KEY를 사용하여 서명
.signWith(SignatureAlgorithm.HS256, JwtConfig.SECRET_KEY)
//토큰을 문자열로 화직렬화
.compact();
}
private String validateRefreshToken(String refreshToken) {
try {
Claims claims = Jwts.parser() //JWT 파서 생성
.setSigningKey(JwtConfig.SECRET_KEY) //서명 키 설정
.parseClaimsJws(refreshToken) //주어진 토큰 파싱, 서명 검증
.getBody(); // 토큰의 클레임 추출
// 유효한 토큰인 경우 클레임에서 key를 추출하여 반환
return claims.getSubject();
} catch (Exception e) {
//예외 처리
return null;
}
}
}
postman.com 사이트로 발급받은 토큰을 검증한다.
https://web.postman.co/
refresh 토큰 재발급
jwt 부분에 새로 받은 토큰을 집어넣어서 실행.
https://curl.se/windows/ 설치 후 curl 명령어가 동작하게 한다.
주의할 점
- 쿠키에 담은 토큰에 Bearer를 앞에 둬서 토큰 기반 인증방식이라는 것을 알려준다.
- https 방식을 사용한다.
- 쿠키에 저장한다면 sameSite: Strict를 사용한다.
- 수명이 짧은 access token을 발급한다.
- url에 토큰을 전달하지 말아야 한다.
토큰 탈취 방지 방법
1. Access Token의 수명을 짧게 설정하여 탈취된 토큰의 유효 기간을 최소화한다.
2. Refresh Token을 사용할 때 추가적인 사용자 인증 단계를 요구한다.
사용자가 민감한 작업을 수행할 때 추가 인증을 요구할 수 있으며
Refresh Token을 사용할 때 토큰을 발급받은 IP 주소 및 디바이스 정보를 확인하여
동일한 조건에서만 토큰을 사용할 수 있도록 제한한다.