Tạo ứng dụng Login với Spring Boot, Spring Security, JPA
1. Mục tiêu của ví dụ
Tài liệu được viết dựa trên:
- Spring Boot 2.x
- Spring Security
- JPA
- Thymeleaf (Or JSP)
- Database: MySQL, SQL Server, Oracle, Postgres
Trong bài viết này tôi sẽ hướng dẫn bạn tạo một ứng dụng Login sử dụng Spring Boot + Spring Security + JPA + Thymeleaf. Và giải thích nguyên tắc hoạt động của Spring Security.
Bắt buộc người dùng phải đăng nhập mới được xem các trang được bảo hộ:
Người dùng đã đăng nhập vào hệ thống chỉ được phép xem các trang thuộc phạm vi vai trò của họ. Nếu truy cập vào các trang được bảo hộ nằm ngoài vai trò của họ truy cập sẽ bị từ chối.
2. Chuẩn bị Database
Trong cơ sở dữ liệu chúng ta có 3 bảng APP_USER, APP_ROLE, USER_ROLE đây là các bảng mà bạn cần quan tâm. Ngoài ra, một bảng khác là PERSISTENT_LOGINS, bảng này được sử dụng bởi Spring Remember Me API để lưu trữ thông tin Token, và thời điểm đăng nhập cuối cùng của mỗi người dùng.
MySQL
-- Create table
create table APP_USER
(
USER_ID BIGINT not null,
USER_NAME VARCHAR(36) not null,
ENCRYTED_PASSWORD VARCHAR(128) not null,
ENABLED BIT not null
) ;
--
alter table APP_USER
add constraint APP_USER_PK primary key (USER_ID);
alter table APP_USER
add constraint APP_USER_UK unique (USER_NAME);
-- Create table
create table APP_ROLE
(
ROLE_ID BIGINT not null,
ROLE_NAME VARCHAR(30) not null
) ;
--
alter table APP_ROLE
add constraint APP_ROLE_PK primary key (ROLE_ID);
alter table APP_ROLE
add constraint APP_ROLE_UK unique (ROLE_NAME);
-- Create table
create table USER_ROLE
(
ID BIGINT not null,
USER_ID BIGINT not null,
ROLE_ID BIGINT not null
);
--
alter table USER_ROLE
add constraint USER_ROLE_PK primary key (ID);
alter table USER_ROLE
add constraint USER_ROLE_UK unique (USER_ID, ROLE_ID);
alter table USER_ROLE
add constraint USER_ROLE_FK1 foreign key (USER_ID)
references APP_USER (USER_ID);
alter table USER_ROLE
add constraint USER_ROLE_FK2 foreign key (ROLE_ID)
references APP_ROLE (ROLE_ID);
-- Used by Spring Remember Me API.
CREATE TABLE Persistent_Logins (
username varchar(64) not null,
series varchar(64) not null,
token varchar(64) not null,
last_used timestamp not null,
PRIMARY KEY (series)
);
--------------------------------------
insert into App_User (USER_ID, USER_NAME, ENCRYTED_PASSWORD, ENABLED)
values (2, 'dbuser1', '$2a$10$PrI5Gk9L.tSZiW9FXhTS8O8Mz9E97k2FZbFvGFFaSsiTUIl.TCrFu', 1);
insert into App_User (USER_ID, USER_NAME, ENCRYTED_PASSWORD, ENABLED)
values (1, 'dbadmin1', '$2a$10$PrI5Gk9L.tSZiW9FXhTS8O8Mz9E97k2FZbFvGFFaSsiTUIl.TCrFu', 1);
---
insert into app_role (ROLE_ID, ROLE_NAME)
values (1, 'ROLE_ADMIN');
insert into app_role (ROLE_ID, ROLE_NAME)
values (2, 'ROLE_USER');
---
insert into user_role (ID, USER_ID, ROLE_ID)
values (1, 1, 1);
insert into user_role (ID, USER_ID, ROLE_ID)
values (2, 1, 2);
insert into user_role (ID, USER_ID, ROLE_ID)
values (3, 2, 2);
---
SQL Server
-- Create table
create table APP_USER
(
USER_ID BIGINT not null,
USER_NAME VARCHAR(36) not null,
ENCRYTED_PASSWORD VARCHAR(128) not null,
ENABLED BIT not null
) ;
--
alter table APP_USER
add constraint APP_USER_PK primary key (USER_ID);
alter table APP_USER
add constraint APP_USER_UK unique (USER_NAME);
-- Create table
create table APP_ROLE
(
ROLE_ID BIGINT not null,
ROLE_NAME VARCHAR(30) not null
) ;
--
alter table APP_ROLE
add constraint APP_ROLE_PK primary key (ROLE_ID);
alter table APP_ROLE
add constraint APP_ROLE_UK unique (ROLE_NAME);
-- Create table
create table USER_ROLE
(
ID BIGINT not null,
USER_ID BIGINT not null,
ROLE_ID BIGINT not null
);
--
alter table USER_ROLE
add constraint USER_ROLE_PK primary key (ID);
alter table USER_ROLE
add constraint USER_ROLE_UK unique (USER_ID, ROLE_ID);
alter table USER_ROLE
add constraint USER_ROLE_FK1 foreign key (USER_ID)
references APP_USER (USER_ID);
alter table USER_ROLE
add constraint USER_ROLE_FK2 foreign key (ROLE_ID)
references APP_ROLE (ROLE_ID);
-- Used by Spring Remember Me API.
CREATE TABLE Persistent_Logins (
username varchar(64) not null,
series varchar(64) not null,
token varchar(64) not null,
last_used Datetime not null,
PRIMARY KEY (series)
);
--------------------------------------
insert into App_User (USER_ID, USER_NAME, ENCRYTED_PASSWORD, ENABLED)
values (2, 'dbuser1', '$2a$10$PrI5Gk9L.tSZiW9FXhTS8O8Mz9E97k2FZbFvGFFaSsiTUIl.TCrFu', 1);
insert into App_User (USER_ID, USER_NAME, ENCRYTED_PASSWORD, ENABLED)
values (1, 'dbadmin1', '$2a$10$PrI5Gk9L.tSZiW9FXhTS8O8Mz9E97k2FZbFvGFFaSsiTUIl.TCrFu', 1);
---
insert into app_role (ROLE_ID, ROLE_NAME)
values (1, 'ROLE_ADMIN');
insert into app_role (ROLE_ID, ROLE_NAME)
values (2, 'ROLE_USER');
---
insert into user_role (ID, USER_ID, ROLE_ID)
values (1, 1, 1);
insert into user_role (ID, USER_ID, ROLE_ID)
values (2, 1, 2);
insert into user_role (ID, USER_ID, ROLE_ID)
values (3, 2, 2);
ORACLE
-- Create table
create table APP_USER
(
USER_ID NUMBER(19) not null,
USER_NAME VARCHAR2(36) not null,
ENCRYTED_PASSWORD VARCHAR2(128) not null,
ENABLED NUMBER(1) not null
) ;
--
alter table APP_USER
add constraint APP_USER_PK primary key (USER_ID);
alter table APP_USER
add constraint APP_USER_UK unique (USER_NAME);
-- Create table
create table APP_ROLE
(
ROLE_ID NUMBER(19) not null,
ROLE_NAME VARCHAR2(30) not null
) ;
--
alter table APP_ROLE
add constraint APP_ROLE_PK primary key (ROLE_ID);
alter table APP_ROLE
add constraint APP_ROLE_UK unique (ROLE_NAME);
-- Create table
create table USER_ROLE
(
ID NUMBER(19) not null,
USER_ID NUMBER(19) not null,
ROLE_ID NUMBER(19) not null
);
--
alter table USER_ROLE
add constraint USER_ROLE_PK primary key (ID);
alter table USER_ROLE
add constraint USER_ROLE_UK unique (USER_ID, ROLE_ID);
alter table USER_ROLE
add constraint USER_ROLE_FK1 foreign key (USER_ID)
references APP_USER (USER_ID);
alter table USER_ROLE
add constraint USER_ROLE_FK2 foreign key (ROLE_ID)
references APP_ROLE (ROLE_ID);
-- Used by Spring Remember Me API.
CREATE TABLE Persistent_Logins (
username varchar2(64) not null,
series varchar2(64) not null,
token varchar2(64) not null,
last_used Date not null,
PRIMARY KEY (series)
);
--------------------------------------
insert into App_User (USER_ID, USER_NAME, ENCRYTED_PASSWORD, ENABLED)
values (2, 'dbuser1', '$2a$10$PrI5Gk9L.tSZiW9FXhTS8O8Mz9E97k2FZbFvGFFaSsiTUIl.TCrFu', 1);
insert into App_User (USER_ID, USER_NAME, ENCRYTED_PASSWORD, ENABLED)
values (1, 'dbadmin1', '$2a$10$PrI5Gk9L.tSZiW9FXhTS8O8Mz9E97k2FZbFvGFFaSsiTUIl.TCrFu', 1);
---
insert into app_role (ROLE_ID, ROLE_NAME)
values (1, 'ROLE_ADMIN');
insert into app_role (ROLE_ID, ROLE_NAME)
values (2, 'ROLE_USER');
---
insert into user_role (ID, USER_ID, ROLE_ID)
values (1, 1, 1);
insert into user_role (ID, USER_ID, ROLE_ID)
values (2, 1, 2);
insert into user_role (ID, USER_ID, ROLE_ID)
values (3, 2, 2);
---
Commit;
Postgres
-- Create table
create table APP_USER
(
USER_ID BIGINT not null,
USER_NAME VARCHAR(36) not null,
ENCRYTED_PASSWORD VARCHAR(128) not null,
ENABLED Int not null
) ;
--
alter table APP_USER
add constraint APP_USER_PK primary key (USER_ID);
alter table APP_USER
add constraint APP_USER_UK unique (USER_NAME);
-- Create table
create table APP_ROLE
(
ROLE_ID BIGINT not null,
ROLE_NAME VARCHAR(30) not null
) ;
--
alter table APP_ROLE
add constraint APP_ROLE_PK primary key (ROLE_ID);
alter table APP_ROLE
add constraint APP_ROLE_UK unique (ROLE_NAME);
-- Create table
create table USER_ROLE
(
ID BIGINT not null,
USER_ID BIGINT not null,
ROLE_ID BIGINT not null
);
--
alter table USER_ROLE
add constraint USER_ROLE_PK primary key (ID);
alter table USER_ROLE
add constraint USER_ROLE_UK unique (USER_ID, ROLE_ID);
alter table USER_ROLE
add constraint USER_ROLE_FK1 foreign key (USER_ID)
references APP_USER (USER_ID);
alter table USER_ROLE
add constraint USER_ROLE_FK2 foreign key (ROLE_ID)
references APP_ROLE (ROLE_ID);
-- Used by Spring Remember Me API.
CREATE TABLE Persistent_Logins (
username varchar(64) not null,
series varchar(64) not null,
token varchar(64) not null,
last_used timestamp not null,
PRIMARY KEY (series)
);
--------------------------------------
insert into App_User (USER_ID, USER_NAME, ENCRYTED_PASSWORD, ENABLED)
values (2, 'dbuser1', '$2a$10$PrI5Gk9L.tSZiW9FXhTS8O8Mz9E97k2FZbFvGFFaSsiTUIl.TCrFu', 1);
insert into App_User (USER_ID, USER_NAME, ENCRYTED_PASSWORD, ENABLED)
values (1, 'dbadmin1', '$2a$10$PrI5Gk9L.tSZiW9FXhTS8O8Mz9E97k2FZbFvGFFaSsiTUIl.TCrFu', 1);
---
insert into app_role (ROLE_ID, ROLE_NAME)
values (1, 'ROLE_ADMIN');
insert into app_role (ROLE_ID, ROLE_NAME)
values (2, 'ROLE_USER');
---
insert into user_role (ID, USER_ID, ROLE_ID)
values (1, 1, 1);
insert into user_role (ID, USER_ID, ROLE_ID)
values (2, 1, 2);
insert into user_role (ID, USER_ID, ROLE_ID)
values (3, 2, 2);
---
Commit;
3. Tạo Spring Boot Project
Trên Eclipse tạo một dự án Spring Boot.
Nhập vào:
- Name: SpringBootSecurityJPA
- Group: org.o7planning
- Artifact: SpringBootSecurityJPA
- Description: Spring Boot +Spring Security + JPA + Remember Me.
- Package: org.o7planning.sbsecurity
Bước tiếp theo, bạn cần lựa chọn các công nghệ và thư viện sẽ được sử dụng (Trong bài học này chúng ta sẽ kết nối vào cơ sở dữ liệu Oracle, MySQL, SQL Server hoặc Postgres).
Database Libraries:
- MySQL
- PostgresSQL
- SQL Server
- Web
- Thymeleaf
- Security
OK Project đã được tạo ra.
SpringBootSecurityJpaApplication.java
package org.o7planning.sbsecurity;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootSecurityJpaApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootSecurityJpaApplication.class, args);
}
}
4. Cấu hình pom.xml
Nếu bạn sử dụng cơ sở dữ liệu Oracle, bạn cần khai báo thư viện cần thiết cho Oracle trong tập tin pom.xml:
** Oracle **
<dependencies>
.....
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.3</version>
</dependency>
.....
</dependencies>
<repositories>
....
<!-- Repository for ORACLE JDBC Driver -->
<repository>
<id>codelds</id>
<url>https://code.lds.org/nexus/content/groups/main-repo</url>
</repository>
.....
</repositories>
Nếu bạn kết nối vào cơ sở dữ liệu SQL Service, bạn có thể sử dụng một trong 2 thư viện JTDS hoặc Mssql-Jdbc:
** SQL Server **
<dependencies>
.....
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>net.sourceforge.jtds</groupId>
<artifactId>jtds</artifactId>
<scope>runtime</scope>
</dependency>
.....
</dependencies>
Nội dung đầy đủ của tập tin pom.xml:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.o7planning</groupId>
<artifactId>SpringBootSecurityJPA</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>SpringBootSecurityJPA</name>
<description>Spring Boot +Spring Security + JPA + Remember Me</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.3</version>
</dependency>
<!-- SQL Server Mssql-Jdbc Driver -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<scope>runtime</scope>
</dependency>
<!-- SQL Server JTDS Driver -->
<dependency>
<groupId>net.sourceforge.jtds</groupId>
<artifactId>jtds</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<!-- Repository for ORACLE JDBC Driver -->
<repository>
<id>codelds</id>
<url>https://code.lds.org/nexus/content/groups/main-repo</url>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
5. Cấu hình Datasource
Để Spring có thể kết nối vào Database bạn cần cấu hình các thông số cần thiết trong tập tin application.properties.
application.properties (MySQL)
# ===============================
# DATABASE
# ===============================
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://tran-vmware-pc:3306/Test
spring.datasource.username=root
spring.datasource.password=12345
# ===============================
# JPA / HIBERNATE
# ===============================
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
application.properties (ORACLE)
# ===============================
# DATABASE
# ===============================
spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@tran-vmware-pc:1521:db12c
spring.datasource.username=Test
spring.datasource.password=12345
# ===============================
# JPA / HIBERNATE
# ===============================
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.Oracle10gDialect
application.properties (SQL Server + Mssql-jdbc Driver)
# ===============================
# DATABASE
# ===============================
spring.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.datasource.url=jdbc:sqlserver://tran-vmware-pc\\SQLEXPRESS:1433;databaseName=Test
spring.datasource.username=sa
spring.datasource.password=12345
# ===============================
# JPA / HIBERNATE
# ===============================
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.SQLServer2012Dialect
application.properties (SQL Server + JTDS driver)
# ===============================
# DATABASE
# ===============================
spring.datasource.driver-class-name=net.sourceforge.jtds.jdbc.Driver
spring.datasource.url=jdbc:jtds:sqlserver://tran-vmware-pc:1433/Test;instance=SQLEXPRESS
spring.datasource.username=sa
spring.datasource.password=12345
# ===============================
# JPA / HIBERNATE
# ===============================
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.SQLServerDialect
application.properties (Postgres)
# ===============================
# DATABASE CONNECTION
# ===============================
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://tran-vmware-pc:5432/Test
spring.datasource.username=postgres
spring.datasource.password=12345
# ===============================
# JPA / HIBERNATE
# ===============================
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# Fix Postgres JPA Error:
# Method org.postgresql.jdbc.PgConnection.createClob() is not yet implemented.
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
6. Cấu hình bảo mật & Remember Me
Ứng dụng này có một vài chức năng (trang), trong đó:
- /userInfo
Đây là trang xem thông tin người dùng, trang này đòi hỏi người dùng phải đăng nhập, và có vai trò ROLE_ADMIN hoặc ROLE_USER.
- /admin
Đây là trang dành cho người quản trị, yêu cầu người dùng phải đăng nhập, và chỉ những người có vai trò ROLE_ADMIN mới có quyền truy cập.
- /. /welcome, /login, /logout, /403
Tất cả các trang khác trong ứng dụng không đòi hỏi người dùng phải đăng nhập.
Lớp WebSecurityConfig được sử dụng để cấu hình bảo mật cho ứng dụng. Nó được chú thích (annotate) bởi @Configuration, Annotation này nói với Spring rằng nó là một lớp cấu hình, và vì vậy nó sẽ được Spring phân tích tại thời điểm ứng dụng được chạy.
WebSecurityConfig.java
package org.o7planning.sbsecurity.config;
import javax.sql.DataSource;
import org.o7planning.sbsecurity.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
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.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;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private DataSource dataSource;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return bCryptPasswordEncoder;
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// Sét đặt dịch vụ để tìm kiếm User trong Database.
// Và sét đặt PasswordEncoder.
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
// Các trang không yêu cầu login
http.authorizeRequests().antMatchers("/", "/login", "/logout").permitAll();
// Trang /userInfo yêu cầu phải login với vai trò ROLE_USER hoặc ROLE_ADMIN.
// Nếu chưa login, nó sẽ redirect tới trang /login.
http.authorizeRequests().antMatchers("/userInfo").access("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')");
// Trang chỉ dành cho ADMIN
http.authorizeRequests().antMatchers("/admin").access("hasRole('ROLE_ADMIN')");
// Khi người dùng đã login, với vai trò XX.
// Nhưng truy cập vào trang yêu cầu vai trò YY,
// Ngoại lệ AccessDeniedException sẽ ném ra.
http.authorizeRequests().and().exceptionHandling().accessDeniedPage("/403");
// Cấu hình cho Login Form.
http.authorizeRequests().and().formLogin()//
// Submit URL của trang login
.loginProcessingUrl("/j_spring_security_check") // Submit URL
.loginPage("/login")//
.defaultSuccessUrl("/userAccountInfo")//
.failureUrl("/login?error=true")//
.usernameParameter("username")//
.passwordParameter("password")
// Cấu hình cho Logout Page.
.and().logout().logoutUrl("/logout").logoutSuccessUrl("/logoutSuccessful");
// Cấu hình Remember Me.
http.authorizeRequests().and() //
.rememberMe().tokenRepository(this.persistentTokenRepository()) //
.tokenValiditySeconds(1 * 24 * 60 * 60); // 24h
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setDataSource(dataSource);
return db;
}
}
Tùy chọn "Remember Me" là gì?
Người dùng truy cập vào một website và đăng nhập. Sau đó người dùng tắt trình duyệt và truy cập vào website trong một thời điểm nào đó (Chẳng hạn ngày hôm sau), và họ phải đăng nhập lại, điều này gây ra các phiền phức không cần thiết. Tùy chọn "Remember Me" cho phép website "nhớ" thông tin người dùng để tự động đăng nhập khi người dùng truy cập vào website trong lần tiếp theo.
Khi người dùng đăng nhập vào ứng dụng với tùy chọn "Remember Me", Spring sẽ lưu trữ lại thông tin thời điểm đăng nhập cuối, và token. Token là một chuỗi (string) đã được mật mã hóa (encryted), nó chứa các thông tin cần thiết để Spring tự động đăng nhập khi người dùng truy cập vào website trong lần tiếp theo.
Có 2 cách thông dụng để Spring lưu trữ các thông tin này:
Có 2 cách thông dụng để Spring lưu trữ các thông tin này:
- Memory
- Database
** WebSecurityConfig **
// Token stored in Table (Persistent_Logins)
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setDataSource(this.dataSource);
return db;
}
// Token stored in Memory (Of Web Server).
@Bean
public PersistentTokenRepository persistentTokenRepository() {
InMemoryTokenRepositoryImpl memory = new InMemoryTokenRepositoryImpl();
return memory;
}
7. Các lớp Entity
AppRole.java
package org.o7planning.sbsecurity.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
@Entity
@Table(name = "App_Role", //
uniqueConstraints = { //
@UniqueConstraint(name = "APP_ROLE_UK", columnNames = "Role_Name") })
public class AppRole {
@Id
@GeneratedValue
@Column(name = "Role_Id", nullable = false)
private Long roleId;
@Column(name = "Role_Name", length = 30, nullable = false)
private String roleName;
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
}
AppUser.java
package org.o7planning.sbsecurity.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
@Entity
@Table(name = "App_User", //
uniqueConstraints = { //
@UniqueConstraint(name = "APP_USER_UK", columnNames = "User_Name") })
public class AppUser {
@Id
@GeneratedValue
@Column(name = "User_Id", nullable = false)
private Long userId;
@Column(name = "User_Name", length = 36, nullable = false)
private String userName;
@Column(name = "Encryted_Password", length = 128, nullable = false)
private String encrytedPassword;
@Column(name = "Enabled", length = 1, nullable = false)
private boolean enabled;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getEncrytedPassword() {
return encrytedPassword;
}
public void setEncrytedPassword(String encrytedPassword) {
this.encrytedPassword = encrytedPassword;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
UserRole.java
package org.o7planning.sbsecurity.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
@Entity
@Table(name = "User_Role", //
uniqueConstraints = { //
@UniqueConstraint(name = "USER_ROLE_UK", columnNames = { "User_Id", "Role_Id" }) })
public class UserRole {
@Id
@GeneratedValue
@Column(name = "Id", nullable = false)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "User_Id", nullable = false)
private AppUser appUser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "Role_Id", nullable = false)
private AppRole appRole;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public AppUser getAppUser() {
return appUser;
}
public void setAppUser(AppUser appUser) {
this.appUser = appUser;
}
public AppRole getAppRole() {
return appRole;
}
public void setAppRole(AppRole appRole) {
this.appRole = appRole;
}
}
8. DAO, WebUtils
Các lớp DAO (Data Access Object) là các lớp sử dụng để truy cập vào cơ sở dữ liệu, chẳng hạn Query, Insert, Update, Delete. Các lớp DAO thường được chú thích bởi @Repository để nói với Spring rằng hãy quản lý chúng như các Spring BEAN.
Lớp AppUserDAO sử dụng để thao tác với bảng APP_USER. Nó có một phương thức để tìm kiếm một người dùng trong Database ứng với tên đăng nhập nào đó.
AppUserDAO.java
package org.o7planning.sbsecurity.dao;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import org.o7planning.sbsecurity.entity.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@Repository
@Transactional
public class AppUserDAO {
@Autowired
private EntityManager entityManager;
public AppUser findUserAccount(String userName) {
try {
String sql = "Select e from " + AppUser.class.getName() + " e " //
+ " Where e.userName = :userName ";
Query query = entityManager.createQuery(sql, AppUser.class);
query.setParameter("userName", userName);
return (AppUser) query.getSingleResult();
} catch (NoResultException e) {
return null;
}
}
}
AppRoleDAO.java
package org.o7planning.sbsecurity.dao;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import org.o7planning.sbsecurity.entity.UserRole;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@Repository
@Transactional
public class AppRoleDAO {
@Autowired
private EntityManager entityManager;
public List<String> getRoleNames(Long userId) {
String sql = "Select ur.appRole.roleName from " + UserRole.class.getName() + " ur " //
+ " where ur.appUser.userId = :userId ";
Query query = this.entityManager.createQuery(sql, String.class);
query.setParameter("userId", userId);
return query.getResultList();
}
}
-
WebUtils.java
package org.o7planning.sbsecurity.utils;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
public class WebUtils {
public static String toString(User user) {
StringBuilder sb = new StringBuilder();
sb.append("UserName:").append(user.getUsername());
Collection<GrantedAuthority> authorities = user.getAuthorities();
if (authorities != null && !authorities.isEmpty()) {
sb.append(" (");
boolean first = true;
for (GrantedAuthority a : authorities) {
if (first) {
sb.append(a.getAuthority());
first = false;
} else {
sb.append(", ").append(a.getAuthority());
}
}
sb.append(")");
}
return sb.toString();
}
}
EncrytedPasswordUtils.java
package org.o7planning.sbsecurity.utils;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class EncrytedPasswordUtils {
// Encryte Password with BCryptPasswordEncoder
public static String encrytePassword(String password) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder.encode(password);
}
public static void main(String[] args) {
String password = "123";
String encrytedPassword = encrytePassword(password);
System.out.println("Encryted Password: " + encrytedPassword);
}
}
9. UserDetailsService
UserDetailsService là một interface trung tâm trong Spring Security. Nó là một dịch vụ để tìm kiếm "Tài khoản người dùng và các vai trò của người dùng đó". Nó được sử dụng bởi Spring Security mỗi lần người dùng đăng nhập vào hệ thống. Chính vì vậy bạn cần viết một lớp thi hành (implements) interface này.
Ở đây tôi tạo lớp UserDetailsServiceImpl thi hành (implements) interface UserDetailsService. Lớp UserDetailsServiceImpl được chú thích bởi @Service để nói với Spring rằng hãy quản lý nó như một Spring BEAN.
UserDetailsServiceImpl.java
package org.o7planning.sbsecurity.service;
import java.util.ArrayList;
import java.util.List;
import org.o7planning.sbsecurity.dao.AppUserDAO;
import org.o7planning.sbsecurity.entity.AppUser;
import org.o7planning.sbsecurity.dao.AppRoleDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private AppUserDAO appUserDAO;
@Autowired
private AppRoleDAO appRoleDAO;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
AppUser appUser = this.appUserDAO.findUserAccount(userName);
if (appUser == null) {
System.out.println("User not found! " + userName);
throw new UsernameNotFoundException("User " + userName + " was not found in the database");
}
System.out.println("Found User: " + appUser);
// [ROLE_USER, ROLE_ADMIN,..]
List<String> roleNames = this.appRoleDAO.getRoleNames(appUser.getUserId());
List<GrantedAuthority> grantList = new ArrayList<GrantedAuthority>();
if (roleNames != null) {
for (String role : roleNames) {
// ROLE_USER, ROLE_ADMIN,..
GrantedAuthority authority = new SimpleGrantedAuthority(role);
grantList.add(authority);
}
}
UserDetails userDetails = (UserDetails) new User(appUser.getUserName(), //
appUser.getEncrytedPassword(), grantList);
return userDetails;
}
}
10. Controllers
MainController.java
package org.o7planning.sbsecurity.controller;
import java.security.Principal;
import org.o7planning.sbsecurity.utils.WebUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class MainController {
@RequestMapping(value = { "/", "/welcome" }, method = RequestMethod.GET)
public String welcomePage(Model model) {
model.addAttribute("title", "Welcome");
model.addAttribute("message", "This is welcome page!");
return "welcomePage";
}
@RequestMapping(value = "/admin", method = RequestMethod.GET)
public String adminPage(Model model, Principal principal) {
User loginedUser = (User) ((Authentication) principal).getPrincipal();
String userInfo = WebUtils.toString(loginedUser);
model.addAttribute("userInfo", userInfo);
return "adminPage";
}
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String loginPage(Model model) {
return "loginPage";
}
@RequestMapping(value = "/logoutSuccessful", method = RequestMethod.GET)
public String logoutSuccessfulPage(Model model) {
model.addAttribute("title", "Logout");
return "logoutSuccessfulPage";
}
@RequestMapping(value = "/userInfo", method = RequestMethod.GET)
public String userInfo(Model model, Principal principal) {
// Sau khi user login thanh cong se co principal
String userName = principal.getName();
System.out.println("User Name: " + userName);
User loginedUser = (User) ((Authentication) principal).getPrincipal();
String userInfo = WebUtils.toString(loginedUser);
model.addAttribute("userInfo", userInfo);
return "userInfoPage";
}
@RequestMapping(value = "/403", method = RequestMethod.GET)
public String accessDenied(Model model, Principal principal) {
if (principal != null) {
User loginedUser = (User) ((Authentication) principal).getPrincipal();
String userInfo = WebUtils.toString(loginedUser);
model.addAttribute("userInfo", userInfo);
String message = "Hi " + principal.getName() //
+ "<br> You do not have permission to access this page!";
model.addAttribute("message", message);
}
return "403Page";
}
}
11. Thymeleaf Template
_menu.html được sử dụng như một phần của trang web, nó được nhúng vào trong các trang khác để tạo nên Menu của trang.
_menu.html
<div xmlns:th="http://www.thymeleaf.org"
style="border: 1px solid #ccc;padding:5px;margin-bottom:20px;">
<a th:href="@{/}">Home</a>
|
<a th:href="@{/userInfo}">User Info</a>
|
<a th:href="@{/admin}">Admin</a>
|
<a th:if="${#request.userPrincipal != null}" th:href="@{/logout}">Logout</a>
</div>
welcomePage.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:utext="${title}"></title>
</head>
<body>
<!-- Include _menu.html -->
<th:block th:include="/_menu"></th:block>
<h2>Message : <span th:utext="${message}"></span></h2>
</body>
</html>
loginPage.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login</title>
</head>
<body>
<!-- Include _menu.html -->
<th:block th:include="/_menu"></th:block>
<h1>Login</h1>
<!-- /login?error=true -->
<div th:if="${#request.getParameter('error') == 'true'}"
style="color:red;margin:10px 0px;">
Login Failed!!!<br />
Reason :
<span th:if="${#session!= null and #session.getAttribute('SPRING_SECURITY_LAST_EXCEPTION') != null}"
th:utext="${#session.getAttribute('SPRING_SECURITY_LAST_EXCEPTION').message}">
Static summary
</span>
</div>
<h3>Enter user name and password:</h3>
<form name='f' th:action="@{/j_spring_security_check}" method='POST'>
<table>
<tr>
<td>User:</td>
<td><input type='text' name='username' value=''></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='password' /></td>
</tr>
<tr>
<td>Remember Me?</td>
<td><input type="checkbox" name="remember-me" /></td>
</tr>
<tr>
<td><input name="submit" type="submit" value="submit" /></td>
</tr>
</table>
</form>
<br>
Username/pass:
<ul>
<li>dbuser1/123</li>
<li>dbadmin1/123</li>
</ul>
</body>
</html>
logoutSuccessfulPage.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Logout</title>
</head>
<body>
<!-- Include _menu.html -->
<th:block th:include="/_menu"></th:block>
<h1>Logout Successful!</h1>
</body>
</html>
userInfoPage.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>User Info</title>
</head>
<body>
<!-- Include _menu.html -->
<th:block th:include="/_menu"></th:block>
<h2>User Info Page</h2>
<h3>Welcome : <span th:utext="${#request.userPrincipal.name}"></span></h3>
<b>This is protected page!</b>
<br/><br/>
<div th:if="${userInfo != null}" th:utext="${userInfo}"></div>
</body>
</html>
adminPage.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:utext="${title}"></title>
</head>
<body>
<!-- Include _menu.html -->
<th:block th:include="/_menu"></th:block>
<h2>Admin Page</h2>
<h3>Welcome :
<span th:utext="${#request.userPrincipal.name}"></span>
</h3>
<b>This is protected page!</b>
<br/><br/>
<div th:if="${userInfo != null}" th:utext="${userInfo}"></div>
</body>
</html>
Nếu người dùng đã đăng nhập vào hệ thống, nhưng truy cập vào một trang không được phép (Không thuộc vai trò của họ), hệ thống sẽ hiển thị nội dung của trang /403 để thông báo truy cập bị cấm (Access Denied).
403Page.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Access Denied</title>
</head>
<body>
<!-- Include _menu.html -->
<th:block th:include="/_menu"></th:block>
<h3 th:if="${message != null}" th:utext="${message}" style="color: red;"></h3>
<div th:if="${userInfo != null}" th:utext="${userInfo}"></div>
</body>
</html>
Các hướng dẫn Spring Boot
- Cài đặt Spring Tool Suite cho Eclipse
- Hướng dẫn lập trình Spring cho người mới bắt đầu
- Hướng dẫn lập trình Spring Boot cho người mới bắt đầu
- Các thuộc tính thông dụng của Spring Boot
- Hướng dẫn sử dụng Spring Boot và Thymeleaf
- Hướng dẫn sử dụng Spring Boot và FreeMarker
- Hướng dẫn sử dụng Spring Boot và Groovy
- Hướng dẫn sử dụng Spring Boot và Mustache
- Hướng dẫn sử dụng Spring Boot và JSP
- Hướng dẫn sử dụng Spring Boot, Apache Tiles, JSP
- Sử dụng Logging trong Spring Boot
- Giám sát ứng dụng với Spring Boot Actuator
- Tạo ứng dụng web đa ngôn ngữ với Spring Boot
- Sử dụng nhiều ViewResolver trong Spring Boot
- Sử dụng Twitter Bootstrap trong Spring Boot
- Hướng dẫn và ví dụ Spring Boot Interceptor
- Hướng dẫn sử dụng Spring Boot, Spring JDBC và Spring Transaction
- Hướng dẫn và ví dụ Spring JDBC
- Hướng dẫn sử dụng Spring Boot, JPA và Spring Transaction
- Hướng dẫn sử dụng Spring Boot và Spring Data JPA
- Hướng dẫn sử dụng Spring Boot, Hibernate và Spring Transaction
- Tương tác Spring Boot, JPA và cơ sở dữ liệu H2
- Hướng dẫn sử dụng Spring Boot và MongoDB
- Sử dụng nhiều DataSource với Spring Boot và JPA
- Sử dụng nhiều DataSource với Spring Boot và RoutingDataSource
- Tạo ứng dụng Login với Spring Boot, Spring Security, Spring JDBC
- Tạo ứng dụng Login với Spring Boot, Spring Security, JPA
- Tạo ứng dụng đăng ký tài khoản với Spring Boot, Spring Form Validation
- Ví dụ OAuth2 Social Login trong Spring Boot
- Chạy các nhiệm vụ nền theo lịch trình trong Spring
- Ví dụ CRUD Restful Web Service với Spring Boot
- Ví dụ Spring Boot Restful Client với RestTemplate
- Ví dụ CRUD với Spring Boot, REST và AngularJS
- Bảo mật Spring Boot RESTful Service sử dụng Basic Authentication
- Bảo mật Spring Boot RESTful Service sử dụng Auth0 JWT
- Ví dụ Upload file với Spring Boot
- Ví dụ Download file với Spring Boot
- Ví dụ Upload file với Spring Boot và jQuery Ajax
- Ví dụ Upload file với Spring Boot và AngularJS
- Tạo ứng dụng Web bán hàng với Spring Boot, Hibernate
- Hướng dẫn và ví dụ Spring Email
- Tạo ứng dụng Chat đơn giản với Spring Boot và Websocket
- Triển khai ứng dụng Spring Boot trên Tomcat Server
- Triển khai ứng dụng Spring Boot trên Oracle WebLogic Server
- Cài đặt chứng chỉ SSL miễn phí Let's Encrypt cho Spring Boot
- Cấu hình Spring Boot chuyển hướng HTTP sang HTTPS
- Tìm nạp dữ liệu với Spring Data JPA DTO Projections
Show More