Search
Duplicate

νšŒμ› κ°€μž…

νšŒμ› κ°€μž…

Spring Security 6

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.java
9.
μš”μ²­ 경둜 λ§€ν•‘
β€’
~/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.x
plugins { 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
볡사

λ‹€μŒ 주제