Spring Security案例

1. Spring Data JPA简介

Spring Data JPA 中文文档 (springdoc.cn)

Spring Data JPA 是 Spring Data 项目的一部分,它提供了一种简化的数据访问方式,用于与关系型数据库进行交互。它基于 Java Persistence API(JPA) 标准,并提供了一套简洁的 API 和注解,使开发人员能够通过简单的 Java 对象来表示数据库表,并通过自动生成的 SQL 语句执行常见的 CRUD 操作。Spring Data JPA 通过封装 JPA 的复杂性,简化了数据访问层的开发工作,使开发人员能够更专注于业务逻辑的实现。它还提供了丰富的查询方法的定义、分页和排序支持、事务管理等功能,使开发人员能够更方便地进行数据访问和操作。简单说就是和mybatis差不错的东西,实现上有所区别。

2. Thymeleaf模板引擎

Thymeleaf**一个html模板引擎,简化开发流程

1
2
3
4
5
6
7
8
<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>

文件路径

image-20250527215358563

路径映射

1
2
3
4
5
6
7
8
spring:
thymeleaf:
suffix: .html
cache: false
prefix: classpath:/templates/
web:
resources:
static-locations: classpath:/static/

访问接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.kdd.chapter07.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* @description: 路径映射
* @author: 康弟弟
* @date: 2025/5/27 21:54
* @param:
* @return:
**/
@Controller
public class FileController {
@Autowired
private RedisTemplate redisTemplate;

@GetMapping("/detail/{type}/{path}")
public String toDetail(@PathVariable("type") String type, @PathVariable("path") String path) {
return "/detail/" + type + "/" + path;
}

@GetMapping("/userLogin")//与login.html中的表单跳转th:action="@{/userLogin}的路径一致
public String toLoginPage() {
return "/login/login";//跳转到静态资源login文件夹的login.html页面
}

}

3. Spring Security安全管理

3.1 简介

Spring Security 中文文档 :: Spring Security Reference (springdoc.cn)

Spring Security是一个Java框架,用于保护应用程序的安全性。它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。Spring Security基于过滤器链的概念,可以轻松地集成到任何基于Spring的应用程序中。它支持多种身份验证选项和授权策略,开发人员可以根据需要选择适合的方式。此外,Spring Security还提供了一些附加功能,如集成第三方身份验证提供商和单点登录,以及会话管理和密码编码等。总之,Spring Security是一个强大且易于使用的框架,可以帮助开发人员提高应用程序的安全性和可靠性。简单来说就是一个提供登录拦截等等用户管理的。

3.2 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!-- Security与Thymeleaf整合实现前端页面安全访问控制 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!-- Spring Data JPA操作数据库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

此时光导入不配置任何会提供一个默认的login页面,可以在后续进行配置替换这个登录页面,用户名是user,密码会在控制台中随机生成。

image-20250527215810692

3.3 连接数据库

1
2
3
4
datasource:
url: jdbc:mysql://localhost:3306/springbootdata?useSSL=false
username: root
password: 123456

JPA的实体类和mybatis的实体类定义有不一样,使用implements Serializable将用户数据进行反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.kdd.chapter07.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.io.Serializable;

@Entity(name = "t_authority ")
public class Authority implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String authority ;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getAuthority() {
return authority;
}

public void setAuthority(String authority) {
this.authority = authority;
}

@Override
public String toString() {
return "Authority{" +
"id=" + id +
", authority='" + authority + '\'' +
'}';
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.kdd.chapter07.domain;

import javax.persistence.*;
import java.io.Serializable;

@Entity(name = "t_customer")
public class Customer implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String password;


public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@Override
public String toString() {
return "Customer{" +
"id=" + id +
", username='" + username + '\'' +
", password=" + password +
'}';
}
}

在以上准备工作完成后,在configure方法中使用JDBC身份认证的方式 进行自定义用户认证。SecurityConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.kdd.chapter07;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer;
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 javax.sql.DataSource;

@EnableWebSecurity //开启MVC Security安全支持,
public class SecurityConfig extends WebSecurityConfigurerAdapter {//用于MVC Security自定义配置
@Autowired
private DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置密码编码器,从Spring Security 5开始,自定义用户认证必须设置密码编码器用于保护密码,并提供了多种密码编码器
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();

/*注释内存身份认证//构建用户
final InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer = auth.inMemoryAuthentication().passwordEncoder(bCryptPasswordEncoder);
//模拟两用户,设置用户名withUser,设置密码password,引用密码编码器加密,设置用户权限roles()
authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer.withUser("shitou").password(bCryptPasswordEncoder.encode("123")).roles("comment");
authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer.withUser("李四").password(bCryptPasswordEncoder.encode("321")).roles("vip");
*/
//使用JDBC进行身份认证
String userSQL ="select username,password,valid from t_customer where username = ?";
String authoritySQL="select c.username,a.authority from t_customer c,t_authority a,t_customer_authority ca where ca.customer_id=c.id and ca.authority_id=a.id and c.username =?";
auth.jdbcAuthentication().passwordEncoder(bCryptPasswordEncoder).dataSource(dataSource).usersByUsernameQuery(userSQL).authoritiesByUsernameQuery(authoritySQL);
}
}

此时可以使用数据库中的用户信息进行登录了。

3.4 连接redis减少IO访问

1
2
3
4
5
6
7
8
9
10
<!-- Redis缓存启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version> <!-- 使用最新版本 -->
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
redis:
host: localhost
port: 6379
password: ''
database: 0
timeout: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms

创建查询语句,在JPA中叫做Repository,和mybatis中的mapper一样的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kdd.chapter07.repository;

import com.kdd.chapter07.domain.Authority;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface AuthorityRepository extends JpaRepository<Authority,Integer> {
@Query(value = "select a.* from t_customer c,t_authority a,t_customer_authority ca where ca.customer_id=c.id and ca.authority_id=a.id and c.username =?1",nativeQuery = true)
public List<Authority> findAuthoritiesByUsername(String username);

}
1
2
3
4
5
6
7
8
package com.kdd.chapter07.repository;

import com.kdd.chapter07.domain.Customer;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CustomerRepository extends JpaRepository<Customer,Integer> {
Customer findByUsername(String username);
}

创建定义查询用户及角色信息的服务接口,实现对用户数据结合Redis缓存进行业务处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.kdd.chapter07.service;

import com.kdd.chapter07.domain.Authority;
import com.kdd.chapter07.domain.Customer;
import com.kdd.chapter07.repository.AuthorityRepository;
import com.kdd.chapter07.repository.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CustomerService {
@Autowired
private CustomerRepository customerRepository;
@Autowired
private AuthorityRepository authorityRepository;
@Autowired
private RedisTemplate redisTemplate;

// 业务控制:使用唯一用户名查询用户信息
public Customer getCustomer(String username){
Customer customer=null;
Object o = redisTemplate.opsForValue().get("customer_"+username);
if(o!=null){
customer=(Customer)o;
}else {
customer = customerRepository.findByUsername(username);
if(customer!=null){
redisTemplate.opsForValue().set("customer_"+username,customer);
}
}
return customer;
}
// 业务控制:使用唯一用户名查询用户权限
public List<Authority> getCustomerAuthority(String username){
List<Authority> authorities=null;
Object o = redisTemplate.opsForValue().get("authorities_"+username);
if(o!=null){
authorities=(List<Authority>)o;
}else {
authorities=authorityRepository.findAuthoritiesByUsername(username);
if(authorities.size()>0){
redisTemplate.opsForValue().set("authorities_"+username,authorities);
}
}
return authorities;
}
}

在configure方法中(先注释掉JDBC身份认证方式),使用UserDetailsService身份认证进行自定义用户认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@EnableWebSecurity //开启MVC Security安全支持,
public class SecurityConfig extends WebSecurityConfigurerAdapter {//用于MVC Security自定义配置
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置密码编码器,从Spring Security 5开始,自定义用户认证必须设置密码编码器用于保护密码,并提供了多种密码编码器
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();

//使用UserDetailsService身份认证
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
}

此时可以查看redis中的登录数据,乱码是因为传输的二进制数据,如果需要的话可以使用json传输中文,可以查看前面的redis使用。

image-20250527221031000

4. 进阶

4.1 配置拦截

在该项目案例中,用户有两个身份,我们需要对不同身份的对象展示不同的数据就可以对configure进行进一步的配置,配置登录用户中的不同身份限制其范围访问。在SecurityConfig中添加重载。

1
2
3
4
5
6
7
8
9
10
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll()//对“/”路径开头的不拦截
// 需要对static文件夹下login文件夹对应的静态资源进行统一放行
.antMatchers("/login/**").permitAll()
.antMatchers("/detail/common/**").hasRole("common")//对路径为"/detail/common/"开头的进行拦截,角色为common的不拦截
.antMatchers("detail/vip/**").hasRole("vip")//对路径为"/detail/vip/"开头的进行拦截,角色为vip的不拦截
.anyRequest().authenticated()//除去以上的3个映射路径以外,其他的均要求登录
.and().formLogin();//配置默认登陆页面

}

4.2 登录替换

在4.1重装的配置中添加http.formLogin()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll()//对“/”路径开头的不拦截
// 需要对static文件夹下login文件夹对应的静态资源进行统一放行
.antMatchers("/login/**").permitAll()
.antMatchers("/detail/common/**").hasRole("common")//对路径为"/detail/common/"开头的进行拦截,角色为common的不拦截
.antMatchers("detail/vip/**").hasRole("vip")//对路径为"/detail/vip/"开头的进行拦截,角色为vip的不拦截
.anyRequest().authenticated()//除去以上的3个映射路径以外,其他的均要求登录
.and().formLogin();//配置默认登陆页面

// 自定义用户登录控制
http.formLogin()
.loginPage("/userLogin").permitAll()//配置登陆路径,跟页面中的设置一致
.usernameParameter("name").passwordParameter("pwd")//接收前台传递的用户名写密码,跟login.html页面的保持一致
.defaultSuccessUrl("/")//登陆成功跳转到首页
.failureUrl("/userLogin?error");//登陆失败返回登陆页面参数为?error


}

同样的可以添加退出和记住我功能。

1
2
3
4
5
6
7
8
// 自定义用户退出控制
http.logout()
.logoutUrl("/mylogout")//跟index.html"注销"表单中的路径一致
.logoutSuccessUrl("/");//注销成功后的跳转页面为根目录的首页
// 定制Remember-me记住我功能
http.rememberMe()
.rememberMeParameter("rememberme")
.tokenValiditySeconds(200);

5. 文件备份

5.1 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.kdd</groupId>
<artifactId>chapter07</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>chapter07</name>
<description>chapter07</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<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>
<!--` security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!-- JDBC-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- Security与Thymeleaf整合实现前端页面安全访问控制 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!-- Redis缓存启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version> <!-- 使用最新版本 -->
</dependency>
<!-- Spring Data JPA操作数据库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

5.2 application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
spring:
thymeleaf:
suffix: .html
cache: false
prefix: classpath:/templates/
web:
resources:
static-locations: classpath:/static/

datasource:
url: jdbc:mysql://localhost:3306/springbootdata?useSSL=false
username: root
password: 123456

redis:
host: localhost
port: 6379
password: ''
database: 0
timeout: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms

5.3 redis启动

双击运行即可

image-20250527221910471

5.4 sql表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 选择使用数据库
USE chapter07;
# 创建表t_customer并插入相关数据,密码为123456
DROP TABLE IF EXISTS `t_customer`;
CREATE TABLE `t_customer` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`username` varchar(200) DEFAULT NULL,
`password` varchar(200) DEFAULT NULL,
`valid` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
INSERT INTO `t_customer` VALUES ('1', 'shitou', '$2a$10$5ooQI8dir8jv0/gCa1Six.GpzAdIPf6pMqdminZ/3ijYzivCyPlfK', '1');
INSERT INTO `t_customer` VALUES ('2', '李四', '$2a$10$5ooQI8dir8jv0/gCa1Six.GpzAdIPf6pMqdminZ/3ijYzivCyPlfK', '1');
# 创建表t_authority并插入相关数据
DROP TABLE IF EXISTS `t_authority`;
CREATE TABLE `t_authority` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`authority` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO `t_authority` VALUES ('1', 'ROLE_common');
INSERT INTO `t_authority` VALUES ('2', 'ROLE_vip');
# 创建表t_customer_authority并插入相关数据
DROP TABLE IF EXISTS `t_customer_authority`;
CREATE TABLE `t_customer_authority` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`customer_id` int(20) DEFAULT NULL,
`authority_id` int(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
INSERT INTO `t_customer_authority` VALUES ('1', '1', '1');
INSERT INTO `t_customer_authority` VALUES ('2', '2', '2');

# 记住我功能中创建持久化Token存储的数据表
create table persistent_logins (username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null);

5.5 项目结构

image-20250527222022401