가장 많이 쓰이는 암호화 방식 중에 하나인 해싱은 암호화만 가능하고 복호화는 불가능합니다.
해싱의 목적은 데이터를 사용하는 것이 아니라, 동일한 값의 데이터를 사용하고 있는지 여부만 확인하는 것이 목적이기 때문에 유저의 비밀번호를 몰라도 해싱한 값이 일치하는지를 판단하여 비밀번호를 확인할 수 있고, 비밀번호를 모르기 때문에 위험도도 줄어들게 됩니다.
해싱은 해시 함수(Hash Function)를 사용하여 암호화를 진행하는데, 해시 함수는 입력 데이터를 해시값으로 매핑하는 역할을 합니다.
<해싱함수의 특징>
- 항상 고정된 크기의 출력 값을 생성합니다.
예를 들어, 256비트 해시 함수는 항상 256비트 크기의 해시 값을 반환합니다.
결과값이 문자열이라면 문자 길이는 항상 동일합니다.- 다양한 입력에 대해 고르게 분포된 출력을 합니다.
서로 다른 문자열에 동일한 해시 함수를 사용하면 반드시 다른 결과값이 나옵니다.
입력 데이터의 작은 변화도 해시값의 크게 달라지도록 보장합니다.- 충돌방지
충돌은 서로 다른 입력에 대해 동일한 해시값이 생성되는 현상을 말합니다. 좋은 해시 함수는 충돌이 발생할 확률을 최소화하여 보장합니다. 그러나 완전히 충돌을 피하는 것은 불가능하며, 충돌이 발생할 수 있는 경우에도 안전한 시스템을 구축하기 위해 충분한 크기의 해시값을 사용합니다.- 동일한 문자열에 동일한 해시 함수를 사용하면 항상 같은 결과값이 나옵니다.
해시 함수는 일방향 함수로, 해시값을 통해 입력 데이터를 다시 복원할 수 없습니다. 즉, 해시값으로부터 원래 데이터를 유추하는 것은 매우 어렵습니다. 하지만 항상 같은 결과값이 나온다는 특성을 이용해 해시 함수를 거치기 이전의 값을 알아낼 수 있도록 기록해 놓은 표인 레인보우 테이블이 존재합니다. 레인보우 테이블에 기록된 값의 경우에는 유출이 되었을 때 해싱을 했더라도 해싱 이전의 값을 알아낼 수 있으므로 보안상 위협이 될 수 있습니다.

그래서 해싱 이전 값에 임의의 값을 더해 데이터가 유출되더라도 해싱 이전의 값을 알아내기 더욱 어렵게 만드는 방법인 솔트(Salt)가 있습니다. 솔트를 사용하게 되면 해싱 값이 유출되더라도, 솔트가 함께 유출된 것이 아니라면 암호화 이전의 값을 알아내는 것은 불가능에 가까워집니다.
| 비밀번호 + 솔트 | 해시 함수(SHA1) 리턴 값 예시 |
|---|---|
| 'password' + 'salt' | 'C88E9C67041A74E0357BEFDFF93F87DDE0904214' |
| 'Password' + 'salt' | '38A8FDE622C0CF723934BA7138A72BEACCFC69D4' |
| 'kimcoding' + 'salt' | '8607976121653D418DDA5F6379EB0324CA8618E6' |
HTTP 프로토콜은 상태가 없는(stateless) 특징이 있는데 쿠키를 통해 상태 정보를 기억시켜주기 때문에 두 요청이 동일한 브라우저에서 들어왔는지 아닌지를 판단할 때 주로 사용합니다.
HTTP 쿠키(웹 쿠키, 브라우저 쿠키)는 서버가 사용자의 웹 브라우저에 전송하는 작은 데이터 조각입니다. 브라우저는 그 데이터 조각들을 저장해 놓았다가, 동일한 서버에 HTTP 요청 시 저장된 쿠키 데이터를 함께 전송합니다. 모든 요청마다 쿠키도 함께 전송되기 때문에, (특히 mobile data connections에서) 성능이 떨어지는 원인이 될 수 있습니다.
쿠키의 목적
- 세션 관리(Session management)
서버에 저장해야 할 로그인, 장바구니, 게임 스코어 등의 정보 관리- 개인화(Personalization)
사용자 선호, 테마 등의 세팅- 트래킹(Tracking)
사용자 행동을 기록하고 분석하는 용도
HTTP 요청을 수신할 때, 서버는 응답과 함께 Set-Cookie 헤더를 전송할 수 있습니다. 쿠키는 보통 브라우저에 의해 저장되며, 그 후 쿠키는 같은 서버에 의해 만들어진 요청(Request)들의 Cookie HTTP 헤더안에 포함되어 전송됩니다.
// 쿠키 만드는법
Set-Cookie: <cookie-name>=<cookie-value>
// 예시) 이 서버 헤더는 클라이언트에게 쿠키를 저장하라고 전달합니다.
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
// 이제 서버로 요청시 브라우저는 Cookie 헤더를 사용하여 저장했던 모든 쿠키들을 회신할 것입니다.
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
서버는 쿠키를 이용하여 데이터를 저장하고 원할 때 이 데이터를 다시 불러와 사용할 수 있습니다.
하지만 데이터를 저장한 이후 특정 조건들이 만족하는 경우에만 다시 가져올 수 있습니다.
이런 조건들은 쿠키 옵션으로 표현할 수 있습니다.
| Cookie Options | ||
|---|---|---|
| Domain | 서버와 '요청 도메인'이 일치하면 쿠키 전송 | naver에서 받은 쿠키 google전송 불가 |
| Path | 서버와 '요청 세부경로'가 일치하면 쿠키 전송 | Path에 설정된 URL를 만족하면 쿠키 전송 |
| MaxAge or Expires | 쿠키의 유효기간 설정 | 세션쿠키와 영속성 쿠키로 나누어짐 |
| HTTP Only | 스크립트의 쿠키 접근 가능 여부 결정 | true인 경우, 자바스크립트에서 쿠키 접근불가 |
| Secure | HTTPS 프로토콜에서만 쿠키 전송 여부 결정 | true로 설정된 경우 HTTPS만 쿠키 전송 |
| SameSite | CORS 요청의 경우 옵션 및 메서드에 따라 쿠키 전송 여부 결정 | Lax, Strict, None |
웹 애플리케이션에서 정보나 데이터를 저장할 때는 저장 위치에 대한 신중한 고려가 필요합니다. 이때 두 가지 주요 기준점을 고려해야 합니다:
웹 브라우저를 통해 클라이언트 측에서 데이터를 저장할 수 있는 방법들:
1. Cookie
2. Local Storage
1. Server Memory - Session 객체
2. Server Memory - Application 객체
3. Server 영속적 저장 - 파일
4. DB Server
쿠키 생성 및 전송
<%
// 서버에서 쿠키 생성
Cookie mycookie = new Cookie("cname","1004");
// 클라이언트에게 쿠키 전달 (서버 -> 클라이언트)
response.addCookie(mycookie);
%>
위 코드는 서버 측에서 쿠키를 생성하고 HTTP 응답을 통해 클라이언트에게 전달하는 기본적인 방법을 보여줍니다. 쿠키가 생성된 후에는 클라이언트의 브라우저에 저장되며, 이후 동일한 서버로의 요청 시 자동으로 함께 전송됩니다.
쿠키 읽기 및 정보 확인
서버에 접속한 클라이언트가 가지고 있는 도메인별로 기록되어 있는 쿠키를 읽어오는 방법:
<%
// 현재 도메인에 해당되는 모든 쿠키 정보 가져오기
Cookie[] cs = request.getCookies();
if(cs != null && cs.length > 0){
for(Cookie c : cs){
out.print(c.getName() + " > 쿠키 이름<br>");
out.print(c.getValue() + " > 쿠키 값<br>");
out.print(c.getMaxAge() + " > 만료 시간 (초)<br>"); // -1이면 세션 쿠키
out.print(c.getDomain() + " > 도메인<br>");
out.print("<hr>");
}
}
%>
쿠키 속성별 의미
getName(): 쿠키의 이름을 반환getValue(): 쿠키에 저장된 값을 반환getMaxAge(): 쿠키의 만료 시간을 초 단위로 반환-1: 세션 쿠키 (브라우저 종료 시 삭제)0: 즉시 삭제양수: 해당 초 후 만료getDomain(): 쿠키가 유효한 도메인을 반환이 방식을 통해 서버는 클라이언트로부터 전송받은 모든 쿠키 정보를 확인하고, 필요한 데이터를 추출하여 사용자 식별이나 상태 유지 등의 작업을 수행할 수 있습니다.
쿠키를 사용하는 것은 보안에 취약하기 때문에 민감한 정보는 쿠키에 담아서는 안됩니다.
쿠키가 데이터를 클라이언트에 저장하는 방식이었다면, 세션은 데이터를 서버에 저장하고, 데이터에 대한 ID를 암호화하여 쿠키에 담아서 클라이언트에 전달하여 사용하는 방식입니다.
세션 전달을 통해 서버는 클라이언트의 상태와 데이터를 유지하고, 클라이언트와의 연속성을 유지할 수 있습니다. 이를 통해 로그인 상태 유지, 장바구니 기능, 사용자 지향적인 경험 제공 등을 구현할 수 있습니다.
웹 서버(WAS)에는 두 가지 중요한 메모리 객체가 존재합니다:
1. 클라이언트의 첫 번째 요청
클라이언트 → 서버: http://192.168.0.29:8090/WebJSP/_Session.jsp 요청
2. 서버의 응답과 세션 생성
JSESSIONID: 43AE93C6D0A5CD99BBCF73149DC1FB143. 세션 저장과 동기화
4. 후속 요청 처리
클라이언트가 추가 요청을 보낼 때마다:
<%
Date time = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
%>
<h3>Session 객체 정보</h3>
session 객체 식별값: <%=session.getId() %>
<%
time.setTime(session.getCreationTime());
%>
[session 생성된 시간]: <%= formatter.format(time) %>
<%
time.setTime(session.getLastAccessedTime());
%>
[session 마지막 접속 시간]: <%= formatter.format(time) %>
이 코드는 실제로 세션의 핵심 정보들을 보여줍니다:
session.getId(): 고유한 세션 식별자session.getCreationTime(): 세션이 생성된 시간session.getLastAccessedTime(): 클라이언트가 마지막으로 접속한 시간로그아웃은 사용자가 현재 세션 또는 인증된 상태를 종료하고, 웹 애플리케이션에서 더 이상 접근할 수 없도록 하는 동작입니다.
session.invalidate())클라이언트: 로그아웃 버튼 클릭
→ 서버: key(Session ID)를 가져와서 해당 session 객체를 찾음
→ 서버: session.invalidate()로 세션 객체 소멸
→ 클라이언트: 쿠키 갱신으로 세션 ID 무효화
개발자로서 이해해야 할 중요한 개념은 세션의 스코프 범위입니다:
// 세션에 데이터 저장
session.setAttribute("userId", "admin123");
session.setAttribute("userName", "홍길동");
// 다른 페이지에서도 접근 가능
String userId = (String) session.getAttribute("userId");
// 공통적으로 사용되는 로그인 체크 예시
<%
String loginUserId = (String) session.getAttribute("userId");
if (loginUserId == null) {
// 로그인 페이지로 리다이렉트
response.sendRedirect("login.jsp");
return;
}
%>
그렇다고 모든 정보를 세션에 다 담아버리는 것은 위험합니다. 세션에 저장되는 데이터가 많을수록 서버 메모리가 그만큼 더 소모되기 때문입니다.
피해야 할 세션 사용:
토큰 기반 인증 시스템(JWT 등)을 사용하면 세션의 일부 단점을 보완할 수 있습니다.
접속자 1000명이 있는 경우:
이처럼 세션은 웹 애플리케이션에서 사용자별 상태 관리를 위한 핵심 기술로, 쿠키의 보안 문제를 해결하면서도 효과적인 사용자 경험을 제공하는 중요한 메커니즘입니다.