---
dir:
order: 2
---
# 之前SpringSecurity
**官方文档:**[https://docs.spring.io/spring-security/reference/index.html](https://docs.spring.io/spring-security/reference/index.html)
**功能:**
+ 身份认证(authentication)
+ 授权(authorization)
+ 防御常见攻击(protection against common attacks)
**身份认证:**
+ 身份认证是验证`谁正在访问系统资源`,判断用户是否为合法用户。认证用户的常见方式是要求用户输入用户名和密码。
**授权:**
+ 用户进行身份认证后,系统会控制`谁能访问哪些资源`,这个过程叫做授权。用户无法访问没有权限的资源。
## 身份认证
**官方代码示例:**[GitHub - spring-projects/spring-security-samples](https://github.com/spring-projects/spring-security-samples/tree/main)
### 项目的基本搭建
项目搭建完成后,默认端口是8080,直接访问`localhost:8080`即可。
**浏览器自动跳转到登录页面:**[http://localhost:8080/login](http://localhost:8080/login)
项目结构

#### 基本包
这里用到了数据库但是在项目刚开始启动时,是没有配置数据库的,这时候启动肯定会报错,所以我们现在启动类上排出连接数据库的类。
```xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.2.0
com.atguigu
security-demo
1.0-SNAPSHOT
jar
security-demo
https://maven.apache.org
UTF-8
21
21
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.thymeleaf.extras
thymeleaf-extras-springsecurity6
org.springframework.boot
spring-boot-starter-test
org.springframework.security
spring-security-test
org.springframework.boot
spring-boot-starter-thymeleaf
mysql
mysql-connector-java
8.0.30
com.baomidou
mybatis-plus-boot-starter
3.5.4.1
org.mybatis
mybatis-spring
org.mybatis
mybatis-spring
3.0.3
org.projectlombok
lombok
```
#### 创建启动类
在启动类上排出数据库的类。
```java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityDemoApplication.class, args);
}
}
```
#### 创建IndexController
```java
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "index";
}
}
```
#### 创建index.html
这里使用的动态标签`th:href="@{/logout}"`目的是可以自动检测路径变化,比如我们在配置文件中配置了全局路径参数。这时候动态标签会自动匹配。
这时需要访问根路径`localhost:8080/demo`
```yaml
server:
servlet:
context-path: /demo
```
HTML模板
```html
Hello Security!
Hello Security
Log Out
```
比如点击下面按钮会自动匹配路径并退出。

## 自定义Security配置
SecurityProperties修改默认用户和密码。
```yaml
spring:
security:
user:
name: user
password: admin123
```
### 使用配置类
```java
@Configuration
@EnableWebSecurity//Spring项目总需要添加此注解,SpringBoot项目中不需要
public class WebSecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser( //此行设置断点可以查看创建的user对象
User
.withDefaultPasswordEncoder()
.username("huan") //自定义用户名
.password("password") //自定义密码
.roles("USER") //自定义角色
.build()
);
return manager;
}
```
## 基于数据库的数据源
### 环境准备
创建三个数据库表并插入测试数据
```sql
-- 创建数据库
CREATE DATABASE `security-demo`;
USE `security-demo`;
-- 创建用户表
CREATE TABLE `user`(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(50) DEFAULT NULL ,
`password` VARCHAR(500) DEFAULT NULL,
`enabled` BOOLEAN NOT NULL
);
-- 唯一索引
CREATE UNIQUE INDEX `user_username_uindex` ON `user`(`username`);
-- 插入用户数据(密码是 "abc" )
INSERT INTO `user` (`username`, `password`, `enabled`) VALUES
('admin', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Helen', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Tom', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE);
```
#### 引入依赖
这个在之前也引用过了。
```xml
mysql
mysql-connector-java
8.0.30
com.baomidou
mybatis-plus-boot-starter
3.5.4.1
org.mybatis
mybatis-spring
org.mybatis
mybatis-spring
3.0.3
org.projectlombok
lombok
```
#### 配置文件中
```yaml
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${bunny.datasource.host}:${bunny.datasource.port}/${bunny.datasource.sqlData}?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf-8&allowPublicKeyRetrieval=true
username: ${bunny.datasource.username}
password: ${bunny.datasource.password}
```
#### 启动类
启动类记得删除排出数据库源的类。
```java
@SpringBootApplication
public class SecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityDemoApplication.class, args);
}
}
```
### 配置数据库查询
创建`DBUserDetailsManager`类实现UserDetailsManager, UserDetailsPasswordService方法。
查询数据库字段进行匹配,如果查询到并且密码正确就可以放行。记得在方法上加上`@Configuration`注解。
```java
@Configuration
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
User user = userMapper.selectOne(queryWrapper);
if (user == null) {
throw new UsernameNotFoundException(username);
} else {
Collection authorities = new ArrayList<>();
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getEnabled(),
true, // 用户账号是否过期
true, // 用户凭证是否过期
true, // 用户是否未被锁定
authorities); // 权限列表
}
}
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
return null;
}
@Override
public void createUser(UserDetails user) {
}
@Override
public void updateUser(UserDetails user) {
}
@Override
public void deleteUser(String username) {
}
@Override
public void changePassword(String oldPassword, String newPassword) {
}
@Override
public boolean userExists(String username) {
return false;
}
}
```
## 配置Security默认配置
+ `formLogin(withDefaults())`提供默认的登录模拟页面。
+ 如果开启了`formLogin(withDefaults())`可以`httpBasic(withDefaults())`屏蔽。
```java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity// Spring项目总需要添加此注解,SpringBoot项目中不需要
public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// authorizeRequests():开启授权保护
// anyRequest():对所有请求开启授权保护
// authenticated():已认证请求会自动被授权
httpSecurity
.authorizeRequests(authorize -> authorize
.anyRequest()
.authenticated())
// .formLogin(withDefaults())// 表单授权方式
.httpBasic(withDefaults());// 基本授权方式
return httpSecurity.build();
}
}
```
## 添加用户
在Controller层添加接口,写入添加用户的方法,之后在实现接口中添加这个方法接口。
```java
@RestController
@RequestMapping("/user")
@Tag(name = "用户请求接口")
public class UserController {
@Autowired
private UserService userService;
@Operation(summary = "添加用户")
@PostMapping("/add")
public void addUser(@RequestBody User user) {
userService.addUserDetails(user);
}
}
```
在实现接口中实现这个方法。
1. 注入`DBUserDetailsManager`之后创建这个user。
```java
import com.atguigu.security.config.DBUserDetailsManager;
import com.atguigu.security.entity.User;
import com.atguigu.security.mapper.UserMapper;
import com.atguigu.security.service.UserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl implements UserService {
@Autowired
private DBUserDetailsManager manager;
/**
* 添加用户
*
* @param user 用户信息
*/
@Override
public void addUserDetails(User user) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 这里的User使用的是springSecurity中的User
UserDetails userDetails = org.springframework.security.core.userdetails.User
.withDefaultPasswordEncoder()
.username(user.getUsername())
.password(user.getPassword())
.build();
manager.createUser(userDetails);
}
}
```
在方法中添加以下内容`createUser`这个方法
1. 导入了一些需要使用的类,包括`User`实体类和`UserMapper`接口。
2. 声明了一个`UserMapper`类型的字段`userMapper`。
3. 实现了`createUser`方法,该方法用于创建用户。在这个示例中,该方法将传入的`UserDetails`对象中的用户名和密码插入到数据库中。
4. 实现了`userExists`方法,该方法用于检查用户是否存在。在这个示例中,该方法始终返回`false`。
```java
import com.atguigu.security.entity.User;
import com.atguigu.security.mapper.UserMapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.provisioning.UserDetailsManager;
import java.util.ArrayList;
import java.util.Collection;
@Configuration
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
User user = userMapper.selectOne(queryWrapper);
if (user == null) {
throw new UsernameNotFoundException(username);
} else {
Collection authorities = new ArrayList<>();
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getEnabled(),
true, // 用户账号是否过期
true, // 用户凭证是否过期
true, // 用户是否未被锁定
authorities); // 权限列表
}
}
@Override
public void createUser(UserDetails userDetails) {
// 插入数据库方法
User user = new User();
user.setUsername(userDetails.getUsername());
user.setPassword(userDetails.getPassword());
user.setEnabled(true);
userMapper.insert(user);
}
@Override
public boolean userExists(String username) {
return false;
}
// 略...
}
```
> 为了方便调试,springSecurity默认开启了csrf(这要求请求参数中必须有一个隐藏的**_csrf**字段),为了测试这里就暂时关闭。
>
> 在filterChain方法中添加如下代码,关闭csrf攻击防御
>
> 代码示例:
>
```java
//关闭csrf攻击防御
http.csrf((csrf) -> {
csrf.disable();
});
```
```java
@Configuration
@EnableWebSecurity// Spring项目总需要添加此注解,SpringBoot项目中不需要
public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// authorizeRequests():开启授权保护
// anyRequest():对所有请求开启授权保护
// authenticated():已认证请求会自动被授权
httpSecurity
.authorizeRequests(authorize -> authorize
.anyRequest()
.authenticated())
.formLogin(withDefaults())// 表单授权方式
.httpBasic(withDefaults());// 基本授权方式
// 关闭csrf攻击
httpSecurity.csrf(AbstractHttpConfigurer::disable);
return httpSecurity.build();
}
}
```
## 密码加密测试
创建测试方法:
```java
@Slf4j
public class PasswordTest {
@Test
void testPassword() throws Exception {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String password = encoder.encode("password");
log.info("Password===>:{}", password);
// 密码校验
Assert.isTrue(encoder.matches("password", password), "密码不一致");
}
}
```
## 自定义登录页面
### 创建登录页

#### 第一步:创建Controller
```java
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping("/login")
public String login() {
return "login";
}
}
```
#### 第二步:创建HTML
```html
登录
登录
错误的用户名和密码.
```
#### 第三步:配置Security
```java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity// Spring项目总需要添加此注解,SpringBoot项目中不需要
public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// authorizeRequests():开启授权保护
// anyRequest():对所有请求开启授权保护
// authenticated():已认证请求会自动被授权
httpSecurity
.authorizeRequests(authorize -> authorize
.anyRequest()
.authenticated())
.formLogin(withDefaults())// 表单授权方式
.httpBasic(withDefaults());// 基本授权方式
// 关闭csrf攻击
httpSecurity.csrf(AbstractHttpConfigurer::disable);
// 配置SecurityFilterChain-自定义登录页
httpSecurity.formLogin(form -> {
form.loginPage("/login").permitAll()// 登录页面无需授权即可访问
.usernameParameter("username")// 自定义表单用户名参数,默认是username
.passwordParameter("password")// 自定义表单密码参数,默认是password
.failureUrl("/login?error"); // 登录失败的返回地址
});
return httpSecurity.build();
}
}
```

### 登录页的细节
在`WebSecurityConfig`中自定义前端传递值,默认传递用户名和密码为`username`和`password`,在下面示例中可以修改为自定义的用户名和密码参数。
```java
// 配置SecurityFilterChain-自定义登录页
httpSecurity.formLogin(form -> {
form.loginPage("/login").permitAll()// 登录页面无需授权即可访问
.usernameParameter("username")// 自定义表单用户名参数,默认是username
.passwordParameter("password")// 自定义表单密码参数,默认是password
.failureUrl("/login?error"); // 登录失败的返回地址
});
```
#### 自定义前端传递参数
```java
// 配置SecurityFilterChain-自定义登录页
httpSecurity.formLogin(form -> {
form.loginPage("自定义登录页").permitAll()// 登录页面无需授权即可访问
.usernameParameter("自定义用户名")// 自定义表单用户名参数,默认是username
.passwordParameter("自定义密码")// 自定义表单密码参数,默认是password
.failureUrl("自定义错误页"); // 登录失败的返回地址
});
```
## 认证响应结果
移入fastjson
```xml
com.alibaba.fastjson2
fastjson2
2.0.37
```
### 认证成功返回
```java
import com.alibaba.fastjson2.JSON;
import com.atguigu.security.result.Result;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import java.io.IOException;
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 获取用户身份信息
Object principal = authentication.getPrincipal();
// 获取用户凭证信息
// Object credentials = authentication.getCredentials();
// 获取用户权限信息
// Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
Result