회원 가입
Code
Preview
1.
메인 화면
2.
회원 가입
작업 프로세스
1.
프로젝트 생성
2.
프로젝트 설정
3.
ERD
4.
테이블 정의
5.
도메인
6.
데이터
7.
서비스
8.
스프링 시큐리티 설정
9.
요청 경로 매핑
Preview
•
메인 화면
•
회원 가입
메인 화면
회원 가입
작업 프로세스
1.
프로젝트 생성
•
build.gradle•
spring boot version : 3.x.x
•
spring security version : 6.x.x
•
의존성 설정
◦
Spring Web
◦
Spring Boot DevTools
◦
Spring Security
◦
Lombok
◦
Thymeleaf
◦
MySQL Driver
◦
Mybatis Framework
2.
프로젝트 설정
•
application.properties◦
데이터 소스 설정
◦
MyBatis 설정
4.
테이블 정의
•
USER
•
USER_AUTH
5.
도메인
•
Users.java
•
UserAuth.java
6.
데이터
•
UserMapper.xml
•
UserMapper.java
7.
서비스
•
UserService.java
•
UserServiceImpl.java
8.
스프링 시큐리티 설정
•
~/config/SecurityConfig.java•
~/config/CommonConfig.java9.
요청 경로 매핑
•
~/controller/HomeController.java◦
메인 화면
▪
GET
▪
/▪
index.html◦
회원 가입 화면
▪
GET
▪
/join▪
join.html◦
회원 가입 처리
▪
POST
▪
/join▪
/login (로그인으로 이동)▪
/join?error (회원가입으로 다시 이동)◦
아이디 중복 확인
▪
POST
▪
/check/{username}▪
true▪
false프로젝트 생성
build.gradle
spring boot 3.x.x
spring security 6.x.xplugins {
id 'java'
id 'war'
id 'org.springframework.boot' version '3.5.9'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.aloha'
version = '0.0.1-SNAPSHOT'
description = 'Demo project for Spring Boot'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(23)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.5'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.5'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
Java
복사
Spring Boot Version
Language
Group Id
Artifact Id
packaging type
Java version
의존성 설정
1.
Spring Web
2.
Spring boot devtools
3.
Lombok
4.
Thymeleaf
5.
Spring Security
6.
MySQL Driver
7.
Mybatis Framework
Spring Web
Spring boot devtools
Lombok
Thymeleaf
Spring Security
MySQL Driver
Mybatis Framework
프로젝트 설정
application.properties
•
데이터 소스 설정
•
MyBatis 설정
spring.application.name=security6
# 데이터 소스 - MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/aloha?serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&useSSL=false&autoReconnection=true&autoReconnection=true
spring.datasource.username=aloha
spring.datasource.password=123456
# Mybatis 설정
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.type-aliases-package=com.aloha.security6.domain
mybatis.mapper-locations=classpath:mybatis/mapper/**/**.xml
Markdown
복사
ERD
테이블 정의
•
USER
•
USER_AUTH
USER
-- user : 회원
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`no` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '회원번호',
`id` VARCHAR(64) NOT NULL UNIQUE COMMENT '아이디',
`username` VARCHAR(100) NOT NULL COMMENT '사용자명',
`password` VARCHAR(200) NOT NULL COMMENT '비밀번호',
`name` VARCHAR(100) NOT NULL COMMENT '이름',
`email` VARCHAR(200) DEFAULT NULL COMMENT '이메일',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '수정일시',
`enabled` INT DEFAULT 1 COMMENT '활성화여부'
) COMMENT='회원';
SQL
복사
USER_AUTH
-- user_auth : 회원권한
DROP TABLE IF EXISTS `user_auth`;
CREATE TABLE `user_auth` (
`no` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '권한번호',
`id` VARCHAR(64) NOT NULL COMMENT '사용자ID (UK)',
`username` VARCHAR(100) NOT NULL COMMENT '사용자명',
`auth` VARCHAR(100) NOT NULL COMMENT '권한 (ROLE_USER, ROLE_ADMIN, ...)',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '수정일시'
) COMMENT='회원권한';
SQL
복사
도메인
•
Users.java
•
UserAuth.java
Users.java
@Data
@Builder
@AllArgsConstructor
public class Users {
private Long no;
@Builder.Default
private String id = UUID.randomUUID().toString();
private String username;
private String password;
private String name;
private String email;
private Date createdAt;
private Date updatedAt;
private int enabled;
private List<UserAuth> authList;
public Users() {
this.id = UUID.randomUUID().toString();
}
}
Java
복사
UserAuth.java
@Data
@Builder
@AllArgsConstructor
public class UserAuth {
private Long no;
@Builder.Default
private String id = UUID.randomUUID().toString();
private String username;
private String auth;
private Date createdAt;
private Date updatedAt;
public UserAuth() {
this.id = UUID.randomUUID().toString();
}
}
Java
복사
데이터
•
UserMapper.xml
•
UserMapper.java
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.aloha.security.mapper.UserMapper">
<resultMap id="UserMap" type="Users">
<id property="no" column="no"/> <!-- PK -->
<result property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="name" column="name"/>
<result property="email" column="email"/>
<result property="enabled" column="enabled"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
<!-- JOIN 쿼리를 짜고 AuthMap 연결하는 방식 -->
<!-- <collection property="authList" resultMap="AuthMap"></collection> -->
<!-- 별도 SELECT 로 연결 -->
<collection property="authList" ofType="UserAuth"
select="selectAuth"
column="username"></collection>
</resultMap>
<resultMap id="AuthMap" type="UserAuth">
<result property="no" column="no" />
<result property="username" column="username" />
<result property="auth" column="auth" />
</resultMap>
<!-- 회원 가입 -->
<insert id="join">
INSERT INTO user ( id, username, password, name, email )
VALUES ( #{id}, #{username}, #{password}, #{name}, #{email} )
</insert>
<!-- 회원 권한 등록 -->
<insert id="insertAuth">
INSERT INTO user_auth ( id, username, auth )
VALUES ( #{id}, #{username}, #{auth} )
</insert>
<!-- 회원 조회 -->
<!-- <select id="select" resultMap="UserMap">
SELECT u.*
,auth
FROM user u
LEFT JOIN user_auth auth ON u.username = auth.username
WHERE u.username = #{username}
</select> -->
<select id="select" resultMap="UserMap">
SELECT *
FROM user
WHERE username = #{username}
</select>
<select id="selectAuth" resultType="UserAuth">
SELECT *
FROM user_auth
WHERE username = #{username}
</select>
</mapper>
XML
복사
UserMapper.java
@Mapper
public interface UserMapper {
// 회원 조회
public Users select(String id) throws Exception;
// 회원 가입
public int join(Users user) throws Exception;
// 회원 수정
public int update(Users user) throws Exception;
// 회원 권한 등록
public int insertAuth(UserAuth userAuth) throws Exception;
}
Java
복사
서비스
•
UserService.java
•
UserServiceImpl.java
UserService.java
public interface UserService {
// 조회
public Users select(String username) throws Exception;
// 회원 가입
public int join(Users user) throws Exception;
// 회원 수정
public int update(Users user) throws Exception;
// 회원 권한 등록
public int insertAuth(UserAuth userAuth) throws Exception;
}
Java
복사
UserServiceImpl.java
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
// @Autowired private UserMapper userMapper;
// @Autowired private PasswordEncoder passwordEncoder;
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
@Override
public Users select(String username) throws Exception {
Users user = userMapper.select(username);
return user;
}
@Override
@Transactional
public int join(Users user) throws Exception {
String username = user.getUsername();
String password = user.getPassword();
// 123456 -> 🔒 F123N905123890N3138932N4 (암호화)
String encodedPassword = passwordEncoder.encode(password); // 🔒 비밀번호 암호화
user.setPassword(encodedPassword);
// 회원 등록
int result = userMapper.join(user);
if( result > 0 ) {
// 회원 기본 권한 등록
UserAuth userAuth = new UserAuth();
userAuth.setUsername(username);
userAuth.setAuth("ROLE_USER");
result = userMapper.insertAuth(userAuth);
}
return result;
}
@Override
public int update(Users user) throws Exception {
// 비밀번호 변경하는 경우 암호화 처리
String password = user.getPassword();
if( password != null && !password.isEmpty() ) {
String encodedPassword = passwordEncoder.encode(password); // 🔒 비밀번호 암호화
user.setPassword(encodedPassword);
}
int result = userMapper.update(user);
return result;
}
@Override
public int insertAuth(UserAuth userAuth) throws Exception {
int result = userMapper.insertAuth(userAuth);
return result;
}
}
Java
복사
스프링 시큐리티 설정
•
~/config/SecurityConfig.java•
~/config/CommonConfig.java
~/config/SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// 스프링 시큐리티 설정 메소드
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
/// ✅ 인가 설정
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/**").permitAll());
return http.build();
}
/**
* 🍃 암호화 방식 빈 등록
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Java
복사
요청 경로 매핑
•
~/controller/HomeController.java◦
메인 화면
▪
GET
▪
/▪
index.html◦
회원 가입 화면
▪
GET
▪
/join▪
join.html◦
회원 가입 처리
▪
POST
▪
/join▪
/login (로그인으로 이동)▪
/join?error (회원가입으로 다시 이동)◦
아이디 중복 확인
▪
POST
▪
/check/{username}▪
true▪
false
~/controller/HomeController.java
@Slf4j
@Controller
public class HomeController {
@Autowired
private UserService userService;
/**
* 메인 화면
* 🔗 [GET] - /
* 📄 index.html
* @return
*/
@GetMapping("")
public String home() {
log.info(":::::::::: 메인 화면 ::::::::::");
return "index";
}
/**
* 회원 가입 화면
* 🔗 [GET] - /join
* 📄 join.html
* @return
*/
@GetMapping("/join")
public String join() {
log.info(":::::::::: 회원 가입 화면 ::::::::::");
return "join";
}
/**
* 회원 가입 처리
* 🔗 [POST] - /join
* ➡ ⭕ /login
* ❌ /join?error
* @param user
* @return
* @throws Exception
*/
@PostMapping("/join")
public String joinPro(Users user) throws Exception {
log.info(":::::::::: 회원 가입 처리 ::::::::::");
log.info("user : " + user);
int result = userService.join(user);
if( result > 0 ) {
return "redirect:/login";
}
return "redirect:/join?error";
}
/**
* 아이디 중복 검사
* @param username
* @return
* @throws Exception
*/
@ResponseBody
@GetMapping("/check/{username}")
public ResponseEntity<Boolean> userCheck(@PathVariable("username") String username) throws Exception {
log.info("아이디 중복 확인 : " + username);
Users user = userService.select(username);
// 아이디 중복
if( user != null ) {
log.info("중복된 아이디 입니다 - " + username);
return new ResponseEntity<>(false, HttpStatus.OK);
}
// 사용 가능한 아이디입니다.
log.info("사용 가능한 아이디 입니다." + username);
return new ResponseEntity<>(true, HttpStatus.OK);
}
}
Java
복사
index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OAuth</title>
<!-- bootstrap css -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container col-12 col-lg-4">
<div class="px-4 py-5 mt-5 text-center">
<h1 class="display-5 fw-bold text-body-emphasis">메인 화면</h1>
</div>
<!-- 비 로그인 시 -->
<th:block sec:authorize="isAnonymous()">
<div class="d-grid gap-2">
<a href="/login" class="btn btn-lg btn-primary">로그인</a>
<a href="/join" class="btn btn-lg btn-success">회원가입</a>
</div>
</th:block>
<!-- 로그인 시 -->
<th:block sec:authorize="isAuthenticated()">
<div class="card">
<div class="inner p-4">
<div class="d-flex flex-column align-items-center">
<div class="item my-2">
<img th:src="${user.profile}" alt="프로필" class="rounded-circle">
</div>
<div class="item my-2">
<h3 th:text="${user.name}"></h3>
</div>
<div class="item my-2">
<h5 th:text="${user.email}"></h5>
</div>
<div class="item my-2 w-100">
<span sec:authentication="principal">인증된 사용자</span>
</div>
</div>
</div>
</div>
<form action="/logout" method="post">
<!-- CSRF TOKEN -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<div class="d-grid gap-2">
<button type="submit" class="btn btn-lg btn-primary">로그아웃</button>
</div>
</form>
</th:block>
</div>
<!-- bootstrap js -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
HTML
복사
join.html
아이디 중복 검사 : 비동기 요청으로 이미 가입된 아이디가 있는지 확인하는 기능비동기 요청은 JavaScript 로 XMLHttpRequest, fetch, axios 등을 사용할 수 있습니다.
비동기 관련 내용은 아래 페이지를 참조해보세요~!
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>회원가입</title>
<!-- bootstrap css -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container col-12 col-lg-4">
<div class="px-4 py-5 mt-5 text-center">
<h1 class="display-5 fw-bold text-body-emphasis">회원 가입</h1>
</div>
<!-- 회원가입 영역 -->
<main class="form-signin login-box w-100 m-auto">
<form id="form" action="/join" method="post" class="needs-validation" onsubmit="return checkSubmit(event)">
<!-- CSRF TOKEN -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<div class="input-group my-2">
<div class="form-floating" id="box-id">
<input type="text" class="form-control" id="username" name="username" value="" placeholder="아이디" autofocus>
<label for="username">아이디</label>
</div>
<div class="input-group-append">
<button type="button" class="btn btn-lg btn-outline-secondary h-100" onclick="checkId()" >중복확인</button>
</div>
</div>
<div class="form-floating my-2">
<input type="password" class="form-control" id="password" name="password" placeholder="비밀번호">
<label for="password">비밀번호</label>
</div>
<div class="form-floating my-2">
<input type="password" class="form-control" id="passwordCheck" name="passwordCheck" placeholder="비밀번호 확인">
<label for="passwordCheck">비밀번호 확인</label>
</div>
<div class="form-floating my-2">
<input type="text" class="form-control" id="name" name="name" value="" placeholder="이름"
autofocus="">
<label for="name">이름</label>
</div>
<div class="form-floating my-2">
<input type="text" class="form-control" id="email" name="email" value="" placeholder="이메일"
autofocus="">
<label for="email">이메일</label>
</div>
<div class="d-grid gap-2">
<button class="btn btn-lg btn-primary w-100 py-2" type="submit">회원가입</button>
<a href="/" class="btn btn-lg btn-success w-100 py-2">메인</a>
</div>
</form>
</main>
</div>
<!-- bootstrap js -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 💍 CRSF TOKEN
const csrfToken = "[[${_csrf.token}]]"
/*
아이디 중복 확인
*/
async function checkId() {
const username = document.getElementById("username").value;
// null 또는 undefined
if (!username) {
alert("아이디를 입력해주세요");
return;
}
try {
// 아이디 중복 확인
const response = await fetch(`/check/${username}`, {
method: 'GET',
headers: {
'X-CSRF-TOKEN': csrfToken
}
});
if (response.ok) {
const result = await response.text();
let boxId = document.getElementById('box-id');
if (result === 'true') {
alert('사용 가능한 아이디입니다.');
boxId.classList.remove('needs-validation');
boxId.classList.add('was-validated');
return true;
} else {
alert('중복된 아이디입니다.');
boxId.classList.remove('was-validated');
boxId.classList.add('needs-validation');
}
return false;
} else {
alert('아이디 중복 확인 중 오류가 발생했습니다.');
return false;
}
} catch (error) {
console.error('Error:', error);
alert('아이디 중복 확인 중 오류가 발생했습니다.');
return false;
}
}
/*
제출 확인
- 아이디 중복 체크
*/
async function checkSubmit(event) {
event.preventDefault(); // 폼 제출 방지
// 아이디 중복 체크
const isIdAvailable = await checkId();
if (!isIdAvailable) {
return;
}
document.getElementById("form").submit();
}
</script>
</body>
</html>
HTML
복사
































