---
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)

项目结构

![](./images/1739884981997-1f74d00b-4ba8-4716-927c-91b26aee6d16.png)

#### 基本包
这里用到了数据库但是在项目刚开始启动时，是没有配置数据库的，这时候启动肯定会报错，所以我们现在启动类上排出连接数据库的类。

```xml
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
    </parent>
    <groupId>com.atguigu</groupId>
    <artifactId>security-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>security-demo</name>
    <url>https://maven.apache.org</url>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity6</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.4.1</version>
            <exclusions>
                <exclusion>
                    <groupId>org.mybatis</groupId>
                    <artifactId>mybatis-spring</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.3</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

```

#### 创建启动类
在启动类上排出数据库的类。

```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
<html lang="zh" xmlns:th="https://www.thymeleaf.org">
<head>
    <title>Hello Security!</title>
</head>
<body>
<h1>Hello Security</h1>
<!--通过使用@{/logout}，Thymeleaf将自动处理生成正确的URL，以适应当前的上下文路径。
这样，无论应用程序部署在哪个上下文路径下，生成的URL都能正确地指向注销功能。-->
<a th:href="@{/logout}">Log Out</a>
</body>
</html>

```

比如点击下面按钮会自动匹配路径并退出。

![](./images/1739884994974-19080b87-0be5-4d8b-a2b4-e57e996da90b.png)

## 自定义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
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.30</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.4.1</version>
    <exclusions>
        <exclusion>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>3.0.3</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

```

#### 配置文件中
```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<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        User user = userMapper.selectOne(queryWrapper);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        } else {
            Collection<GrantedAuthority> 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<UserMapper, User> 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<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        User user = userMapper.selectOne(queryWrapper);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        } else {
            Collection<GrantedAuthority> 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), "密码不一致");
    }
}
```

## 自定义登录页面
### 创建登录页
![](./images/1739885018440-f282a25f-0223-4c1f-9ae9-fa67b4f6dfa1.png)

#### 第一步：创建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
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
    <title>登录</title>
</head>
<body>
<h1>登录</h1>
<div th:if="${param.error}">
    错误的用户名和密码.
</div>
<!--method必须为"post"-->
<!--th:action="@{/login}" ，
使用动态参数，表单中会自动生成_csrf隐藏字段，用于防止csrf攻击
login: 和登录页面保持一致即可，SpringSecurity自动进行登录认证-->
<form method="post" th:action="@{/login}">
    <div>
        <!--name必须为"username"-->
        <input name="username" placeholder="用户名" type="text"/>
    </div>
    <div>
        <!--name必须为"password"-->
        <input name="password" placeholder="密码" type="password"/>
    </div>
    <input type="submit" value="登录"/>
</form>
</body>
</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();
    }
}
```

![](./images/1739885055597-43b7107f-a1b2-462a-996b-fd3b4d66ccb0.png)

### 登录页的细节
在`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
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.37</version>
</dependency>

```

### 认证成功返回
```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<Object> result = Result.success(principal);

        // 返回
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(JSON.toJSON(result));
    }
}
```

![](./images/1739885100104-b414e402-cf91-4713-8789-9570a53721c5.png)

在`WebSecurityConfig`类中添加以下`.successHandler(new MyAuthenticationSuccessHandler());`表示成功的返回结果，自定义结果。

如果想看Result的类翻到最下面附录。

```java
// 配置SecurityFilterChain-自定义登录页
httpSecurity.formLogin(form -> {
    form.loginPage("/login").permitAll()// 登录页面无需授权即可访问
            .usernameParameter("username")// 自定义表单用户名参数，默认是username
            .passwordParameter("password")// 自定义表单密码参数，默认是password
            .failureUrl("/login?error") // 登录失败的返回地址
            .successHandler(new MyAuthenticationSuccessHandler());// 认证成功时的处理
});
```

### 认证失败返回
和成功的返回相似，只需要修改两个地方即可。

认证失败的类

```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.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import java.io.IOException;

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        // 错误消息
        String localizedMessage = exception.getLocalizedMessage();
        Result<String> result = Result.error(localizedMessage);

        // 转成JSON
        Object json = JSON.toJSON(result);

        // 返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}
```

在WebSecurityConfig类中添加`failureHandler`即可`.failureHandler(new MyAuthenticationFailureHandler());`

```java
// 配置SecurityFilterChain-自定义登录页
httpSecurity.formLogin(form -> {
    form.loginPage("/login").permitAll()// 登录页面无需授权即可访问
            .usernameParameter("username")// 自定义表单用户名参数，默认是username
            .passwordParameter("password")// 自定义表单密码参数，默认是password
            .failureUrl("/login?error") // 登录失败的返回地址
            .successHandler(new MyAuthenticationSuccessHandler())// 认证成功时的处理
            .failureHandler(new MyAuthenticationFailureHandler());// 认证失败的处理
});
```

![](./images/1739885069146-d3cae399-23b5-447a-a025-fea765c1d8be.png)

### 注销响应
和前面成功和失败过程相似，只需要在`from`中再添加即可。

```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.logout.LogoutSuccessHandler;

import java.io.IOException;

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 创建结果对象
        Result<Object> result = Result.success();

        // 转为JSON
        Object json = JSON.toJSON(result);

        // 返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}
```

在`WebSecurityConfig`类中添加以下。

```java
// 注销响应
httpSecurity.logout(logout -> {
    logout.logoutSuccessHandler(new MyLogoutSuccessHandler());// 注销成功时的处理
});
```

![](./images/1739885117054-f7b847c2-7948-485f-bbc4-b53da35671dc.png)

之后访问：[http://localhost/demo](http://localhost/demo)

### 请求未认证接口
```java
import com.alibaba.fastjson2.JSON;
import com.atguigu.security.result.Result;
import com.atguigu.security.result.ResultCodeEnum;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import java.io.IOException;

/**
 * 请求未认证接口
 */
@Slf4j
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        // 错误信息
        String localizedMessage = authException.getLocalizedMessage();
        log.error("请求未认证接口：{}", localizedMessage);

        // 创建结果对象
        Result<Object> result = Result.error(ResultCodeEnum.FAIL_NEED_LOGIN);

        // 返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(JSON.toJSON(result));
    }
}
```

在`WebSecurityConfig`类中添加以下。

```java
// 请求未认证接口
httpSecurity.exceptionHandling(exception -> {
    // 请求未认证的接口
    exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());
});
```

访问：[http://localhost/demo/](http://localhost/demo/)

![](./images/1739885133278-ef71eb9a-48e4-423a-96b5-ccfe68b07a8f.png)

登录后

![](./images/1739885137700-67858269-4190-4596-a4be-7227280220f1.png)

![](./images/1739885142319-b86e0961-1eef-4ef9-b42a-f7c9d9335ff7.png)

### 跨域访问
在`WebSecurityConfig`类中添加以下。

```java
// 跨域访问权限
httpSecurity.cors(withDefaults());
```

## 获取用户认证信息
USerVo类见附录。

```java
@RestController
public class IndexController {
    @GetMapping("/")
    public Result<UserVo> index() {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();

        // 用户名
        String username = authentication.getName();
        // 身份
        Object principal = authentication.getPrincipal();
        // 凭证(脱敏)
        Object credentials = authentication.getCredentials();
        // 权限
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

        // 整理返回参数
        UserVo userVo = UserVo.builder().authorities(authorities).credentials(credentials)
                .principal(principal).username(username).build();

        return Result.success(userVo);
    }
}
```

## 会话并发处理
后登录的账号会使先登录的账号失效。实现方式和之前的差不多也是实现一个接口。

```java
import com.alibaba.fastjson2.JSON;
import com.atguigu.security.result.Result;
import com.atguigu.security.result.ResultCodeEnum;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;

import java.io.IOException;

public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        // 创建结果对象
        Result<Object> result = Result.error(ResultCodeEnum.LOGGED_IN_FROM_ANOTHER_DEVICE);

        // 转为JSON
        Object json = JSON.toJSON(result);

        // 返回响应
        HttpServletResponse response = event.getResponse();
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}
```

在`WebSecurityConfig`类中添加以下。

```java
// 后登录的账号会使先登录的账号失效
httpSecurity.sessionManagement(session -> {
    session.maximumSessions(1)
            .expiredSessionStrategy(new MySessionInformationExpiredStrategy());
});
```

## 授权访问
授权管理的实现在SpringSecurity中非常灵活，可以帮助应用程序实现以下两种常见的授权需求：

+ 用户-权限-资源：例如张三的权限是添加用户、查看用户列表，李四的权限是查看用户列表
+ 用户-角色-权限-资源：例如 张三是角色是管理员、李四的角色是普通用户，管理员能做所有操作，普通用户只能查看信息

### 基于request的授权
**需求：**

+ 具有USER_LIST权限的用户可以访问/user/list接口
+ 具有USER_ADD权限的用户可以访问/user/add接口

在`WebSecurityConfig`添加下面内容。

```java
// authorizeRequests()：开启授权保护
httpSecurity.authorizeRequests(authorize -> {
    // 具有USER_LIST权限的用户可以访问/user/list，访问路径是Controller中的路径
    authorize.requestMatchers("/user/list").hasAuthority("USER_LIST")
            // 具有USER_ADD权限的用户可以访问/user/add
            .requestMatchers("/user/add").hasAuthority("USER_ADD")
            // 对所有请求开启授权保护
            .anyRequest()
            // 已认证请求会自动被授权
            .authenticated();
});
```

之后在`DBUserDetailsManager`中授予访问权限。

在其中注释`authorities.add(() -> "USER_LIST")`会发现没有权限访问。

```java
// 授予访问权限
authorities.add(() -> "USER_LIST");
authorities.add(() -> "USER_ADD");
```

完整代码

```java
import com.atguigu.security.mapper.UserMapper;
import com.atguigu.security.model.entity.User;
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<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        User user = userMapper.selectOne(queryWrapper);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        } else {
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            // 授予访问权限
            authorities.add(() -> "USER_LIST");
            authorities.add(() -> "USER_ADD");
            return new org.springframework.security.core.userdetails.User(
                    user.getUsername(),
                    user.getPassword(),
                    user.getEnabled(),
                    true, // 用户账号是否过期
                    true, // 用户凭证是否过期
                    true, // 用户是否未被锁定
                    authorities); // 权限列表
        }
    }
}
```

请求未授权的接口

创建未授权访问类，返回对象。

```java
import com.alibaba.fastjson2.JSON;
import com.atguigu.security.result.Result;
import com.atguigu.security.result.ResultCodeEnum;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import java.io.IOException;

public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Result<Object> result = Result.error(ResultCodeEnum.FAIL_NO_ACCESS_DENIED);

        Object json = JSON.toJSON(result);

        // 返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}
```

在`WebSecurityConfig`添加下面内容。

```java
// 请求未授权的接口
httpSecurity.exceptionHandling(exception -> {
    exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());
    // 没有权限访问
    exception.accessDeniedHandler(new MyAccessDeniedHandler());
});
```

**更多的例子：**[Method Security :: Spring Security](https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html)

#### 用户-角色-资源
**需求：**角色为ADMIN的用户才可以访问/user/**路径下的资源

##### 配置角色
在`filterChain`方法中配置；将之前的手动添加的路径注释。

`hasRole("ADMIN")`可以自定义内容。

```java
// authorizeRequests()：开启授权保护
httpSecurity.authorizeRequests(authorize -> {
    // 具有USER_LIST权限的用户可以访问/user/list，访问路径是Controller中的路径
    authorize
            // 具有管理员角色的用户可以访问/user/**
            .requestMatchers("/user/**").hasRole("ADMIN")
            // .requestMatchers("/user/list").hasAuthority("USER_LIST")
            // 具有USER_ADD权限的用户可以访问/user/add
            // .requestMatchers("/user/add").hasAuthority("USER_ADD")
            // 对所有请求开启授权保护
            .anyRequest()
            // 已认证请求会自动被授权
            .authenticated();
});
```

配置DBUserDetailsManager，改变`roles("USER")`

+ 如果为`roles("ADMIN")`表示都可以访问
+ 如果为`roles("USER")`表示只是普通用户。

```java
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("username", username);
    User user = userMapper.selectOne(queryWrapper);
    if (user == null) {
        throw new UsernameNotFoundException(username);
    } else {
        // Collection<GrantedAuthority> authorities = new ArrayList<>();
        // // 授予访问权限
        // authorities.add(() -> "USER_LIST");
        // authorities.add(() -> "USER_ADD");
        // return new org.springframework.security.core.userdetails.User(
        //         user.getUsername(),
        //         user.getPassword(),
        //         user.getEnabled(),
        //         true, // 用户账号是否过期
        //         true, // 用户凭证是否过期
        //         true, // 用户是否未被锁定
        //         authorities); // 权限列表

        return org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword())
                .disabled(!user.getEnabled())
                .credentialsExpired(false)
                .accountLocked(false)
                // SecurityFilterChain中配置hasRole("ADMIN")来判断的
                .roles("USER")
                .build();
    }
}
```

为`roles("ADMIN")`时全部可以访问。

![](./images/1739885159429-1ba4d90f-d7c2-4d48-9da1-2c11a9252233.png)

为普通用户时。

![](./images/1739885163072-25c78a6a-56dc-46cf-9fc7-cbc36b2fe155.png)



### RBAC设置权限表方式
RBAC（Role-Based Access Control，基于角色的访问控制）是一种常用的数据库设计方案，它将用户的权限分配和管理与角色相关联。以下是一个基本的RBAC数据库设计方案的示例：

1. 用户表（User table）：包含用户的基本信息，例如用户名、密码和其他身份验证信息。

| 列名 | 数据类型 | 描述 |
| --- | --- | --- |
| user_id | int | 用户ID |
| username | varchar | 用户名 |
| password | varchar | 密码 |
| email | varchar | 电子邮件地址 |
| ... | ... | ... |


2. 角色表（Role table）：存储所有可能的角色及其描述。

| 列名 | 数据类型 | 描述 |
| --- | --- | --- |
| role_id | int | 角色ID |
| role_name | varchar | 角色名称 |
| description | varchar | 角色描述 |
| ... | ... | ... |


3. 权限表（Permission table）：定义系统中所有可能的权限。

| 列名 | 数据类型 | 描述 |
| --- | --- | --- |
| permission_id | int | 权限ID |
| permission_name | varchar | 权限名称 |
| description | varchar | 权限描述 |
| ... | ... | ... |


4. 用户角色关联表（User-Role table）：将用户与角色关联起来。

| 列名 | 数据类型 | 描述 |
| --- | --- | --- |
| user_role_id | int | 用户角色关联ID |
| user_id | int | 用户ID |
| role_id | int | 角色ID |
| ... | ... | ... |


5. 角色权限关联表（Role-Permission table）：将角色与权限关联起来。

| 列名 | 数据类型 | 描述 |
| --- | --- | --- |
| role_permission_id | int | 角色权限关联ID |
| role_id | int | 角色ID |
| permission_id | int | 权限ID |
| ... | ... | ... |


在这个设计方案中，用户可以被分配一个或多个角色，而每个角色又可以具有一个或多个权限。通过对用户角色关联和角色权限关联表进行操作，可以实现灵活的权限管理和访问控制。

当用户尝试访问系统资源时，系统可以根据用户的角色和权限决定是否允许访问。这样的设计方案使得权限管理更加简单和可维护，因为只需调整角色和权限的分配即可，而不需要针对每个用户进行单独的设置。

```sql
CREATE TABLE admin_power (
 id INT PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID',
 power_name VARCHAR(50) NOT NULL COMMENT '权限名称',
 power_code VARCHAR(255) NOT NULL COMMENT '权限编码',
 description VARCHAR(100) COMMENT '描述',
 create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 update_user VARCHAR(50) COMMENT '更新用户',
 is_delete TINYINT(1) DEFAULT 0 COMMENT '是否删除，0-未删除，1-已删除'
);

CREATE TABLE admin_user_role (
 id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID',
 user_id VARCHAR(50) NOT NULL COMMENT '用户id',
 role_id VARCHAR(255) NOT NULL COMMENT '角色id',
 description VARCHAR(100) COMMENT '描述',
 create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 update_user VARCHAR(50) COMMENT '更新用户',
 is_delete TINYINT(1) DEFAULT 0 COMMENT '是否删除，0-未删除，1-已删除'
);

CREATE TABLE admin_role_power (
 id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID',
 role_id VARCHAR(50) NOT NULL COMMENT '角色id',
 power_id VARCHAR(255) NOT NULL COMMENT '权限id',
 description VARCHAR(100) COMMENT '描述',
 create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 update_user VARCHAR(50) COMMENT '更新用户',
 is_delete TINYINT(1) DEFAULT 0 COMMENT '是否删除，0-未删除，1-已删除'
);

```

### 基于方法的授权
#### 开启方法授权
在配置文件中添加如下注解，或者在启动类上添加都可以。

+ 默认如果开启了方法授权访问同时也配置了全局角色为`roles("ADMIN")`那么所有接口都是可以访问的。

```java
@EnableMethodSecurity
```

#### 给用户授予角色和权限
DBUserDetailsManager中的loadUserByUsername方法，添加`authorities("USER_ADD", "USER_UPDATE")`

+ `roles("ADMIN")`：与authorities不能同时使用

```java
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("username", username);
    User user = userMapper.selectOne(queryWrapper);
    if (user == null) {
        throw new UsernameNotFoundException(username);
    } else {
        // Collection<GrantedAuthority> authorities = new ArrayList<>();
        // // 授予访问权限
        // authorities.add(() -> "USER_LIST");
        // authorities.add(() -> "USER_ADD");
        // return new org.springframework.security.core.userdetails.User(
        //         user.getUsername(),
        //         user.getPassword(),
        //         user.getEnabled(),
        //         true, // 用户账号是否过期
        //         true, // 用户凭证是否过期
        //         true, // 用户是否未被锁定
        //         authorities); // 权限列表

        return org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword())
                .disabled(!user.getEnabled())
                .credentialsExpired(false)
                .accountLocked(false)
                // SecurityFilterChain中配置hasRole("ADMIN")来判断的
                // .roles("ADMIN")// 与authorities不能同时使用
                // 给用户授予角色和权限
                .authorities("USER_ADD", "USER_UPDATE")
                .build();
    }
}
```

#### 常用授权注解
+ `hasAnyRole('ADMIN')`当前角色权限为ADMIN同时访问用户也要为`authentication.name == 'admin'`

```java
@RestController
@RequestMapping("/user")
@Tag(name = "用户请求接口")
public class UserController {
    @Autowired
    private UserService userService;

    @Operation(summary = "查询所有用户")
    // @PreAuthorize("hasAnyRole('ADMIN')")
    @PreAuthorize("hasAnyRole('ADMIN') and authentication.name == 'admin'")// 编写逻辑表达式
    @GetMapping("/list")
    public List<User> getList() {
        return userService.list();
    }

    // 用户必须有 USER_ADD 权限 才能访问此方法
    @Operation(summary = "添加用户")
    @PreAuthorize("hasAuthority('USER_ADD')")
    @PostMapping("/add")
    public void addUser(@RequestBody User user) {
        userService.addUserDetails(user);
    }
}
```

> 如果当前用户不是admin用户访问不了。
>

![](./images/1739885175291-ac74528f-137c-4021-bd8d-4d925ff98d82.png)

