Boopack with Spring Security part 1/5
สร้างโปรเจกต์ชื่อ Boopack ร้านค้าออนไลน์ขายหนังสือ ให้ backend ใช้ Spring ส่วน frontend ใช้ Angular
โค้ดทั้งหมดบน GitHub
Project Description
โปรเจกต์นี้จะประยุกต์ใช้ความรู้ที่มี backend เลือก Spring Boot 2 ส่วน frontend เลือก Angular 8 อยากสร้างร้านขายหนังสือออนไลน์เล็กๆที่ประกอบไปด้วย
- หน้าเว็บขายหนังสือ (ตอนนี้เอาแค่หนังสือก่อน)
- หน้าเว็บของ Admin เพื่อดูสถิติการซื้อขายหนังสือของลูกค้า (มี Admin ได้หลายคน)
- ลูกค้าสมัครสมาชิกด้วย email เป็นสำคัญ และต้องยืนยัน email
- เรื่อง payment ตัดไปก่อน หลังจากลูกค้ายืนยัน email ถูกต้อง ลูกค้าจะได้รับเงินจาก wallet ของร้านค้าจำนวนหนึ่งไว้ใช้สอย
เท่านี้ก่อน มากไปอาจทำไม่จบใน 5 parts
Tools
- IntelliJ community version: https://www.jetbrains.com/idea/download/
- Visual Studio Code: https://code.visualstudio.com/download
- Install Angular: https://angular.io/guide/setup-local
- MacOS (Windows OS ก็ทำได้)
*** Init Backend ***
Google ค้นไปว่า spring init
- Project: Gradle Project
- Language: Java
- Spring Boot: 2.1.6
- Group: com.pros
- Artifact: boopack
- Name (Option): boopack
- Description (Option): -
- Package Name (Option): com.pros.boopack
- Packaging: Jar
- Java: 8 (1.8)
- Dependencies: Spring Web Starter, Spring Data JPA, Spring Security
โหลดแล้วเปิดด้วย IntelliJ แล้วรอ Gradle ทำงาน
รันโปรแกรม
Description:Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.Reason: Failed to determine a suitable driver class
คือผมยังไม่มีฐานข้อมูล
ฐานข้อมูลให้เลือกเป็นประเภท SQL ตามใจชอบครับ ส่วนผมเลือก Microsoft SQL Server 2017 ติดตั้งโดย Docker
docker images
microsoft/mssql-server-linux latest 7 months ago 1.35GB
สำหรับเพื่อนที่โหลด image แล้ว เรียก Microsoft SQL Server 2017 ทำงาน
docker run --name mssqlserver2017 -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Password1234' -p 1433:1433 -d microsoft/mssql-server-linux
ดูความพร้อมของฐานข้อมูล
docker ps -a
หา Microsoft SQL Server Driver แล้วบอกมันกับ build.gradle
runtime 'com.microsoft.sqlserver:mssql-jdbc'
เขียน configuration บอกกับ application.properties
ดูเพิ่มเติมเกี่ยวกับ Hibernate Dialect
สร้างฐานข้อมูล
CREATE DATABASE boopackCOLLATE Thai_CI_AS;
รันโปรแกรม
สังเกตตรงนี้คือผลของ Spring Security เมื่อผมได้บอกกับ Gradle ว่าต้องการใช้ spring boot starter security มันจะสร้างรหัสผ่านอย่างนี้ให้ (รันแต่ละครั้งก็ต่างกันไป)
Using generated security password: 0c10d317-7e1e-4f70-9248-0e7dd14af3cc
ด้วยผลของ spring boot starter web โปรเจกต์นี้จะมี Tomcat Server แถมมาให้เลย ทำงานกับ port หมายเลข 8080
เปิด Browser ทดสอบ
http://localhost:8080
จะได้หน้าแรก (login) มาให้กรอก username กับ password ครับ
username: user
password: ตามที่มันสร้างเลย
เอกสารอ้างอิง
*** Init Frontend ***
เรื่องของ backend กับ database เรียบร้อย ทีนี้มาถึง Angular 8
ตรวจสอบว่าพร้อม
ng --version
นิดหน่อยก่อนจะเริ่มต้น backend ผมตั้งชื่อโปรเจกต์ว่า boopack ส่วน frontend ก็จะชื่อ boopack ไม่ต่างกัน ดังนั้นผมจะแยก folder ระหว่าง backend กับ frontend แทน
ng new boopack
เข้าในในโปรเจกต์ boopack (Angular project) นี้แล้วรัน
npm start
มันจะทำงานบน port 4200 เปิด Browser ทดสอบ
http://localhost:4200
เพื่อนจะเห็นว่าทั้ง backend และ frontend ใช้ port ต่างกัน (โดย default) กล่าวคือ
- backend ใช้ 8080
- frontend ใช้ 4200
ผมต้องการให้ frontend คุยกับ backend ได้ โดยให้ backend ทำตัวเป็น endpoint ในขั้นต้นนี้เมื่อผมบอกกับ Angular ว่า
http://localhost:4200/login
จะต้องหมายถึง /login ที่ฝั่ง backend
Angular สร้างหน้า Login
UI เปล่าๆแบบ HTML เลยยังไม่สวยครับ ขอใช้ Materialize เป็น Theme นะครับ
ติดตั้งในโปเจกต์ boopack frontend
npm install materialize-css@next
กำหนดค่าใน angular.json ให้อ่าน CSS และ JS ของ Materialize ในส่วนของ
projects/architect/build/options/stylesprojects/architect/build/options/scripts
สร้าง Login Component
ng generate component login
สร้าง App Routing (ถ้ายังไม่มีไฟล์นี้)
ng generate module app-routing --flat --module=app
อ่านเพิ่มเติม
จะได้ไฟล์ชื่อ app-routing.module.ts เพิ่มรายละเอียดลงไป
แก้ไข app.component.html ให้เหลือเพียงเท่านี้
<router-outlet></router-outlet>
ทดสอบเรียก
ปั้นหน้า Login
เพื่อนคนไหนอยากเล่นกับ icons (Material Icons)
อ่านเพิ่มเติม
ตามคำแนะนำให้ clone มาก่อน
git clone https://github.com/google/material-design-icons.git
จากนั้นเข้าไปที่ folder ชื่อ iconfont แล้ว copy ทั้งหมดไปใส่ไว้ใน src/assets/fonts/material-icons ของโปรเจกต์ (ไม่มีก็สร้างเอง)
ตรงไปบอกกับ angular.json เพิ่ม material-icons.css ในส่วนของ styles
"src/assets/fonts/material-icons/material-icons.css"
ผมเพิ่ม icons เข้าไปที่ login.component.html (login template)
<i class="material-icons prefix">email</i>
ทวนกันเล็กน้อยก่อนจะไปต่อ ตอนนี้ frontend กับ backend ต่างคนต่างคุยกันคนละ port ผมจะยึด frontend เป็นหลักให้พยายามคุยกับ backend และด้วยความสามารถของ Angular มันมีเรื่อง proxy มาให้เล่น
อ่านเพิ่มเติม
สร้างไฟล์ชื่อ proxy.conf.json ภายใต้โปรเจกต์ (ไว้ใน src แล้วหาไม่เจอ)
สิ่งที่ทำคือเมื่อไรก็ตามที่ฝั่ง frontend ร้องขอ URI ที่ขึ้นต้นด้วย /api มันจะเปลี่ยน /api นี้เป็นสตริงว่าง (ตัดออก) สมมติ /api/login ก็จะกลายเป็น /login จึงได้ว่าเมื่อไรเรียก localhost:4200/login เท่ากับว่า localhost:8080/login นั่นเอง
หน้าบ้านคุยกับหลังบ้านได้แล้วนะ
หน้า Login นี้สิ่งที่ frontend จะส่งไปยัง backend คือ email กับ password เพื่อทำ Basic Authentication ระบุตัวตนว่ามีอยู่จริง สิ่งที่ได้กลับมาก็ควรจะเป็น user นั้นๆ
พื้นฐานการจัดการ Forms ของ Angular แนะนำให้อ่านเรื่อง FormsModule และ ReactiveFormsModule อ่านเพิ่มเติม
พื้นฐาน Angular ใช้ HTTP เพื่อร้องขอไปยัง backend อ่านเพิ่มเติม
ปรับปรุง login.component.html
ปรับปรุง login.component.ts
ปรับปรุง app.module.ts เพิ่มการจัดการ Forms
ปรับปรุง package.json ในส่วนของ scripts เพื่อเรียก proxy ให้ทำงาน
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
Spring Boot สร้าง POST ด้วย Basic Authentication
เนื้อหาการใช้ Spring Boot ทำ RESTful Web Service ผมเคยเขียนไปแล้ว สามารถอ่านได้ที่
และอ่านเพิ่มเติมเรื่อง Exception กับ RESTful Web Service ได้ที่
เริ่มที่การแบ่ง package กันก่อน ปกติผมจะแบ่งแบบ MVC แต่หนนี้จะแบ่งตามเรื่องไป
ให้ resource แรกเป็น User ดังนั้นสร้าง package ชื่อ user ด้านในประกอบด้วย
- User (Entity)
- UserController (REST Controller)
- UserRepository (CRUD repository)
หมายเหตุ เนื่องจาก Microsoft SQL Server 2017 ที่ใช้นี้ user เป็นคำสงวน ดังนั้นจะใช้ชื่อตารางว่า users แทน
ลองมาเรียกด้วย Postman ดูหน่อย
หากอยากเปลี่ยน username กับ password ที่ Spring Security จัดเตรียมไว้ให้ล่ะ
เปิดไฟล์ application.properties แล้วเพิ่ม
spring.security.user.name=adminspring.security.user.password=1234
rerun แล้วเรียกใหม่
สร้าง user สักคน (ไว้ทดสอบ) ให้เปิดคลาส BoopackApplication (คลาสที่มีเมธอด main) แล้ว implement CommandLineRunner
ดูตัวอย่างเพิ่มเติม
เพราะ user แต่ละคนจำต้องมี email และ password ซึ่ง password ควรที่จะเข้ารหัสทุกครั้งแม้เก็บลงฐานข้อมูล Spring ก็มีทางออกของเรื่องนี้ครับ
ทีนี้กลับไปเรียกด้วย Postman
เหตุเพราะ Spring Security ทราบว่าเราใช้ PasswordEncoder เมื่อเป็นอย่างนี้มันจึงเข้าใจว่า 1234 (password สมมติข้างต้น) ก็ต้องถูก encode ด้วย PasswordEncoder โปรดสังเกตที่ Log
Encoded password does not look like BCrypt
แก้ไขอย่างไร?
Spring Security มีคลาส WebSecurityConfigurerAdapter ทำหน้าที่รับผิดชอบกระบวนการ Authentication โดยเมธอดชื่อ
configure(AuthenticationManagerBuilder auth)
ดังนั้นเราจะ override เมธอดนี้ เพื่อบอกกับ Spring Security ว่าเข้ารหัสด้วย PasswordEncoder นะ
สร้าง package ใหม่ชื่อว่า security จากนั้นสร้างคลาส CustomWebSecurityConfigurerAdapter สืบทอดคลาส WebSecurityConfigurerAdapter
เมื่อมีตรงนี้แล้วในไฟล์ application.properties ก็ลบหรือ comment สองบรรทัดนี้เสีย
#spring.security.user.name=admin#spring.security.user.password=1234
rerun แล้วทดสอบใหม่!
เอาล่ะ ยาวไปจึงขอตัด part นี้เท่านี้ก่อน มาสรุปว่าเราได้อะไรไปบ้าง
- frontend ใช้ Angular เราเตรียมหน้า Login ไว้ เราต้องการส่ง email กับ password ผ่านเข้าไปทาง /login ซึ่งขณะนี้ยังไม่ทำที่ Spring Security
- backend ใช้ Spring Security ทำ Basic Authentication การขอ resources ใดจากมันจะต้องระบุ Authorization แบบ Basic Authentication ไปด้วยเสมอ
- และเมื่อทำ Basic Authentication สำเร็จจะได้ JSESSIONID กลับมาในรูปของ Cookies
- ใน part ถัดไปเราจะใช้ email และ password ที่ได้บันทึกไว้ในฐานข้อมูลทำ Basic Authentication (แทน admin กับ 1234)
- และสร้างหน้า Register ด้วย
เรื่องใหม่และท้าทายสำหรับผมต่อโปรเจกต์นี้คือ Spring Security Session Management พูดว่า Spring มีระบบคิดและจัดการอย่างไรบ้าง ก็หวังว่าเหล่านี้ที่ทำจะเป็นประโยชน์ต่อเพื่อนที่เข้ามาอ่านนะครับ