v2 의 두 문제를 해결한다
이 두 문제를 풀어야 실용적인 인증이 됩니다. 오늘이 그 작업.
pwd VARCHAR(20) 으로는 BCrypt 가 안 들어간다
-- v2 의 스키마
CREATE TABLE mymember (
id VARCHAR(50) PRIMARY KEY,
pwd VARCHAR(20) NOT NULL -- ← 평문 기준
);
BCrypt 해시는 항상 60자. VARCHAR(20) 에 60자를 넣으면 잘려서 들어가거나 에러.
→ 도구 도입 전에 스키마부터 늘려야 합니다.
v2 의 코드에 두 가지 변경:
BCrypt 는 단방향 자물쇠 — 한 번 잠그면 서버도 원본 비밀번호를 모릅니다.
-- v2 → v3: pwd 컬럼 길이 확장
ALTER TABLE mymember MODIFY COLUMN pwd VARCHAR(100) NOT NULL;
<!-- Spring Security Crypto (BCrypt 만 가벼이) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>5.8.5</version>
</dependency>
👉 Spring Security 전체가 아닌 crypto 모듈만. 가볍게 BCrypt 만 쓰기 위함.
package com.smhrd.service;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Service
public class MemberService {
@Autowired
private MemberMapper mapper;
private final BCryptPasswordEncoder encoder
= new BCryptPasswordEncoder();
// 회원가입 — 해시해서 저장
public void signup(Member m) {
String hashed = encoder.encode(m.getPwd());
m.setPwd(hashed);
mapper.insert(m);
}
}
👉 encoder.encode 한 줄로 평문이 단방향 해시로 변환됩니다.
// MemberService 안에 이어서
// 로그인 — 해시 비교
public Member login(String id, String rawPw) {
Member m = mapper.selectOne(id);
if (m == null) return null;
if (encoder.matches(rawPw, m.getPwd())) {
return m;
}
return null;
}
👉 비교는 encoder.matches(평문, 해시) — 직접 해시 비교 금지.
package com.smhrd.controller;
@Controller
public class LoginController {
@Autowired private MemberService service;
@GetMapping("/login")
public String form() { return "login"; }
@PostMapping("/login")
public String login(@RequestParam String id,
@RequestParam String pwd,
HttpSession session) {
Member m = service.login(id, pwd);
if (m == null) return "redirect:/login?error";
session.setAttribute("loginUser", m); // ← 핵심
return "redirect:/";
}
}
👉 session.setAttribute 한 줄이 「페이지 이동해도 풀리지 않는」 인증을 만듭니다.
// LoginController 안에 이어서
@GetMapping("/logout")
public String logout(HttpSession session) {
session.invalidate();
return "redirect:/";
}
👉 invalidate() 는 서버 메모리의 보관함을 통째로 비웁니다. 브라우저의
JSESSIONID 쿠키는 남아 있어도 의미 없는 값이 됩니다.
public void signup(Member m) {
// 평문 그대로!
mapper.insert(m);
}
public void signup(Member m) {
m.setPwd(
encoder.encode(m.getPwd())); // ⭐
mapper.insert(m);
}
👉 변경량: 한 줄. encode 호출만 끼워 넣음.
public Member login(String id, String pwd) {
Member m = mapper.selectOne(id);
if (m != null
&& m.getPwd().equals(pwd))
return m;
return null;
}
public Member login(String id, String pwd) {
Member m = mapper.selectOne(id);
if (m != null
&& encoder.matches(pwd,
m.getPwd())) // ⭐
return m;
return null;
}
👉 변경량: 한 줄. equals → encoder.matches.
// Controller
public String login(...) {
Member m = service.login(...);
if (m != null) return "redirect:/";
return "redirect:/login?error";
// 세션 저장 X
}
public String login(..., HttpSession s) {
Member m = service.login(...);
if (m != null) {
s.setAttribute("loginUser", m); // ⭐
return "redirect:/";
}
return "redirect:/login?error";
}
👉 변경량: 한 줄. 합계 3 줄로 v2 → v3.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:choose>
<c:when test="${not empty sessionScope.loginUser}">
<span>${sessionScope.loginUser.id}님</span>
<a href="/logout">로그아웃</a>
</c:when>
<c:otherwise>
<a href="/login">로그인</a>
<a href="/signup">회원가입</a>
</c:otherwise>
</c:choose>
👉 sessionScope.loginUser 로 JSP 의 EL 에서 세션 접근. (v4 에서 nick 추가 후엔 표시명을 loginUser.nick 으로.)
SELECT id, pwd FROM mymember;
값이 $2a$10$... 형태로 저장됐는지 확인 (60자 길이)| 실험 | 증상 |
|---|---|
encoder.encode 빼기 | 평문 저장 → v2 의 함정 재현 |
encoder.matches → equals | 로그인 항상 실패 (해시는 평문과 다름) |
session.setAttribute 빼기 | 로그인 페이지에선 성공해도 다른 페이지에선 풀림 |
invalidate() 빼기 | 로그아웃해도 사용자 정보가 안 사라짐 |
if (session.getAttribute("loginUser") == null) redirect 를 직접 써야 함→ 다음 차시 v4 깔끔한 로그인 — 인터셉터로 자동화.
오늘이 우리 프로젝트의 다섯 번째 마일스톤:
v3 부터 우리 사이트는 실제로 사용 가능한 인증을 갖춥니다.
평문 저장 + 무상태. 보안적으로 위험하고, 페이지 이동 시 로그인 풀림.
BCrypt 단방향 해시 + HttpSession. 평문은 어디에도 안 남고, 페이지 이동 시 로그인 유지.
encoder.encode, encoder.matches 두 메서드session.setAttribute / invalidate — 로그인 유지·해제다음: v4 깔끔한 로그인 — 인터셉터로 가드를 자동화.