本文通过逐步学习Spring Security,由浅入深,SpringBoot整合Spring Security 分别实现自定义的HTTP Basic认证和Form表单认证。
本文是学习笔记,网上的教程五花八门,由于时间久远,很难拿来就用。

主要内容:

  • 用户信息管理
  • 敏感信息加密解密
  • 用户认证
  • 权限控制
  • 跨站点请求伪造保护
  • 跨域支持
  • 全局安全方法
  • 单点登录

一、Spring Security 快速开始一个例子

创建SpringBoot项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ tree -I test
.
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── example
│ └── demo
│ ├── Application.java
│ └── controller
│ └── IndexController.java
└── resources
├── application.yml
├── static
└── templates

引入Spring Security依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

完整依赖 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
<?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.7.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

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

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

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

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>

配置 application.yml

1
2
server:
port: 8080

启动类 Application.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}

控制器 IndexController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class IndexController {
@GetMapping("/")
public String index(){
return "Hello";
}
}

直接访问应用会被重定向到登录页面

1
2
3
http://localhost:8080/
=> 302
http://localhost:8080/login

现在使用默认的账号密码登录

  • 默认的用户名:user
  • 默认的密码:(控制台打印出的密码)
1
Using generated security password: cdd28beb-9a64-4130-be58-6bde1684476d

再次访问 http://localhost:8080/

可以看到返回结果

看到上图说明成功集成Spring Security。

二、认证与授权说明

  • 认证authentication用户身份
  • 授权authorization用户权限

单体应用

微服务架构

三、Spring Security基础认证与表单认证

  1. 用户对象 UserDetails
  • 内存存储
  • 数据库存储
  1. 认证对象 Authentication
  • HTTP基础认证
  • HTTP表单认证

1、HTTP基础认证

通过HTTP请求头携带用户名和密码进行登录认证

HTTP请求头格式

1
2
# 用户名和密码的Base64编码
Authonrization: Basic Base64-encoded(username:password)

image.png

Spring Boot2.4版本以前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// 所有请求都需要认证,认证方式:httpBasic
http.authorizeHttpRequests((auth) -> {
auth.anyRequest().authenticated();
}).httpBasic(Customizer.withDefaults());
}
}

Spring Boot2.4版本之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfiguration {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

// 所有请求都需要认证,认证方式:httpBasic
http.authorizeHttpRequests((auth) -> {
auth.anyRequest().authenticated();
}).httpBasic(Customizer.withDefaults());

return http.build();
}
}

发送HTTP请求

1
2
GET http://localhost:8080/
Authorization: Basic dXNlcjo2ZjRhMGY5ZS1hY2ZkLTRmNTYtYjIzNy01MTZmYmZjMTk3NGM=

可以获得响应数据

1
Hello

base64解码之后可以得到用户名和密码

1
2
3
atob('dXNlcjo2ZjRhMGY5ZS1hY2ZkLTRmNTYtYjIzNy01MTZmYmZjMTk3NGM=')

'user:6f4a0f9e-acfd-4f56-b237-516fbfc1974c'

2、HTTP表单认证

Spring Security的默认认证方式

四、Spring Security 用户与认证对象说明

1、用户对象

UserDetails 用户对象接口说明

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 org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;

public interface UserDetails extends Serializable {
// 获取用户权限信息
Collection<? extends GrantedAuthority> getAuthorities();

// 获取密码
java.lang.String getPassword();

// 获取用户名
java.lang.String getUsername();

// 判断账户是否失效
boolean isAccountNonExpired();

// 判断账户是否锁定
boolean isAccountNonLocked();

// 判断账户凭证信息是否已失效
boolean isCredentialsNonExpired();

// 判断账户是否可用
boolean isEnabled();
}

GrantedAuthority 用户拥有权限接口说明

1
2
3
4
5
6
7
8
package org.springframework.security.core;

import java.io.Serializable;

public interface GrantedAuthority extends Serializable {
// 获取权限信息
String getAuthority();
}

UserDetailsService 用户查询操作说明

1
2
3
4
5
6
7
8
9
package org.springframework.security.core.userdetails;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

public interface UserDetailsService {
// 根据用户名获取用户信息
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetailsManager 用户CRUD操作说明

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
package org.springframework.security.provisioning;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;


public interface UserDetailsManager extends UserDetailsService {

// 创建用户
void createUser(UserDetails user);

// 更新用户
void updateUser(UserDetails user);

// 删除用户
void deleteUser(String username);

// 修改密码
void changePassword(String oldPassword, String newPassword);

// 判断用户是否存在
boolean userExists(String username);

}

2、认证对象

Authentication 认证请求详细信息

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
package org.springframework.security.core;

import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContextHolder;

public interface Authentication extends Principal, Serializable {

// 安全主体所具有的的权限
Collection<? extends GrantedAuthority> getAuthorities();

// 证明主体有效性的凭证
Object getCredentials();

// 认证请求的明细信息
Object getDetails();

// 主体的标识信息
Object getPrincipal();

// 是否认证通过
boolean isAuthenticated();

// 设置认证结果
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;

}

AuthenticationProvider 认证的业务执行者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.springframework.security.authentication;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

public interface AuthenticationProvider {

// 执行认证,返回认证结果
Authentication authenticate(Authentication authentication) throws AuthenticationException;

// 判断是否支持当前的认证对象
boolean supports(Class<?> authentication);
}

五、基于MySQL自定义认证过程例子

1、项目结构

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
$ tree -I target
.
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── com.example.springboot
│ │ ├── Application.java
│ │ ├── controller
│ │ │ └── IndexController.java
│ │ ├── entity
│ │ │ └── User.java
│ │ ├── mapper
│ │ │ └── UserMapper.java
│ │ ├── security
│ │ │ ├── SecurityConfiguration.java
│ │ │ └── UserAuthenticationProvider.java
│ │ └── service
│ │ ├── UserService.java
│ │ └── impl
│ │ └── UserServiceImpl.java
│ └── resources
│ ├── application.yml
│ ├── sql
│ │ └── schema.sql
│ ├── static
│ │ └── login.html
│ └── templates
└── test
└── java
└── com.example.springboot
└── ApplicationTests.java

2、用户表

默认表结构的SQL路径

spring-security-core-5.7.6.jar!/org/springframework/security/core/userdetails/jdbc/users.ddl

1
2
3
4
5
6
7
8
9
10
11
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(500) not null,
enabled boolean not null
);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);

一般情况下,我们使用自己创建的用户表

schema.sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 创建用户表
CREATE TABLE `tb_user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
`username` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
`password` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
`nickname` varchar(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '昵称',
`enabled` tinyint NOT NULL DEFAULT '1' COMMENT '账号可用标识',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表';

-- 添加初始数据
insert into `tb_user` values (1, "zhangsan", "zhangsan", "张三", 1);
insert into `tb_user` values (2, "lisi", "lisi", "李四", 1);
insert into `tb_user` values (3, "wangwu", "wangwu", "王五", 1);

3、依赖

  • Spring Security
  • MyBatis-Plus
  • MySQL8 JDBC
  • Lombok

完整依赖

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
<?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>3.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot</name>
<description>springboot学习</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!--starter-web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--spring-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>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 添加 JDBC Starter 以引入 HikariCP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
<!--MyBatisPlus核心库-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

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

</project>

4、数据库配置

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
server:
port: 8080

#数据库配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: zaq1xsw2

hikari:
maximum-pool-size: 10 # 最大连接数
minimum-idle: 5 # 最小空闲连接数
idle-timeout: 300000 # 空闲连接超时时间(毫秒)
connection-timeout: 20000 # 连接超时时间(毫秒)

#配置mybatis实体配置
mybatis:
mapper-locations:
- classpath:mapper/*.xml #映射到resources/mapper/User.xml里

#mybatis-plus相关配置
mybatis-plus:
# xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
mapper-locations: classpath:mapper/*.xml
# 以下配置均有默认值,可以不设置
global-config:
db-config:
#主键类型 AUTO:"数据库ID自增" INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: auto
#字段策略 IGNORED:"忽略判断" NOT_NULL:"非 NULL 判断") NOT_EMPTY:"非空判断"
field-strategy: NOT_EMPTY
#数据库类型
db-type: MYSQL
configuration:
# 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
map-underscore-to-camel-case: true
# 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段
call-setters-on-nulls: true
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

5、SpringBoot基本框架

启动类 Application.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

public class SpringbootApplication {

public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}

}

实体类 User.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
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
package com.example.springboot.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Arrays;
import java.util.Collection;

@Data
@TableName("tb_user")
public class User implements UserDetails {
/**
* 主键id
*/
@TableId
private Long id;

/**
* 用户名
*/
private String username;

/**
* 密码
*/
private String password;

/**
* 昵称
*/
private String nickname;

/**
* 账号可用标识
*/
private Integer enabled;

/**
* 获取用户权限信息
*
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
}

/**
* 判断账户是否失效
*
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}

/**
* 判断账户是否锁定
*
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}

/**
* 判断账户凭证信息是否已失效
*
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}

/**
* 判断账户是否可用
*
* @return
*/
@Override
public boolean isEnabled() {
return this.enabled == 1;
}
}

UserMapper.java

1
2
3
4
5
6
7
8
9
10
package com.example.springboot.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.springboot.entity.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

UserService.java

1
2
3
4
5
6
7
8
package com.example.springboot.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.springboot.entity.User;

public interface UserService extends IService<User> {
}

UserServiceImpl.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
33
34
35
36
37
38
39
40
package com.example.springboot.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.springboot.entity.User;
import com.example.springboot.mapper.UserMapper;
import com.example.springboot.service.UserService;
import lombok.extern.slf4j.Slf4j;
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
@Slf4j
public class UserServiceImpl
extends ServiceImpl<UserMapper, User>
implements UserService, UserDetailsService {

/**
* 根据用户名获取用户信息
* @param username
* @return UserDetails
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername, username);

User user = super.getOne(queryWrapper);

if(user == null){
log.error("Access Denied, user not found:" + username);
throw new UsernameNotFoundException("user not found:" + username);
}

return user;
}
}

IndexController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.springboot.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class IndexController {
@GetMapping("/hello")
public String hello(){
return "Hello";
}
}

6、自动定义Spring Security

SecurityConfiguration.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
package com.example.springboot.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfiguration {
/**
* 基于基础认证模式
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

// 所有请求都需要认证,认证方式:httpBasic
http.authorizeHttpRequests((auth) -> {
auth.anyRequest().authenticated();
}).httpBasic(Customizer.withDefaults());

return http.build();
}
}


UserAuthenticationProvider.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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.example.springboot.security;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class UserAuthenticationProvider implements AuthenticationProvider {

@Autowired
private UserDetailsService userService;

/**
* 自己实现认证过程
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 从Authentication 对象中获取用户名和密码
String username = authentication.getName();
String password = authentication.getCredentials().toString();

UserDetails user = userService.loadUserByUsername(username);

if (password.equals(user.getPassword())) {
// 密码匹配成功
log.info("Access Success: " + user);
return new UsernamePasswordAuthenticationToken(username, password, user.getAuthorities());
} else {
// 密码匹配失败
log.error("Access Denied: The username or password is wrong!");
throw new BadCredentialsException("The username or password is wrong!");
}
}

@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}

7、接口测试

IndexController.http

1
2
3
4
5
6
7
8
9
10
11
12
13
14
###
# 不提供认证信息
GET http://localhost:8080/hello

###
# 提供错误的认证信息
GET http://localhost:8080/hello
Authorization: Basic dXNlcjo2YzVlMTUyOS1kMTc2LTRkYjItYmZlMy0zZTIzOTNlMjY2MTk=

###
# 提供正确的认证信息
GET http://localhost:8080/hello
Authorization: Basic emhhbmdzYW46emhhbmdzYW4=
###

六、使用PasswordEncoder加密密码

PasswordEncoder接口说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.springframework.security.crypto.password;

public interface PasswordEncoder {

// 对原始密码编码
String encode(CharSequence rawPassword);

// 密码比对
boolean matches(CharSequence rawPassword, String encodedPassword);

// 判断加密密码是否需要再次加密
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}

常见的实现类

Bcrypt算法简介

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.sprintboot;


import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class BCryptPasswordEncoderTest {

@Test
public void encode(){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode("123456");
System.out.println(encode);
}
}

输出

1
$2a$10$lKqmIKbEPNDx/RXssgN6POgb8YssAK7pVtMFDosmC8FxozUgQq58K

解释

1
2
3
4
5
6
$是分隔符
2a表示Bcrypt算法版本
10表示算法强度
中间22位表示盐值
中间面的位数表示加密后的文本
总长度60位

使用Bcrypt算法加密密码后的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 建表
CREATE TABLE `tb_user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
`username` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
`password` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
`nickname` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '昵称',
`enabled` tinyint NOT NULL DEFAULT '1' COMMENT '账号可用标识',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表';

-- 数据
INSERT INTO `tb_user` VALUES (1, 'zhangsan', '$2a$10$/1XHgJYXtF4g/AiR41si8uvVC6Zc.Z9xVmXX4hO2z.b4.DX.H2j5W', '张三', 1);
INSERT INTO `tb_user` VALUES (2, 'lisi', '$2a$10$PEcF03ina7x9mmt2VbB0ueVkLZWQo/yoKOfvfQpoL09/faBlNuuZ.', '李四', 1);
INSERT INTO `tb_user` VALUES (3, 'wangwu', '$2a$10$PMumxkwwrELTbNDXCj0N4.jD/e/Hv.JiiZTFkdFqlDNLU2TahdYNq', '王五', 1);

UserAuthenticationProvider实现类替换如下

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
package com.example.springboot.security;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class UserAuthenticationProvider implements AuthenticationProvider {

@Autowired
private UserDetailsService userService;

// 密码加密
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

/**
* 自己实现认证过程
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 从Authentication 对象中获取用户名和密码
String username = authentication.getName();
String password = authentication.getCredentials().toString();

UserDetails user = userService.loadUserByUsername(username);

// 替换密码比对方式
// if (password.equals(user.getPassword())) {
if (this.passwordEncoder().matches(password, user.getPassword())) {
// 密码匹配成功
log.info("Access Success: " + user);
return new UsernamePasswordAuthenticationToken(username, password, user.getAuthorities());
} else {
// 密码匹配失败
log.error("Access Denied: The username or password is wrong!");
throw new BadCredentialsException("The username or password is wrong!");
}
}

@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}

七、Session会话控制

改为基于基础认证模式
修改配置类SecurityConfiguration

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.example.springboot.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfiguration {
/**
* 基于基础认证模式
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

// 禁用session会话
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

// 所有请求都需要认证,认证方式:httpBasic
http.authorizeHttpRequests((auth) -> {
auth.anyRequest().authenticated();
}).httpBasic(Customizer.withDefaults());

return http.build();
}
}

八、基于表单模式实现自定义认证

SecurityFormConfiguration 配置类

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
package com.example.springboot.security;

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.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityFormConfiguration {
/**
* 基于表单认证模式
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

// 启用session会话
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);

// 认证方式:Form
http.authorizeRequests()
//.requestMatchers("/login.html").permitAll() // 放行登录页面
// 所有请求都需要认证
.anyRequest().authenticated()
.and()
// 启动表单认证模式
.formLogin()
// 登录页面
.loginPage("/login.html")
// 请求提交地址
.loginProcessingUrl("/login")
// 成功跳转页面
.defaultSuccessUrl("/hello", true)
// 放行上面的两个地址
.permitAll()
// 设置提交的参数名
.usernameParameter("username")
.passwordParameter("password")
.and()
// 开始设置注销功能
.logout()
// 注销的url
.logoutUrl("/logout")
// session直接过期
.invalidateHttpSession(true)
// 清除认证信息
.clearAuthentication(true)
// 注销成功后跳转地址
.logoutSuccessUrl("/login.html")
.and()
// 禁用csrf安全防护
.csrf().disable();

return http.build();
}
}

登录页面 static/login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>

<h2>Login</h2>

<form action="/login" method="post">
<div><label>username:<input type="text" name="username"></label></div>
<div><label>password:<input type="password" name="password"></label></div>
<div><input type="submit"></div>
</form>
</body>
</html>

显示效果