| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- junit5
- 자바스크립트
- input
- math
- Visual Studio Code
- 단위테스트
- 배열
- vscode
- html
- js
- CSS
- junit
- 자바문법
- 테스트자동화
- javascript
- Java
- ArrayList
- 이클립스
- Array
- list
- IntelliJ
- 문자열
- 정규식
- 자바
- java테스트
- HashMap
- 인텔리제이
- json
- string
- Eclipse
- Today
- Total
어제 오늘 내일
[Spring Security] 5편 - 회원가입 & 비밀번호 암호화 (BCrypt) 본문
지금까지는 서버가 켜질 때 자동으로 생성되는 admin 계정만 사용했습니다.
이번 5편에서는 사용자가 직접 아이디와 비밀번호를 입력해 가입하고, 그 비밀번호를 안전하게 암호화하여 DB에 저장하는 기능을 구현합니다.
특히 마지막에는 DB를 직접 조회해서 비밀번호가 정말 암호화되었는지 확인해보겠습니다.
Step 1. SecurityConfig 수정 (H2 Console 허용)
나중에 DB에 데이터가 잘 들어갔는지 확인하려면 H2 Console(localhost:8080/h2-console)에 접속해야 합니다. 하지만 시큐리티는 기본적으로 이 경로도 막아버립니다.
접속을 허용하고, 화면이 깨지지 않도록 설정을 추가하겠습니다.
- 위치:
src/main/java/com/example/board/config/SecurityConfig.java
package com.example.board.config;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest; // [중요] 추가하세요
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf((csrf) -> csrf.disable())
// ▼▼▼ 1. H2 Console 화면 깨짐 방지 (Frame 허용) ▼▼▼
.headers((headers) -> headers
.frameOptions((frame) -> frame.sameOrigin())
)
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/", "/login", "/join", "/test").permitAll()
// ▼▼▼ 2. H2 Console 접속 허용 (시큐리티 무시) ▼▼▼
.requestMatchers(PathRequest.toH2Console()).permitAll()
.anyRequest().authenticated()
)
.formLogin((form) -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/", true)
.permitAll()
)
.logout((logout) -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
💡 코드 상세 해석
PathRequest.toH2Console():/h2-console/**하위 모든 요청을 가리킵니다. 이를permitAll()하여 로그인 검사를 면제해줍니다.frameOptions().sameOrigin(): H2 Console은 내부적으로<iframe>태그를 사용하는데, 시큐리티는 보안상 이를 차단합니다. 같은 도메인 내에서는 허용하도록 설정을 푼 것입니다.
Step 2. 회원가입용 DTO 만들기
Entity(Member)를 컨트롤러에서 직접 노출하지 않고, 가입용 데이터를 전달받을 객체(DTO)를 따로 만듭니다.
- 위치:
src/main/java/com/example/board/dto/MemberJoinDto.java
package com.example.board.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class MemberJoinDto {
private String username;
private String password;
}
Step 3. 회원가입 로직 만들기 (MemberService)
비밀번호 암호화의 핵심 로직이 들어있는 곳입니다. SecurityConfig에서 등록한 PasswordEncoder를 주입받아 사용합니다.
- 위치:
src/main/java/com/example/board/service/MemberService.java
package com.example.board.service;
import com.example.board.domain.entity.Member;
import com.example.board.domain.repository.MemberRepository;
import com.example.board.dto.MemberJoinDto;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder; // 암호화 객체 주입
public void join(MemberJoinDto dto) {
// 1. 아이디 중복 체크
if (memberRepository.findByUsername(dto.getUsername()).isPresent()) {
throw new IllegalStateException("이미 존재하는 아이디입니다.");
}
// 2. [핵심] 비밀번호 암호화
// 사용자가 입력한 "1234"를 "$2a$10$..." 형태의 암호문으로 변환합니다.
String encodedPassword = passwordEncoder.encode(dto.getPassword());
// 3. DB 저장
Member member = Member.builder()
.username(dto.getUsername())
.password(encodedPassword) // 암호화된 비번 저장
.role("USER")
.build();
memberRepository.save(member);
}
}
Step 4. 컨트롤러 연결하기 (MemberController)
- 위치:
src/main/java/com/example/board/controller/MemberController.java
package com.example.board.controller;
import com.example.board.dto.MemberJoinDto;
import com.example.board.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@RequiredArgsConstructor
@Controller
public class MemberController {
private final MemberService memberService;
// 기존 로그인 페이지 매핑
@GetMapping("/login")
public String login() {
return "login";
}
// 회원가입 페이지 이동
@GetMapping("/join")
public String joinForm() {
return "join"; // templates/join.html
}
// 회원가입 처리
@PostMapping("/join")
public String join(MemberJoinDto dto) {
try {
memberService.join(dto); // 서비스 호출
} catch (IllegalStateException e) {
return "redirect:/join?error=duplicate"; // 중복 아이디 발생 시
}
return "redirect:/login"; // 성공 시 로그인 페이지로
}
}
Step 5. 회원가입 화면 만들기 (join.html)
로그인 화면과 비슷하지만 action이 /join입니다.
- 위치:
src/main/resources/templates/join.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>회원가입</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.join-form { width: 300px; margin: 100px auto; }
</style>
</head>
<body>
<div class="container join-form">
<h2 class="text-center mb-4">📝 회원가입</h2>
<div th:if="${param.error}" class="alert alert-danger">
이미 사용 중인 아이디입니다.
</div>
<form action="/join" method="post">
<div class="mb-3">
<label for="username" class="form-label">아이디</label>
<input type="text" class="form-control" id="username" name="username" placeholder="아이디" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">비밀번호</label>
<input type="password" class="form-control" id="password" name="password" placeholder="비밀번호" required>
</div>
<button type="submit" class="btn btn-primary w-100">가입하기</button>
</form>
<div class="text-center mt-3">
<a href="/login">로그인 하러가기</a>
</div>
</div>
</body>
</html>
Step 6. 기능 동작 테스트 (웹 화면)
자, 이제 코드는 다 짰으니 실제로 사용자가 되어 회원가입부터 로그인까지의 흐름을 타봅시다.
- 서버 실행 (Run)
- 프로젝트를 다시 시작합니다.
- 메인 화면 접속
http://localhost:8080에 접속해서 우측 상단의 [회원가입] 버튼을 클릭합니다.
- 회원가입 진행
- 아이디:
dev_user - 비밀번호:
1234 - 입력 후 [가입하기] 버튼을 누릅니다.
- 아이디:
- 로그인 화면 이동
- 가입이 성공하면 자동으로 로그인 페이지로 이동됩니다.
- 로그인 시도
- 방금 가입한
dev_user/1234를 입력하고 로그인을 누릅니다.
- 방금 가입한
- 성공 확인
- 다시 메인 화면으로 돌아왔을 때, 우측 상단에 "dev_user님 환영합니다!" 라는 문구가 뜨면 성공입니다!
Step 7. 진짜 암호화됐을까? (DB 데이터 검증)
"로그인은 잘 되는데, 내 비밀번호 1234가 DB에 그대로 들어있는 건 아니겠지?"
의심 많은 개발자를 위해 DB 내부를 직접 확인해 보겠습니다.
- H2 Console 접속
- 브라우저 새 탭을 열고
http://localhost:8080/h2-console에 접속합니다. - (Step 1에서 설정을 추가했으므로 로그인 창으로 튕기지 않고 접속되어야 합니다.)
- 브라우저 새 탭을 열고
- DB 연결
JDBC URL이jdbc:h2:mem:testdb인지 확인하고 [Connect] 버튼을 클릭합니다.
- 데이터 조회
- SQL 입력창에 아래 명령어를 치고 [Run]을 누릅니다.
SELECT * FROM MEMBER;
- 결과 확인 (충격과 공포?)
| ID | USERNAME | PASSWORD | ROLE |
| 1 | admin | $2a$10$r6.f... |
USER |
| 2 | dev_user | $2a$10$Xk9z... |
USER |
보시다시피 dev_user의 비밀번호 칸에 1234는 온데간데없고, $2a$10$으로 시작하는 알 수 없는 외계어(해시값)가 들어있습니다.
이것이 바로 BCrypt 암호화의 힘입니다.
이제 DB 관리자조차 여러분의 비밀번호가 무엇인지 알 수 없게 되었습니다. 보안 성공! 🎉
마무리
오늘 우리는 "사용자 입력 -> 컨트롤러 -> 서비스(암호화) -> DB 저장"으로 이어지는 완벽한 회원가입 프로세스를 구축했습니다.
그리고 H2 Console을 열어 비밀번호가 안전하게 암호화되어 저장된 것까지 직접 눈으로 확인했습니다.
이제 이 프로젝트는 안전한 회원가입과 로그인이 가능한 어엿한 웹 애플리케이션이 되었습니다.
다음 6편에서는 "세션(Session) 방식과 JWT(Token) 방식의 차이"에 대해 알아보고,
요즘 트렌드인 API 보안으로 넘어가는 준비를 해보겠습니다.
고생하셨습니다!
'IT > SpringBoot' 카테고리의 다른 글
| [Spring Security] 7편 - JWT 로그인을 위한 준비 & TokenProvider 만들기 (0) | 2026.02.19 |
|---|---|
| [Spring Security] 6편 - 세션(Session) vs 토큰(JWT), 도대체 뭐가 다를까? (0) | 2026.02.19 |
| [Spring Security] 4편 - 로그인 페이지 커스텀 & 로그아웃 처리 (0) | 2026.02.18 |
| [Spring Security] 3편 - 내 DB 정보로 로그인하기 (UserDetailsService) (0) | 2026.02.17 |
| [Spring Security] 2편 - "Hello Security" - 기본 설정과 비밀번호 암호화 (0) | 2026.02.17 |
