๐งค Blog
๐ application.yml
server:
port: 8000 # ์๋ฒ ํฌํธ์ค์
servlet:
context-path: /blog # ์ง์
์ : localhost:8000/blog/
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver # mysql ์ค์
url: jdbc:mysql://localhost:3306/blog?serverTimezone=Asia/Seoul
username: root
password:
jpa: # jpa ์ค์
open-in-view: true # lazy Loading
hibernate:
ddl-auto: update # create:์์ฑ๋ชจ๋, update:์
๋ฐ์ดํธ๋ชจ๋, none: ์์ฑ,์
๋ฐ์ดํธ (x)
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl # ํ
์ด๋ธ์ ๋ง๋ค๋ ๋ณ์๋ช
๊ทธ๋๋ก ํ
์ด๋ธ์ ๋ฃ์ด์ค.
use-new-id-generator-mappings: false # false: jpa๊ฐ ์ฌ์ฉํ๋ ๊ธฐ๋ณธ ๋๋ฒ๋ง ์ ๋ต์ ๋ฐ๋ผ๊ฐ์ง ์๋๋ค.
show-sql: true # ์ฟผ๋ฆฌ ๋ณด์ฌ์ฃผ๊ธฐ
properties:
hibernate.format_sql: true # ์ฟผ๋ฆฌ ์์๊ฒ ๋ณด์ฌ์ฃผ๊ธฐ
jackson:
serialization:
fail-on-empty-beans: false
๐ Http ํต์
-
ํจํท ์ค์์นญ
A๊ฐ B์๊ฒ '๊ฐ๋๋ค๋ผ'๋ผ๋ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ผ ๋ '๊ฐ','๋','๋ค','๋ผ ์ด๋ ๊ฒ ํจํท๋จ์๋ก ์ชผ๊ฐ์ ๋ณด๋ด์ ์ ์กํ๋ค.
C๋ผ๋ ์ ๊ฐ B์๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๊ณ ์ถ์ ๊ฒฝ์ฐ์๋ A์ ์ ์ ๊ณต์ ํ๋ค.
๋จ์ : ๋์์ ๋ณด๋ด๊ฒ ๋ ๊ฒฝ์ฐ, ํ๋์ ์ ์ ์ฌ์ฉํ๋ฏ๋ก A์ C์ ๋ฐ์ดํฐ๊ฐ ์์ด๊ฒ ๋๋ค.
๊ทธ๋์ ํด๊ฒฐ์ฑ ์ผ๋ก ํจํท์ ํค๋๋ฅผ ์ถ๊ฐ์์ผ ์ด๋ค ๊ณณ์์ ์จ ๋ ์์ธ์ง ๊ตฌ๋ถํ๋ค.
-
์ํท ์ค์์นญ
A์ B๊ฐ ๋ฐ์ดํฐ๋ฅผ ๊ตํํ ๋ ๋ฐ์ดํฐ๋ฅผ ํ๋ฐฉ์ ์ค์ด์ ์ ์กํ๋ค.
๋จ์ : C๋ผ๋ ์ ๊ฐ B์๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๊ณ ์ถ์ผ๋ฉด ์ ์ ํ๋ ์ถ๊ฐํด์ผํ๋ค.
์๋๋ ๋น ๋ฅด์ง๋ง, ๋น์ฉ์ด ๋ง์ด ๋ฌ.
๐ MIME ํ์
MIME ํ์ ์ด๋ ํด๋ผ์ด์ธํธ์๊ฒ ์ ์ก๋ ๋ฌธ์์ ๋ค์์ฑ์ ์๋ ค์ฃผ๊ธฐ ์ํ ๋ฉ์ปค๋์ฆ์ ๋๋ค.
๋ฌธ๋ฒ
type/subtype
'/'๋ก ๊ตฌ๋ถ๋ ๋๊ฐ์ ๋ฌธ์์ด์ธ ํ์ ๊ณผ ์๋ธํ์ ์ผ๋ก ๊ตฌ์ฑ๋๋ค. (์คํ์ด์ค๋ ํ์ฉ X)
๊ฐ๋ณํ์
text/plain
text/html
image/jpeg
image/png
audio/mpeg
audio/ogg
audio/*
video/mp4
application/octet-stream
...
MIME ํ์ ์ ์ฒด๋ชฉ๋ก : https://developer.mozilla.org/ko/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_type
๐ Http ์์ฒญ ์ค์ต
@Controller // ์ฌ์ฉ์๊ฐ ์์ฒญํ๋ฉด HTMLํ์ผ์ ์๋ต
@RestController // ์ฌ์ฉ์๊ฐ ์์ฒญํ๋ฉด Data๋ฅผ ์๋ต
// ์ฌ์ฉ์๊ฐ ์์ฒญ -> ์๋ต(HTMLํ์ผ)
// @Controller
// ์ฌ์ฉ์๊ฐ ์์ฒญ -> ์๋ต(Data)
@RestController
public class HttpController {
// ์ธํฐ๋ท ์์ฒญ์ ๋ฌด์กฐ๊ฑด get๋ง ๊ฐ๋ฅ
// http://localhost:8080/http/get (select)
@GetMapping("/http/get")
public String getTest(){
return "get ์์ฒญ";
}
// http://localhost:8080/http/post (insert)
@PostMapping("/http/post")
public String postTest(){
return "post ์์ฒญ";
}
// http://localhost:8080/http/put (update)
@PutMapping("/http/put")
public String putTest(){
return "put ์์ฒญ";
}
// http://localhost:8080/http/delete (delete)
@DeleteMapping("/http/delete")
public String deleteTest(){
return "delete ์์ฒญ";
}
}
์๋ฐ์์ ๋ณ์๋ ๋ค private์ผ๋ก ๋ง๋ ๋ค.
๊ทธ ์ด์ ๋ ๋ณ์์ ์ง์ ์ ๊ทผํด์ ๊ฐ์ ์์ ํ๋ ๊ฒ์ ๊ฐ์ฒด์งํฅ์ ๋ง์ง ์์ผ๋ฏ๋ก
๋ฉ์๋๋ฅผ public์ผ๋ก ๋ง๋ค์ด ๋ฉ์๋๋ก ๊ฐ์ ๋ณ๊ฒฝํ๋๋ก ํด์ค๋ค.
@GetMapping
@RequestParam("๋ณ์๋ช ") : get์์ฒญ์ผ๋ก ๋ค์ด์จ ๋ณ์๋ฅผ ์ฝ์ด๋ค์ผ ์ ์๋ค.
๋งค๊ฐ๋ณ์๋ฅผ ์๋์ ๊ฐ์ด ๊ฐ์ฒด๋ก ๋ฐ์ผ๋ฉด ํ๊บผ๋ฒ์ ๋ฐ์ ์ ์๋ค. (setter ํ์ํจ)
...
@GetMapping("/http/get")
public String get(Member m){
return m.getId() + m.getUsername();
}
...
@PostMapping
PostMapping ๋ํ ๊ฐ์ฒด๋ก ๋งค๊ฐ๋ณ์๋ฅผ ๋ฐ์ ์ ์๋ค. (html์์ formํ๊ทธ๋ก ๋ณด๋์๋)
JSON ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊ธฐ ์ํด์๋ @ResponseBody ํ๊ทธ๋ฅผ ์จ์ค์ผํ๋ค.
// http://localhost:8080/http/post (insert)
@PostMapping("/http/post")
public String postTest(@RequestBody Member m){
return "post ์์ฒญ" + m.getId();
}
...
์ ์ฝ๋๋ฅผ ์คํํด์ JSON ํ์์ผ๋ก POST์์ฒญ์ ๋ณด๋ด๋ฉด JSON์ด ์๋ฐ์ Member ๊ฐ์ฒด๋ก ์ ๋ค์ด๊ฐ๋ค.
์ด ๊ณผ์ ์ฆ, JSON => ์๋ฐ ๊ฐ์ฒด ๋ก ๋ฐ๊พธ์ด์ฃผ๋ ๊ณผ์ ์ ์คํ๋ง ๋ถํธ์ MessageConverter๊ฐ ์ํํ๋ค.
๋๋จธ์ง @PutMapping๊ณผ @DeleteMapping๋ ๋ง์ฐฌ๊ฐ์ง๋ก @PostMapping๊ณผ ๋น์ทํ ์ผ์ ์ํํ๋ค.
๐ AJAX ์์ฒญ
์ผ๋จ, ajax ์์ฒญ์ ํ๊ธฐ ์ํด์๋ ์๋ฐ์คํฌ๋ฆฝํธ์ ๋์์ด ํ์ํ๋ค.
staticํด๋์ js ํด๋๋ฅผ ํ๋ ๋ง๋ค์ด user.js๋ฅผ ๋ง๋ค์.
// ํด๋ ๊ฒฝ๋ก : /resources/static/js/user.js
let index = {
init:function(){
// btn-save ๋ฒํผ์ด ํด๋ฆญ๋๋ฉด, saveํจ์๋ฅผ ํธ์ถ
document.querySelector("#btn-save").addEventListener('click',()=>{
this.save();
});
},
save:function(){
let data = {
username: document.querySelector("#username").value,
password: document.querySelector("#password").value,
email: document.querySelector("#email").value
}
// console.log(data);
// ajax ์์ฒญ
fetch('/blog/api/user',{
method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(data)
})
.then(response => response.json())
.then(data=>{
alert("ํ์๊ฐ์
์๋ฃ");
console.log(data);
// location.href="/blog";
})
.catch(error=>{alert(error.message)});
}
};
index.init();
์์ ๊ฐ์ด jQuery ๋์ ์ Javascript๋ก ajax ์์ฒญ์ ํ๊ธฐ์ํด์๋ fetch ํจ์๊ฐ ํ์ํ๋ค.
fetchํจ์๋ Promise๊ฐ์ฒด๋ก ๋ฆฌํด์ด ๋๊ธฐ ๋๋ฌธ์, ๋ณ๋์ response.json()๊ณผ ๊ฐ์ด Promise๊ฐ์ฒด => json ์ผ๋ก์ ๋ณํ์ด ํ์ํ๋ค.
ajax ์ฌ์ฉ ์ ์ฅ์
- ์นํ์ด์ง์ ์๋๊ฐ ํฅ์๋๋ค.
- ์๋ฒ์ ์ฒ๋ฆฌ๊ฐ ์๋ฃ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค.
- ์๋ฒ์์ Data๋ง ์ ์กํ๋ฉด ๋๋ฏ๋ก ์ ์ฒด์ ์ธ ์ฝ๋ฉ์ ์์ด ์ค์ด๋ ๋ค.
- ๊ธฐ์กด ์น์์๋ ๋ถ๊ฐ๋ฅํ๋ ๋ค์ํ UI๋ฅผ ๊ฐ๋ฅํ๊ฒ ํด์ค๋ค.
ajax ์ฌ์ฉ ์ ๋จ์
- ํ์คํ ๋ฆฌ ๊ด๋ฆฌ๊ฐ ๋์ง ์๋๋ค.
- ํ์ด์ง ์ด๋ ์๋ ํต์ ์ผ๋ก ์ธํ ๋ณด์์์ ๋ฌธ์ ๊ฐ ์๋ค.
- ์ฐ์์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๋ฉด ์๋ฒ ๋ถํ๊ฐ ์ฆ๊ฐํ ์ ์๋ค.
- ajax๋ฅผ ์ธ ์ ์๋ ๋ธ๋ผ์ฐ์ ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
- HTTP ํด๋ผ์ด์ธํธ์ ๊ธฐ๋ฅ์ด ํ์ ๋์ด ์๋ค.
- ์ง์ํ๋ Charset์ด ํ์ ๋์ด ์๋ค.
- script๋ก ์์ฑ๋๋ฏ๋ก ๋๋ฒ๊น ์ด ์ฉ์ดํ์ง ์๋ค.
- ๋์ผ-์ถ์ฒ ์ ์ฑ ์ผ๋ก ๋ค๋ฅธ ๋๋ฉ์ธ๊ณผ๋ ํต์ ์ด ๋ถ๊ฐ๋ฅํ๋ค.
๐ ์คํธ๋ง ๋ถํธ์ ํธ๋์ญ์
์ ํต์ ์ธ ๋ฐฉ์์ ์์์ฑ ์ปจํ ์คํธ, JDBC, ํธ๋์ญ์ ์ ๋ชจ๋ ๊ฐ์ ๊ตฌ๊ฐ์์ ์ฐ๊ฒฐํ์ง๋ง,
-
lazy Loading์ ์ํด, ์ธ์ ์ ์์์ ์๋ธ๋ฆฟ์ด ์์๋๋ ์์ ๋ถํฐ (์ธ์ ์ ์์์ฑ ์ปจํ ์คํธ๋ฅผ ํฌํจ)
-
ํธ๋์ญ์ ๊ณผ JDBC์ ์์์ Service ๋ ์ด์ด๋ถํฐ
-
ํธ๋์ญ์ ๊ณผ JDBC์ ์ข ๋ฃ๋ Service ๋ ์ด์ด์์ ์ข ๋ฃ
-
์ธ์ (์์์ฑ ์ปจํ ์คํธ)์ ์ปจํธ๋กค๋ฌ ์์ญ๊น์ง ๋๊ณ ๊ฐ๊ธฐ ๋๋ฌธ์ ์์์ฑ์ด ๋ณด์ฅ๋์ด select๊ฐ ๊ฐ๋ฅํด์ง๊ณ , lazy Loading์ด ๊ฐ๋ฅํด์ง๋ค.
- ์ ํต์ ์ธ ๋ฐฉ์
์ ํต์ ์ธ ๋ฐฉ์์ ์๋์๊ฐ์ด ์ธ์ ์์์์ ์ ์์์ฑ ์ปจํ ์คํธ, JDBC, Transaction ์ฐ๊ฒฐ์ ํ๋ฒ์ ์คํํ๊ณ ํ๋ฒ์ ์ข ๋ฃํ๋ค.
- lazy Loading ๋ฐฉ์
lazy Loading ๋ฐฉ์์ ์์์ ์ค๋ช ํ๊ฒ์ฒ๋ผ ์ธ์ ์ด ์์๋ ๋ ์์์ฑ ์ปจํ ์คํธ๊ฐ ์คํ๋๊ณ , Service ์์ ๋ถํฐ JDBC์ Transaction์ ์ฐ๊ฒฐ์ด ์ฐ๊ฒฐ๋๊ณ , ๋๊ธด๋ค.
์๋์ ๊ทธ๋ฆผ์ ์ต์ด ์ ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์์๋์ธ๋ฐ, ์ ์ ์ ๋ณด๋ ๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์์ง๋ง, ์ ์ ์ ๋ณด์ ์ธ๋ํค์ธ ํ ์ ๋ณด๋ ํ๋ก์ ๊ฐ์ฒด๋ก ๊ฐ์ ธ์จ๋ค.
์ด๋ ๊ฒ ์ ์ ์ ๋ณด๋ง ํ์ํ ๊ฒฝ์ฐ์๋ Controller์์ ๊ทธ๋ฅ ์ฐ๋ฉด ๋๋ค.
๋ง์ฝ, ํ ์ ๋ณด๋ฅผ ์ธ์ผ์ด ์์ด ํ ์ ๋ณด์ ํ๋ก์ ๊ฐ์ฒด๋ฅผ ํธ์ถํ๊ฒ ๋๋ฉด, ๋ค์ JDBC ์ฐ๊ฒฐ์ ํตํด select๋ฌธ์ ์ค์ํด ํ๋ก์ ๊ฐ์ฒด๊ฐ ์๋ ์ง์ง ํ ์ ๋ณด ๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์์ค๋ค.
์ฌ์ฉ๋ฒ
Lazy Loading ๋ฐฉ์์ ์ฐ๊ธฐ ์ํด์๋ application.yml์ jpa ์ค์ ์์ open-in-view: true ์ค์ ์ ํด์ฃผ๋ฉด ๋๋ค. ( ๋งจ์์ ๋์์์. ๊ทธ๋ฆฌ๊ณ Default๋ true์ )
๋ง์ฝ, open-in-view: false๋ฅผ ์ฐ๊ฒ๋๋ค๋ฉด, Service ๊ณ์ธต์์ ์์์ฑ ์ปจํ ์คํธ์ JDBC, Transaction ์ฐ๊ฒฐ์ด ์ฐ๊ฒฐ๋๊ณ ๋๊ธฐ๊ฒ ๋๋ค.
์ฆ, Controller์์ ์์์ฑ ์ปจํ ์คํธ๋ก ์ ๊ทผ์ด ๋ถ๊ฐํ๋ค.
๐ ์ ํต์ ์ธ ๋ก๊ทธ์ธ ๋ฐฉ์
์ ํต์ ์ธ ๋ก๊ทธ์ธ ๊ตฌํ
userService์์ ๋ก๊ทธ์ธ ํจ์๋ฅผ ์คํ์ํค๊ณ , ( ๋ก๊ทธ์ธ ํจ์๋ @RequestBody๋ก ๋ฐ์ User์ ์์ด๋์ ํจ์ค์๋๊ฐ ์ผ์นํ๋์ง ํ์ธ )
์๋์ ์ฝ๋์ฒ๋ผ HttpSession ์ ํตํด ์ธ์ ์ ์์ฑํ๋ค.
@PostMapping("/api/user/login")
public ResponseDto<Integer> login(@RequestBody User user, HttpSession session){
User principal = userService.๋ก๊ทธ์ธ(user);
if(principal != null){
session.setAttribute("principle",principal);
}
return new ResponseDto<Integer>(HttpStatus.OK.value(),1);
}
thymeleaf์์ ์ธ์ ๊ฐ์ ํตํด ์ธ์ ๋ณ ํ์ํ ์ ๋ณด๋ฅผ ๊ตฌ๋ถํด์ค๋ค.
<!-- fragment bodyHeader ํ์ผ -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<div th:fragment="bodyHeader">
<nav class="navbar navbar-expand-md bg-dark navbar-dark">
<a class="navbar-brand" href="/blog">ํ</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#collapsibleNavbar">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="collapsibleNavbar">
<ul th:if="${session.principle == null}" class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/blog/user/loginForm">๋ก๊ทธ์ธ</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/blog/user/joinForm">ํ์๊ฐ์
</a>
</li>
</ul>
<ul th:if="${session.principle != null}" class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/blog/board/writeForm">๊ธ์ฐ๊ธฐ</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/blog/user/userForm">๋ด ์ ๋ณด</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/blog/user/logout">๋ก๊ทธ์์</a>
</li>
</ul>
</div>
</nav>
</div>
๐ ์คํ๋ง ์ํ๋ฆฌํฐ
์คํ๋ง ์ํ๋ฆฌํฐ๋ ์คํ๋ง ๊ธฐ๋ฐ์ ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ณด์(์ธ์ฆ๊ณผ ๊ถํ)์ ๋ด๋นํ๋ ํ๋ ์์ํฌ์ด๋ค.
๋ง์ฝ ์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ์ฌ์ฉํ์ง ์์๋ค๋ฉด, ์์ ์ ํต์ ์ธ ๋ฐฉ๋ฒ์ฒ๋ผ ์์ฒด์ ์ผ๋ก ์ธ์ ์ ์ฒดํฌํ๊ณ , redirect๋ฑ์ ํด์ผํ๋ค.
spring security๋ filter ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ spring MVC์ ๋ถ๋ฆฌ๋์ด ๊ด๋ฆฌ ๋ฐ ๋์ํ๋ค.
๋ณด์ ๊ด๋ จ ์ฉ์ด
- ์ ๊ทผ ์ฃผ์ฒด(Principal) : ๋ณดํธ๋ ๋์์ ์ ๊ทผํ๋ ์ ์
- ์ธ์ฆ (Authenticate) : ํ์ฌ ์ ์ ๊ฐ ๋๊ตฌ์ธ์ง ํ์ธ ex) ๋ก๊ทธ์ธ
- ์ ํ๋ฆฌ์ผ์ด์ ์ ์์ ์ ์ํํ ์ ์๋ ์ฃผ์ฒด์์ ์ฆ๋ช ํ๋ค.
- ์ธ๊ฐ (Authorize) : ํ์ฌ ์ ์ ๊ฐ ์ด๋ค ์๋น์ค, ํ์ด์ง์ ์ ๊ทผํ ์ ์๋ ๊ถํ์ด ์๋์ง ๊ฒ์ฌ
- ๊ถํ : ์ธ์ฆ๋ ์ฃผ์ฒด๊ฐ ์ ํ๋ฆฌ์ผ์ด์
์ ๋์์ ์ํํ ์ ์๋๋ก ํ๋ฝ๋์ด์๋์ง๋ฅผ ๊ฒฐ์
- ๊ถํ ์น์ธ์ด ํ์ํ ๋ถ๋ถ์ผ๋ก ์ ๊ทผํ๋ ค๋ฉด ์ธ์ฆ ๊ณผ์ ์ ํตํด ์ฃผ์ฒด๊ฐ ์ฆ๋ช ๋์ด์ผ๋ง ํ๋ค.
- ๊ถํ ๋ถ์ฌ์๋ ๋๊ฐ์ง ์์ญ์ด ์กด์ฌํ๋๋ฐ ์น ์์ฒญ ๊ถํ, ๋ฉ์๋ ํธ์ถ ๋ฐ ๋๋ฉ์ธ ์ธ์คํด์ค์ ๋ํ ์ ๊ทผ ๊ถํ ๋ถ์ฌ
ํ์๋ฆฌํ์์ ์๋ฐ ์ํ๋ฆฌํฐ ์ฌ์ฉํ๊ธฐ
// build.gradle
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
<!-- html ํ์ผ -->
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
๊ธฐ์กด์ ํ๊ทธ์์ xmlns:sec ๊ฐ ์ถ๊ฐ ๋ ํํ์ด๋ค.
์๋์ ๊ฐ์ด ์ฌ์ฉํ ์ ์๋ค.
<!--ROLE_USER ๊ถํ์ ๊ฐ๋๋ค๋ฉด ์ด ๊ธ์ด ๋ณด์-->
<h1 sec:authorize="hasRole('ADMIN')">Has admin Role</h1>
<!--ROLE_ADMIN ๊ถํ์ ๊ฐ๋๋ค๋ฉด ์ด ๊ธ์ด ๋ณด์-->
<h1 sec:authorize="hasRole('USER')">Has user Role</h1>
<!--์ด๋ค ๊ถํ์ด๊ฑด ์๊ด์์ด ์ธ์ฆ์ด ๋์๋ค๋ฉด ์ด ๊ธ์ด ๋ณด์-->
<div sec:authorize="isAuthenticated()">
Only Authenticated user can see this Text
</div>
<!--์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์ ๊ฒฝ์ฐ ์ด ๊ธ์ด ๋ณด์-->
<div sec:authorize="isAnonymous()">
Only Authenticated user can see this Text
</div>
<!--์ธ์ฆ์ ์ฌ์ฉ๋ ๊ฐ์ฒด์ ๋ํ ์ ๋ณด-->
<b>Authenticated DTO:</b>
<div sec:authentication="principal"></div>
<!--์ธ์ฆ์ ์ฌ์ฉ๋ ๊ฐ์ฒด์ Username (ID)-->
<b>Authenticated username:</b>
<div sec:authentication="name"></div>
<!--๊ฐ์ฒด์ ๊ถํ-->
<b>Authenticated user role:</b>
<div sec:authentication="principal.authorities"></div>
๋ก๊ทธ์ธ ํ์ด์ง ์ปค์คํฐ๋ง์ด์ง ํ๊ธฐ
blog ํจํค์ง์ config ํจํค์ง๋ฅผ ํ๋ ์์ฑํ๊ณ , SecurityConfig ํด๋์ค๋ฅผ ์์ฑํด์ค๋ค.
package com.cos.blog.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
// ๋น ๋ฑ๋ก: ์คํ๋ง ์ปจํ
์ด๋์์ ๊ฐ์ฒด๋ฅผ ๊ด๋ฆฌํ ์ ์๊ฒ ํ๋ ๊ฒ
@Configuration // ๋น ๋ฑ๋ก: IoC
@EnableWebSecurity // ํํฐ ์ถ๊ฐ: ์ํ๋ฆฌํฐ ํํฐ๋ฅผ ๊ฑฐ๋ ๊ฒ
@EnableGlobalAuthentication // ํน์ ์ฃผ์๋ก ์ ๊ทผ์ํ๋ฉด ๊ถํ ๋ฐ ์ธ์ฆ์ ๋ฏธ๋ฆฌ ์ฒดํฌํ๋ ๊ฒ
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.authorizeRequests()
.antMatchers("/auth/**")// /auth/ ์ดํ์ ๋ชจ๋ ๊ฒฝ๋ก๋
.permitAll() // ๋๊ตฌ๋ ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค
.anyRequest() // ๊ทธ๊ฒ ์๋๊ณ ๋
.authenticated() // ํ๋ฝ๋ ์ฌ๋๋ง ์ ๊ทผ ๊ฐ๋ฅํ๋ค.
.and()
.formLogin()
.loginPage("/auth/loginForm");
}
}
์์ ๊ฐ์ด WebSecurityConfigurerAdapter๋ฅผ ์์ํ๊ณ , configure๋ฅผ Override ํ๋ฉด, login ํ์ด์ง ์ปค์คํฐ๋ง์ด์ง์ด ๊ฐ๋ฅํด์ง๋ค.
๋น๋ฐ๋ฒํธ ํด์๊ฐ์ผ๋ก ๋ณ๊ฒฝํ๊ธฐ
๊ฐ์ฌํ๊ฒ๋, ๋ฌธ์์ด์ ๋ฃ์ผ๋ฉด ํด์๊ฐ์ผ๋ก ๋ณ๊ฒฝํด์ฃผ๋ ํด๋์ค๋ฅผ Spring Security์์ ์ง์ํด์ค๋ค.
์ฐ๋ฆฌ๋ BCryptPasswordEncoder๋ฅผ ์ธ ๊ฒ์ด๋ค.
package com.cos.blog.config;
@Bean // ์คํ๋ง์ด ๊ด๋ฆฌํ๋ IoC๊ฐ ๋๋ค.
BCryptPasswordEncoder encodePWD(){ return new BCryptPasswordEncoder(); }
์์๊ฐ์ด SecurityConfig ํด๋์ค์ ํด๋น ๋ฉ์๋๋ฅผ ์ถ๊ฐํด์ฃผ๋ฉด ๋๋ค. @Bean ์ด๋ ธํ ์ด์ ์ ํตํด ์คํ๋ง IoC์์ ๊ด๋ฆฌํ๋๋ก ํ๋ค.
BCryptPasswordEncoder์ encode( ) ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด String Wrapper Class ๋ก ํด์๋ ๊ฐ์ ๋ฐํํด์ค๋ค.
๊ธฐ์กด์ ํ์๊ฐ์ ์ ํ ๋ password ๊ทธ๋๋ก DB์ ์ ์ฅํ์ง๋ง, ์ด์ ๋ ํด์๋ก ๋ณํ๋ ๊ฐ์ ๋ฃ์ด์ค๋ค.
๋ฐ๋ ํ์๊ฐ์ Service ๊ฐ์ฒด๋ฅผ ๋ด๋ณด์.
package com.cos.blog.service;
import com.cos.blog.model.RoleType;
import com.cos.blog.model.User;
import com.cos.blog.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository; // ์์กด์ฑ ์ฃผ์
private final BCryptPasswordEncoder encoder; // BCryptPasswordEncoder ํด๋์ค ์์กด์ฑ ์ฃผ์
@Transactional
public void ํ์๊ฐ์
(User user){
String rawPassword = user.getPassword(); // ์๋ password
String encPassword = encoder.encode(rawPassword); // ํด์
user.setPassword(encPassword);
user.setRole(RoleType.USER);
userRepository.save(user);
}
}
์ ์ฝ๋์ ๊ฐ์ด ์๋ User ๊ฐ์ฒด๋ฅผ ๋ฐ์์ rawPassword๋ฅผ ํด์๊ฐ์ผ๋ก ๋ณ๊ฒฝํด์ฃผ๊ณ , ๋ณ๊ฒฝ๋ ํด์๊ฐ์ User ๊ฐ์ฒด์ password๋ก ์ค์ ํด์ค๋ค.
๊ทธ ๋ค์ userRepository.save() ๋ฉ์๋๋ฅผ ํตํด DB์ ํ์์ ๋ณด๋ฅผ ์ ์ฅํ๋ค.
XSS์ CSRF ๊ณต๊ฒฉ
XSS๋ (Cross Site Scripting) ์ ์ฝ์๋ก ์ฃผ๋ก ๋ค๋ฅธ ์น์ฌ์ดํธ์ ์ ๋ณด๋ฅผ ๊ตํํ๋ ์์ผ๋ก ์๋ํ๋ฏ๋ก ์ฌ์ดํธ ๊ฐ ์คํธ๋ฆฝํ ์ด๋ผ๊ณ ํ๋ค.
ํฌ๋ก์ค ์ฌ์ดํธ ์คํฌ๋ฆฝํ ์ ์๋ฐ์คํฌ๋ฆฝํธ๋ฅผ ์ฌ์ฉํ์ฌ ๊ณต๊ฒฉํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค. ๊ฒ์ํ ๊ฐ์ ๊ณต๊ฐ์ <script> </script> ํ๊ทธ๋ฅผ ์ ์ฅํ์๋, ์ด ์คํฌ๋ฆฝํธ๊ฐ ์๋ฒ์ ์ ์ฅ๋ผ ์คํ์ด ๋์ด
์๋ฒ์ ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ๋นผ์ฌ์๊ฐ ์๊ฒ๋๋ค.
CSRF๋ (Cross Site Request Fogery)์ ์ฝ์๋ก ์ฌ์ดํธ๊ฐ ์์ฒญ์ ์์กฐํ๋ ๊ณต๊ฒฉ์ด๋ค.
์ ๋ํ ์ฌ์ฉ์๊ฐ ์์ ์ ์์ง์๋ ๋ฌด๊ดํ๊ฒ ๊ณต๊ฒฉ์๊ฐ ์๋ํ ํ์๋ฅผ ์น์ฌ์ดํธ์ ์์ฒญํ๊ฒํ๋ ๊ณต๊ฒฉ์ ๋งํ๋ค.
ex) ์๋ฅผ๋ค์ด, ์ด์์๊ฐ http://www.example.com/point?100&username?ghdcksgml ํด๋น GET์์ฒญ์ ๋ณด๋ด๋ฉด
ghdcksgml๋ผ๋ ์ ์ ์๊ฒ 100ํฌ์ธํธ๊ฐ ์ง๊ธ๋๋ค๊ณ ํด๋ณด์.
์๋ ์ด์์๋ง์ด ๊ถํ์ด ์๊ธฐ๋๋ฌธ์, ์ผ๋ฐ์ฌ์ฉ์๊ฐ ํด๋น GET์์ฒญ์ ๋ณด๋ด๋ ์๋ฌด๋ฐ ์๋ต์ด ์๊ณ , ์ค์ง ์ด์์๋ง์ด ํด๋น GET์์ฒญ์ ํ์๋ ๋์ํ๋ค.
์ด ์ ์ ์ด์ฉํด ์๋์ ๊ฐ์ด ๋์๋ฅผ ์งํํ๋ค.
์ด์์๊ฐ ๋์ฌ์ ํด๋น ๋งํฌ๋ฅผ ํด๋ฆญํ๊ฒ ๋๋ค๋ฉด ghdcksgml๋ผ๋ ์ ์ ์๊ฒ 100ํฌ์ธํธ๊ฐ ๋ค์ด๊ฐ๊ฒ ๋๋ ๊ฒ์ด๋ค.
์ด๊ฒ์ด ๋ฐ๋ก CSRF ๊ณต๊ฒฉ์ด๋ค.
์ถ์ฒ: https://lucete1230-cyberpolice.tistory.com/23
์คํ๋ง ์ํ๋ฆฌํฐ ๋ก๊ทธ์ธ
์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ๋ก๊ทธ์ธ ์์ฒญ์ ๊ฐ๋ก์ฑ๊ฒ ๋ง๋ ๋ค.
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.csrf().disable() // csrf ํ ํฐ ๋นํ์ฑํ (ํ
์คํธ์ ๊ฑธ์ด๋๋ ๊ฒ ์ข์)
.authorizeRequests()
.antMatchers("/","/auth/**","/js/**","/css/**","/image/**")// /auth/ ์ดํ์ ๋ชจ๋ ๊ฒฝ๋ก๋
.permitAll() // ๋๊ตฌ๋ ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค
.anyRequest() // ๊ทธ๊ฒ ์๋๊ณ ๋
.authenticated() // ํ๋ฝ๋ ์ฌ๋๋ง ์ ๊ทผ ๊ฐ๋ฅํ๋ค.
.and()
.formLogin()
.loginPage("/auth/loginForm")
.loginProcessingUrl("/auth/loginProc") // ์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ํด๋น ์ฃผ์๋ก ์์ฒญ์ค๋ ๋ก๊ทธ์ธ์ ๊ฐ๋ก์ฑ์ ๋์ ๋ก๊ทธ์ธ ํด์ค๋ค.
.defaultSuccessUrl("/");
}
์ ์ฝ๋์ฒ๋ผ "/auth/loginFrom" ์ด๋ผ๋ ์์ฒญ์ด ๋ค์ด์ค๋ฉด, ์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ํด๋น ์์ฒญ์ ๊ฐ๋ก์ฑ ๋์ ๋ก๊ทธ์ธ ํด์ค๋ค.
antMatchers์ ์ค์ ๋ ํ์ด์ง๋ฅผ ์ ์ธํ ๋ชจ๋ ํ์ด์ง์ ์์ฒญ์ loginPage๋ก ๋์ด์จ๋ค.
defaultSuccessUrl์ ์ ์์ ์ผ๋ก ์๋ฃ๋๋ฉด, ํด๋น URL๋ก ์ด๋ํ๋ค.
๊ฐ๋ก์ฑ์ ๋ก๊ทธ์ธ์ ํ ๋ ๊ทธ๋ ๋ด๊ฐ ๋ง๋ค์ด์ผ๋ ํด๋์ค๊ฐ ํ๋ ์๋ค.
๋ฐ๋ก, UserDetails๋ฅผ ๊ฐ์ง๊ณ ์๋ User Object๋ฅผ ๋ง๋ค์ด์ผํ๋ค. (๋ก๊ทธ์ธ ์์ฒญ์ ํ๊ณ ์ธ์ ์ ๋ฑ๋ก์ ํด์ค์ผํ๋๋ฐ ๊ทธ๋ฅ User ์ค๋ธ์ ํธ๋ฅผ ๋ฆฌํดํ๋ฉด ํ์ ์ด ๋ง์ง ์๊ธฐ๋๋ฌธ์)
package com.cos.blog.config.auth;
import com.cos.blog.model.User;
import lombok.AllArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
// ์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ๋ก๊ทธ์ธ ์์ฒญ์ ๊ฐ๋ก์ฑ์ ๋ก๊ทธ์ธ์ ์งํํ๊ณ ์๋ฃ๊ฐ ๋๋ฉด, UserDetails ํ์
์ ์ค๋ธ์ ํธ๋ฅผ
// ์คํ๋ง ์ํ๋ฆฌํฐ์ ๊ณ ์ ํ ์ธ์
์ ์ฅ์์ ์ ์ฅ์ ํด์ค๋ค.
@AllArgsConstructor
public class PrincipalDetail implements UserDetails {
private User user; // composition
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
// ๊ณ์ ์ด ๋ง๋ฃ๋์ง ์์๋์ง ๋ฆฌํดํ๋ค. (true:๋ง๋ฃ์๋จ)
@Override
public boolean isAccountNonExpired() {
return true;
}
// ๊ณ์ ์ด ์ ๊ฒจ์๋์ง ์์๋์ง ๋ฆฌํดํ๋ค. (true:์ ๊ธฐ์ง ์์)
@Override
public boolean isAccountNonLocked() {
return true;
}
// ๋น๋ฐ๋ฒํธ๊ฐ ๋ง๋ฃ๋์ง ์์๋์ง ๋ฆฌํดํ๋ค. (true:๋ง๋ฃ์๋จ)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// ๊ณ์ ์ด ํ์ฑํ(์ฌ์ฉ๊ฐ๋ฅ)์ธ์ง ๋ฆฌํดํ๋ค. (true:ํ์ฑํ)
@Override
public boolean isEnabled() {
return true;
}
// ๊ณ์ ์ด ๊ฐ๊ณ ์๋ ๊ถํ ๋ชฉ๋ก์ ๋ฆฌํดํ๋ค. (๊ถํ์ด ์ฌ๋ฌ๊ฐ ์์ ์ ์์ด์ ๋ฃจํ๋ฅผ ๋์์ผ ํ๋๋ฐ ์ฐ๋ฆฌ๋ 1๊ฐ๋ฐ์ ์์.)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collectors = new ArrayList<>();
collectors.add(()->{ return "ROLE_"+user.getRole();}); // ์์ ROLE_์ ๋ถ์ด๋๊ฑด ์๋ฐ ์ํ๋ฆฌํฐ ๊ท์น์.
return collectors;
}
}
package com.cos.blog.config.auth;
import com.cos.blog.model.User;
import com.cos.blog.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service // Bean ๋ฑ๋ก
@RequiredArgsConstructor
public class PrincipalDetailService implements UserDetailsService {
private final UserRepository userRepository;
// ์คํ๋ง์ด ๋ก๊ทธ์ธ ์์ฒญ์ ๊ฐ๋ก์ฑ๋, username,password ๋ณ์ 2๊ฐ๋ฅผ ๊ฐ๋ก์ฑ๋๋ฐ
// password ๋ถ๋ถ ์ฒ๋ฆฌ๋ ์์์ ํจ.
// username์ด DB์ ์๋์ง๋ง ํ์ธํด์ฃผ๋ฉด ๋จ.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User principal = userRepository.findByUsername(username) // username์ด ์ผ์นํ๋ ์ฌ์ฉ์ ์ฐพ๊ธฐ
.orElseThrow(()->{
return new UsernameNotFoundException("ํด๋น ์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
});
return new PrincipalDetail(principal); // ์ํ๋ฆฌํฐ์ ์ธ์
์ ์ ์ ์ ๋ณด๊ฐ ์ ์ฅ์ด ๋จ.
}
}
package com.cos.blog.config;
import com.cos.blog.config.auth.PrincipalDetailService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
// ๋น ๋ฑ๋ก: ์คํ๋ง ์ปจํ
์ด๋์์ ๊ฐ์ฒด๋ฅผ ๊ด๋ฆฌํ ์ ์๊ฒ ํ๋ ๊ฒ
@Configuration // ๋น ๋ฑ๋ก: IoC
@EnableWebSecurity // ํํฐ ์ถ๊ฐ: ์ํ๋ฆฌํฐ ํํฐ๋ฅผ ๊ฑฐ๋ ๊ฒ
@EnableGlobalAuthentication // ํน์ ์ฃผ์๋ก ์ ๊ทผ์ํ๋ฉด ๊ถํ ๋ฐ ์ธ์ฆ์ ๋ฏธ๋ฆฌ ์ฒดํฌํ๋ ๊ฒ
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final PrincipalDetailService principalDetailService; // DI
@Bean // ์คํ๋ง์ด ๊ด๋ฆฌํ๋ IoC๊ฐ ๋๋ค.
public BCryptPasswordEncoder encodePWD(){
return new BCryptPasswordEncoder();
}
// ์ํ๋ฆฌํฐ๊ฐ ๋์ ๋ก๊ทธ์ธํด์ฃผ๋๋ฐ password๋ฅผ ๊ฐ๋ก์ฑ๊ธฐ๋ฅผ ํ๋๋ฐ
// ํด๋น password๊ฐ ๋ญ๋ก ํด์ฌ๊ฐ ๋์ด ํ์๊ฐ์
์ด ๋์๋์ง ์์์ผ
// ๊ฐ์ ํด์ฌ๋ก ์ํธํํด์ DB์ ์๋ ํด์ฌ๋ ๋น๊ตํ ์ ์์.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD());
// principalDetailService๋ฅผ ํตํด์ ๋ก๊ทธ์ธ์ํ ๋ password๋ฅผ encodePWD๋ก ์ธ์ฝ๋ํด์ ๋น๊ต๋ฅผ ์์์ ํด์ค๋ค.
}
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.csrf().disable() // csrf ํ ํฐ ๋นํ์ฑํ (ํ
์คํธ์ ๊ฑธ์ด๋๋ ๊ฒ ์ข์)
.authorizeRequests()
.antMatchers("/","/auth/**","/js/**","/css/**","/image/**")// /auth/ ์ดํ์ ๋ชจ๋ ๊ฒฝ๋ก๋
.permitAll() // ๋๊ตฌ๋ ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค
.anyRequest() // ๊ทธ๊ฒ ์๋๊ณ ๋
.authenticated() // ํ๋ฝ๋ ์ฌ๋๋ง ์ ๊ทผ ๊ฐ๋ฅํ๋ค.
.and()
.formLogin()
.loginPage("/auth/loginForm")
.loginProcessingUrl("/auth/loginProc") // ์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ํด๋น ์ฃผ์๋ก ์์ฒญ์ค๋ ๋ก๊ทธ์ธ์ ๊ฐ๋ก์ฑ์ ๋์ ๋ก๊ทธ์ธ ํด์ค๋ค.
.defaultSuccessUrl("/");
}
}
ํด๋น ์ฝ๋๋ฅผ ์ถ๊ฐํด์ค๋ค.
๋ก๊ทธ์ธ์์ฒญ์ด ์ค๋ ์๊ฐ, loginProcessingUrl์ด ๊ฐ๋ก์ฑ๋ค. => username๊ณผ password ์ ๋ณด๋ฅผ PrincipalDetailService์ ์๋ loadUserByUsername์ผ๋ก ๋ณด๋ธ๋ค.
=> username์ ๋น๊ตํด์ PrincipalDetail์ ๋ฆฌํดํด์ค๋ค. => ๋ฆฌํดํ ๋ ๋น๋ฐ๋ฒํธ ์ฒดํฌ๋ฅผ ํ๋ค. SecurityConfig์ configure์ ํตํด์ principalDetailService๊ฐ ๋ก๊ทธ์ธ ์์ฒญ์ ํ๊ณ
=> auth.userDetailsService(principalDetailService) ์ด ๋ฆฌํด์ด ๋๋ฉด, passwordEncoder๋ฅผ ํตํด encodePWD๋ก ๋ค์ ์ํธํ๋ฅผ ํ๊ณ , ๋ฐ์ดํฐ ๋ฒ ์ด์ค์ ๋น๊ตํ๋ค.
=> ๋น๊ต๊ฐ ๋๋๋ฉด Spring Security ์์ญ์ PrincipalDetail๋ก ๊ฐ์ธ์ ธ์ ์ ์ฅ์ด ๋๋ค.
๐ ๊ธ์ฐ๊ธฐ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ
๊ธ์ฐ๊ธฐ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ธฐ ์ํด์ ๊ฐ์ฅ๋จผ์ ๊ธ์ฐ๊ธฐ ํ์ด์ง๋ฅผ ์์ฑํด์คฌ๋ค. (/resources/templates/board/saveForm.html)
๊ทธ ๋ค์ ํ์์ ๊ธ์ฐ๊ธฐ ๋ฒํผ์ ๋๋ ์๋, ๊ธ์ฐ๊ธฐ ํ์ด์ง๋ก ๋งคํ๋๋๋ก BoardController์ (/board/saveForm) GET ์์ฒญ์ ๋ฐ์ ์ ์๋๋ก GetMapping์ ์ถ๊ฐํ๋ค.
๊ธ์ ์ธ ์ ์๋ ํ์ด์ง์ธ saveForm.html์๋ ์ ๋ชฉ๊ณผ ๋ด์ฉ์ ์์ฑํ ์ ์๋๋กํ๊ณ , ๊ธ์ฐ๊ธฐ ์๋ฃ๋ฒํผ์ ajaxํต์ ์ ์ํด formํ๊ทธ ๋ฐ์ผ๋ก ๊บผ๋๋ค.
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="/layout/fragments/header::header"/>
<body>
<div class="container">
<div th:replace="/layout/fragments/bodyHeader::bodyHeader"/>
<form>
<div class="form-group">
<label for="title">Username</label>
<input type="text" class="form-control" placeholder="Enter title" id="title">
</div>
<div class="form-group">
<label for="content">Content:</label>
<textarea class="form-control summernote" rows="5" id="content"></textarea>
</div>
</form>
<button id="save" class="btn btn-primary">๊ธ์ฐ๊ธฐ ์๋ฃ</button>
<script>
$('.summernote').summernote({
placeholder: 'Hello Bootstrap 4',
tabsize: 2,
height: 300
});
</script>
<div th:replace="/layout/fragments/footer::footer"/>
</div>
</body>
<script src="/js/board.js"></script>
</html>
๋ด์ฉ ๋ถ๋ถ์ ํ ์คํธ ์์๋ฅผ ๋ง๋ค ์ ์์ง๋ง, ๋๋ฌด ํ์ ํด์ summer note๋ฅผ ์ฌ์ฉํ๋ค. summer note
ํด๋น ์ฝ๋์์ ์์์๋ถํฐ scriptํ๊ทธ 2๊ฐ๊น์ง๋ ๊ฐ๊ฐ jQuery, boostrap4์ ๊ด๋ จ๋ ๋ด์ฉ์ด๋ฏ๋ก ์ด๋ฏธ ์ถ๊ฐํ๋ค๋ฉด ์ง์์ฃผ์.
์ด์ ajax ํต์ ์ ํด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์์ฑํ ๊ธ์ ๋ฃ์ด์ค์ผํ๋ฏ๋ก, (resources/static/js/board.js) ํ์ผ์ ์์ฑํ๋ค.
user.js์ ๋น์ทํ๋ฏ๋ก ๋ฐ์ดํฐ ๋ง์ถฐ์ฃผ๊ณ ํต์ ~
let index = {
init:function(){
// btn-save ๋ฒํผ์ด ํด๋ฆญ๋๋ฉด, saveํจ์๋ฅผ ํธ์ถ
document.querySelector("#save").addEventListener('click',()=>{
this.save();
});
},
save:function(){
let data = {
title: document.querySelector("#title").value,
content: document.querySelector("#content").value
}
// ajax ์์ฒญ
fetch("/api/board",{
method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(data)
})
.then(response => response.json())
.then(data=>{
alert("๊ธ์ฐ๊ธฐ ์๋ฃ");
console.log(data);
location.href="/";
})
.catch(error=>{alert(error.message)});
}
};
index.init();
์ด๋ ๊ฒ ๋๋ฉด, ๊ธ์ ์์ฑํ๊ณ ๊ธ์ฐ๊ธฐ ์๋ฃ ๋ฒํผ์ ๋๋ฅด๊ฒ ๋๋ฉด, /api/board ๋ก post์์ฒญ์ด ๋ค์ด๊ฐ๊ฒ ๋๋ค.
์ฐ๋ฆฌ๋ ์์ง board์ ๋ํ api ์ฒ๋ฆฌ๋ฅผ ํ์ง ์์๊ธฐ ๋๋ฌธ์
BoardApiController(ํด๋์ค), BoardService(ํด๋์ค), BoardRepository(์ธํฐํ์ด์ค) ๋ฅผ ๊ฐ๊ฐ ์์ฑํด์ค๋ค.
BoardApiController๋ฅผ ํตํด BoardService์ Board,User ๊ฐ์ฒด๋ฅผ ๋๊ธฐ๋ฉด
BoardService๋ Board์ User๊ฐ์ฒด๋ฅผ ๋ฃ์ด DB์ ์ ์ฅํด์ค๋ค. ( Board์ ๋๊ฐ์ด๊ฑด์ง ์ ์ฅํ๊ธฐ ์ํ Userํ ์ด๋ธ์ด ์๊ธฐ ๋๋ฌธ์ )
// BoardApiController.class
package com.cos.blog.controller.api;
import com.cos.blog.config.auth.PrincipalDetail;
import com.cos.blog.controller.dto.ResponseDto;
import com.cos.blog.model.Board;
import com.cos.blog.service.BoardService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class BoardApiController {
private final BoardService boardService;
@PostMapping("/api/board")
public ResponseDto<Integer> save(@RequestBody Board board, @AuthenticationPrincipal PrincipalDetail principal){
boardService.save(board,principal.getUser());
return new ResponseDto<>(HttpStatus.OK.value(), 1);
}
}
// BoardService.class
package com.cos.blog.service;
import com.cos.blog.model.Board;
import com.cos.blog.model.User;
import com.cos.blog.repository.BoardRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
@Transactional
public void save(Board board, User user){
board.setCount(0L);
board.setUser(user);
boardRepository.save(board);
}
}
// BoardRepository.class
package com.cos.blog.repository;
import com.cos.blog.model.Board;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BoardRepository extends JpaRepository<Board,Long> {
}
์ด๋ ๊ฒ ๊ตฌํํด์ ๊ธ์ฐ๊ธฐ ์๋ฃ๋ฅผ ๋๋ฌ๋ณด๋ฉด ์๋์ฒ๋ผ DB์ ์ ๋ค์ด๊ฐ ๋ชจ์ต์ ๋ณผ ์ ์๋ค.
๐ ๊ธ ๋ชฉ๋ก ๋ถ๋ฌ์ค๊ธฐ
์ง๊ธ ๊ตฌํํ๊ณ ์๋ ๊ฒ์ํ์ ๋ชจ๋ ์ ์ ์ ๊ธ ๋ชฉ๋ก๋ค์ ๋ณผ ์ ์๊ธฐ ๋๋ฌธ์ ๊ธ ๋ชฉ๋ก์ ๋ถ๋ฌ์ค๋๊ฒ์ ์์ฃผ ๊ฐ๋จํฉ๋๋ค.
๊ทธ๋ฅ BoardRepository์ findAll() ๋ฉ์๋๋ฅผ ์ ๋ ฅํ๋ฉด Listํํ๋ก ์ ์ฒด ๊ธ ๋ชฉ๋ก์ ์กฐํํ ์ ์์ต๋๋ค.
BoardService๋ contentList๋ฉ์๋๋ฅผ ์์ฑํด์ฃผ๊ณ ๊ธ ๋ชฉ๋ก์ ๊ฐ์ ธ์จ ๋ค List๋ก ๋ฐํํด์ฃผ๋ฉด ๋ฉ๋๋ค.
package com.cos.blog.service;
import com.cos.blog.model.Board;
import com.cos.blog.model.User;
import com.cos.blog.repository.BoardRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
@Transactional
public void save(Board board, User user){
board.setCount(0L);
board.setUser(user);
boardRepository.save(board);
}
// ์๋ซ ๋ถ๋ถ์
๋๋น.
@Transactional(readOnly = true)
public List<Board> contentList(){
return boardRepository.findAll();
}
}
์ด๋ ๊ฒ Listํํ๋ก ๋ฐํํ๊ฒ๋๋ฉด, BoardController์์๋ index.html(๊ฒ์ํ)์ผ๋ก ํด๋น ๋ฆฌ์คํธ๋ฅผ ๋๊ฒจ์ฃผ๋ฉด ๋๋๋ฐ
์คํ๋ง์์๋ Model ๊ฐ์ฒด๋ฅผ ์ด์ฉํฉ๋๋ค.
package com.cos.blog.controller;
import com.cos.blog.config.auth.PrincipalDetail;
import com.cos.blog.service.BoardService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
@RequiredArgsConstructor
public class BoardController {
private final BoardService boardService;
// ์๋ถ๋ถ ์
๋๋น.
@GetMapping({"","/"})
public String index(Model model){
model.addAttribute("boards",boardService.contentList());
return "index"; // viewResolver ์๋
}
@GetMapping("/board/saveForm")
public String saveForm(){
return "board/saveForm";
}
}
model.addAttribute("key","value") ์ด๋ค.
index.html์์ model์ ์ถ๊ฐํ ๊ฒ์ ์ฌ์ฉํ ์ ์๋ค.
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="/layout/fragments/header::header"/>
<body>
<div class="container">
<div th:replace="/layout/fragments/bodyHeader::bodyHeader"/>
<div th:each="board : ${boards}">
<div class="card m-2">
<div class="card-body">
<h4 class="card-title" th:text="${board.getTitle()}">์ ๋ชฉ ์ ๋ ๋ถ๋ถ</h4>
<a href="#" class="btn btn-primary">์์ธ ๋ณด๊ธฐ</a>
</div>
</div>
</div>
<div th:replace="/layout/fragments/footer::footer"/>
</div>
</body>
</html>
th:each ๋ foreach์ ๊ฐ์ ์ญํ ์ ํ๋ค. boards๊ฐ List๋ก ๋์ด์๊ธฐ๋๋ฌธ์, boards๋ฅผ ํ๊ฐ์ฉ ๊ฐ์ ธ์ ์ ๋ชฉ์ ๋ฃ์ด์ค๋ค.