SpringSecurity快速入门

1
2
3
4
5
6
作者: 夜泊1990
企鹅: 1611756908
Q 群: 948233848
邮箱: hd1611756908@163.com
博客: https://hs-an-yue.github.io/
B 站: https://space.bilibili.com/514155929/

第一章 SpringSecurity概述

1
2
3
4
5
6
保护JavaWeb网站安全的框架

环境需求:
JDK1.8
SpringBoot2.7.6 版本下的 SpringSecurity版本为 5.7.5
阿里云脚手架

第二章 SpringSecurity结构(内部原理)

1
了解 SpringSecurity 框架,需要了解JavaWeb的基础知识(Listener、Filter、Servlet) 否则学习 SpringSecurity回很麻烦,根本看不懂。

第1节 JavaWeb 内部结构

1
2
3
上图是Javaweb的运行原理图,客户端(浏览器或者其他APP)发送请求,先经过多个过滤器,然后最后交给Servlet进行处理

如果看不懂这个就不要往下看了 The End

第2节 SpringSecurity 内部结构

1
SpringSecurity的核心其实就是Filter   下面看一下它的设计演化
1
2
3
官网地址: https://docs.spring.io/spring-security/reference/5.8/servlet/architecture.html

官网中介绍了 SecurityFilterChain 是SpringSecurity的默认过滤器,这个过滤器提供了拦截规则以及登录逻辑,后面详细介绍

第三章 SpringSecurity内置案例

1
官方内置了一套简单的验证逻辑,自带登录页面(登录功能)和注销以及内置账户和密码,方便开发者快速入门,本章主要介绍内置案例

第1节 环境准备

1
2
1. 创建SpringBoot项目,添加依赖
SpringBoot版本采用的 2.7.6 Java开发环境基于JDK1.8,如果想使用更高版本,具体请查看官网
1
2. 项目创建完成后,删除无用的代码
1
2
3. 项目创建完成后,添加测试代码 HelloController.java  运行项目
通过浏览器 http://127.0.0.1:8080/test1或者test2或者test3 查看效果
1
4. 运行原理图,根据原理图了解他的执行原理

第2节 内置案例详细介绍

1
2
3
4
5
6
7
8
官网: https://docs.spring.io/spring-security/reference/5.8/servlet/authentication/passwords/in-memory.html
SpringSecurity内置案例核心API

InMemoryUserDetailsManager: 内置账户信息管理类,是UserDetailsService的子类
UserDetailsService: SpringSecurity用户信息管理类的核心接口,管理用户信息来源(数据库还是内存以及其他...)
UserDetails: SpringSecurity封装用户信息的核心接口,给SpringSecurity送用户信息时SpringSecurity只认UserDetails

以上的API是内置认证的简单API

第3节 修改内置的用户名和密码

1
2
3
4
5
6
7
8
9
官网: https://docs.spring.io/spring-security/reference/5.8/servlet/authentication/passwords/in-memory.html
方式一: API方式(略)

方式二: 配置文件方式
spring:
security:
user:
name: admin
password: 123

第四章 替换系统自带用户名和密码的获取方式

第1节 认证的运行原理

第2节 替换默认生成的用户信息

2.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
34
-- 当前流行的权限控制系统 RBAC 模式,所以数据库表设计基于 RBAC

-- 创建数据库
CREATE DATABASE rbac DEFAULT CHARACTER SET utf8;

-- 用户表
CREATE TABLE user(
user_id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID主键',
phone VARCHAR(50) NOT NULL UNIQUE COMMENT '手机号,唯一',
password VARCHAR(255) NOT NULL COMMENT '密码',
username VARCHAR(100) NOT NULL COMMENT '用户名'
)AUTO_INCREMENT=1000 DEFAULT charset=utf8 COMMENT='用户表';
-- 角色表
CREATE TABLE role(
role_id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID主键',
role_name VARCHAR(100) NOT NULL COMMENT '角色名'
)AUTO_INCREMENT=1000 DEFAULT charset=utf8 COMMENT='角色表';
-- 权限表
CREATE TABLE permission(
permission_id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID主键',
permission_name VARCHAR(100) NOT NULL COMMENT '权限名'
)AUTO_INCREMENT=1000 DEFAULT charset=utf8 COMMENT='权限表';
-- 用户角色关联表
CREATE TABLE user_role(
user_id BIGINT NOT NULL COMMENT '用户ID',
role_id BIGINT NOT NULL COMMENT '角色ID',
PRIMARY KEY (user_id,role_id)
) DEFAULT charset=utf8 COMMENT='用户角色关联表';
-- 角色权限关联表
CREATE TABLE role_permission(
role_id BIGINT NOT NULL COMMENT '角色ID',
permission_id BIGINT NOT NULL COMMENT '权限ID',
PRIMARY KEY (role_id,permission_id)
) DEFAULT charset=utf8 COMMENT='角色权限关联表';

2.2 添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- ORM框架 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<!-- 省略GET/SET等工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

2.3 代码实现

第一步: 配置文件(配置数据库,配置mybatis)

1
2
3
4
5
6
7
8
9
10
11
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/rbac?characterEncoding=utf-8
username: root
password: root
mybatis:
mapper-locations: mapper/*.xml # 映射接口的xml文件
type-aliases-package: com.hs.entity # 类型别名
configuration:
map-underscore-to-camel-case: true # 驼峰命名

第二步: 映射数据库表的实体类

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
/**
* 数据库用户表 - user
*/
@Data
public class User {
/**
* 用户ID
*/
private Long userId;
/**
* 手机号
*/
private String phone;
/**
* 密码
*/
private String password;
/**
* 用户名
*/
private String username;
}

/**
* 数据库角色表 - role
*/
@Data
public class Role {
/**
* 角色ID
*/
private Long roleId;
/**
* 角色名称
*/
private String roleName;
}

/**
* 数据库权限表 - permission
*/
@Data
public class Permission {
/**
* 权限ID
*/
private Long permissionId;
/**
* 权限名称
*/
private String permissionName;
}

/**
* 数据库用户角色关联表 - user_role
*/
@Data
public class UserRole {
/**
* 用户ID
*/
private Long userId;
/**
* 角色ID
*/
private Long roleId;
}

/**
* 数据库角色权限关联表 - role_permission
*/
@Data
public class RolePermission {
/**
* 角色ID
*/
private Long roleId;
/**
* 权限ID
*/
private Long permissionId;
}

第三步: mapper层实现使用用户名获取用户信息的函数

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 用户表操作,使用手机号用来代替用户登录账号
*/
@Mapper
public interface UserMapper {
/**
* 通过手机号获取用户信息
* @param phone 手机号
* @return 用户信息
*/
User getUserByPhone(String phone);
}

第四步: UserDetails接口实现,封装用户信息(给SpringSecurity送数据)

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
/**
* 封装用户信息
* SpringSecurity规定给他传递的用户信息必须是UserDetails接口的子类实例对象进行封装
*/
@Data
public class LoginUserDetails implements UserDetails {
private User user;
public LoginUserDetails() {
}
public LoginUserDetails(User user) {
this.user = user;
}
/**
* 暂时只实现认证,不实现授权,所以这边权限给空集合
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return new ArrayList<>();
}
//装配密码
@Override
public String getPassword() {
return user.getPassword();
}
//装配账户,这里用手机号作为登录账号
@Override
public String getUsername() {
return user.getPhone();
}
//账号是否过期,在数据库中没有设置,给默认值不过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//账号是否锁定,在数据库中没有设置,给默认值不锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
//密码是否过期,在数据库中没有设置,给默认值不过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//账号是否可用,在数据库中没有设置,给默认值可用
@Override
public boolean isEnabled() {
return true;
}
}

第五步: 替换 InMemoryUserDetailsManager 实现 UserDetailsService方法

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
/**
* UserDetailsService是Spring Security提供的从数据库获取数据的核心接口
* 实现 UserDetailsService 重写里面的 loadUserByUsername方法,替换默认从内存中获取用户信息
* 具体loadUserByUsername方法中的逻辑可以参考InMemoryUserDetailsManager实现
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//通过用户名从数据库中查询用户信息(这个用户名从前端传递过来时可以使用手机号,邮箱或者其他用户账号)
User user = userMapper.getUserByPhone(username);
//判断当前账号是否存在
if(Objects.isNull(user)){
//如果为空,直接抛出异常
throw new UsernameNotFoundException(username);
}
/*
* 不为空说明数据库中存在,将信息送到SpringSecurity上下文中
* 参考InMemoryUserDetailsManager类中的loadUserByUsername方法逻辑
*/
return new LoginUserDetails(user);
}
}

第五步: 在数据库user表中添加测试账户

第六步: 验证基于数据库的登录是否生效替换了默认用户名和密码的形式

1
2
3
4
5
6
7
8
验证是否生效
第一步: 启动项目
第二步: 在浏览器中输入 http://127.0.0.1:8080/test1 服务会自动跳转到SpringSecurity的内置登录页面
第三步: 数据在数据库中自己添加的用户信息
第四步: 查看现象
注意: 肯定会失败,因为密码,因为密码现在在数据库中是明文的,需要设置明文规则 如果在密码前添加{noop},可以不需要进行加密

修改密码后,浏览器中继续测试,就会成功

第七步: 如果想数据库中的密码是密文,可以使用 BCryptPasswordEncoder 进行加密和解密

1
2
1. 创建配置类
2. 将 BCryptPasswordEncoder 加入到IOC容器中,SpringSecurity 自动生效
1
编写测试代码,将明文密码加密成密文,保存到数据库中,然后在继续使用浏览器测试.
1
在浏览器中输入 http://127.0.0.1:8080/test1 会跳转到登录页,在登录页输入 用户名/密码  18233333333/123456 看测试结果

第五章 替换页面登录为前后端分离方式

1
2
3
4
5
6
7
前后端分离架构是当前最流行的软件架构模式

下面需要修改SpringSecurity默认的逻辑,改成成前后端分离的架构模式。
1. 替换掉登陆页面
2. 修改从内置登录页面获取用户名和密码的逻辑
3. 修改内部默认跳转登录页面的逻辑
4. 修改登录失败的逻辑

第0节 前期准备

1
2
在设计前后端分离的认证前,需要针对当前项目做一些约束
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
34
35
36
37
38
39
40
41
42
/**
* 统一返回值
*/
@Setter
@Getter
public class Result {
/**
* 响应状态码
*/
private int code=200;
/**
* 响应提示消息
*/
private String msg="成功";
/**
* 响应结果
*/
private Object data;

public static Result ok(){
return new Result();
}
public static Result ok(Object data){
return new Result(data);
}
public static Result error(String msg){
return new Result(msg);
}
public Result() {
}
public Result(Object data) {
this.data = data;
}
public Result(String msg) {
this.code = -1;
this.msg = msg;
}
public Result(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
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
//将前面测试 HelloController.java中的返回值进行统一修改

/**
* 测试控制器
* 验证SpringSecurity是否生效
*/
@RestController
public class HelloController {
//测试方法1
@GetMapping(value = "/test1")
public Result test1(){
System.out.println("test1...");
return Result.ok("test1...");
}
//测试方法2
@GetMapping(value = "/test2")
public Result test2(){
System.out.println("test2...");
return Result.ok("test2...");
}
//测试方法3
@GetMapping(value = "/test3")
public Result test3(){
System.out.println("test3...");
return Result.ok("test3...");
}
}

第1节 替换内置登录验证逻辑

1.1 登录验证流程图

1
此流程图是基于前后端不分离的表单流程图,和前后端分离的流程基本上一模一样,后面实现前后端分离的登录,参考此流程

1.2 登录核心API介绍

1
2
3
4
5
6
7
8
9
10
11
根据登录流程图介绍登录相关API

SecurityFilterChain: SpringSecurity核心过滤器,SpringSecurity默认会自动创建一个此对象,用来支持自带的登录,现在采用前后端分离,登录逻辑发生变化,所以需要我们自己创建SpringSecurity来覆盖默认的。

UsernamePasswordAuthenticationToken: 封装前端页面传递过来的用户名和密码,封装好后通过AuthenticationManager传递给SpringSecurity上下文

AuthenticationManager: SpringSecurity的认证管理器,见名知意,用来进行认证

Authentication: 认证实例,认证成功后,里面封装认证成功后的信息

流程图总的其它API暂时使用不上,不做介绍

1.3 登录逻辑代码实现

配置认证管理器实例

1
2
3
4
5
6
7
/**
* SpringSecurity 认证管理器
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

配置 SecurityFilterChain

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* SpringSecurity过滤器
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable() //防止跨站请求伪造
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //取消session
.and()
.authorizeRequests()
.antMatchers("/login").permitAll() //登陆和未登录的人都可以访问访问
.anyRequest().authenticated()//除了上面设置的地址可以匿名访问,其它所有的请求地址需要认证访问
return http.build();
}

登录逻辑实现

1
2
3
官网案例: 
https://docs.spring.io/spring-security/reference/5.8/servlet/authentication/architecture.html
https://docs.spring.io/spring-security/reference/5.8/servlet/authentication/passwords/index.html
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
@RestController
public class LoginController {
@Resource
private AuthenticationManager authenticationManager;
/**
* 登录
* @param phone 手机号
* @param password 密码
* @return 响应结果
*/
@PostMapping(value = "/login")
public Result login(String phone,String password){
/*
* SpringSecurity 的认证逻辑
* 默认情况SpringSecurity内置了登录页面,内置了从页面获取数据,并将其数据送到SpringSecurity上下文的方式
* 当前前后端分离的逻辑,数据不再从页面获取,所以不能再使用内置逻辑,需要程序员自己实现将数据送到SpringSecurity上下文中
* 官网地址: https://docs.spring.io/spring-security/reference/5.8/servlet/authentication/passwords/form.html
*/
//封装用户名(手机号作为用户名)和密码
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(phone,password);
//调用认证管理中的认证方法,调用后可能出现异常,所以需要try...catch
try {
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果认证成功Authentication中就会有用户信息,否则为空
if(Objects.isNull(authenticate)){
//认证失败
return Result.error("认证失败,用户名或密码错误");
}
}catch (RuntimeException e){
e.printStackTrace();
//认证失败
return Result.error("认证失败,用户名或密码错误");
}
return Result.ok();
}
}
1
代码完成后进行测试,因为没有前端页面所以需要一个客户端工具进行测试,我这里采用postman进行测试
1
2
3
4
上面是调用登录接口成功和失败的样式

还有另外一种情况,在未登录的情况下,访问了其它资源 例如: http://127.0.0.1:8080/test1
注意: 有人会有疑问,说我前面已经登陆了为什么还不能访问,那是因为前后端分离项目不在是Session-Cookie机制
1
未登录访问其它资源怎么处理呢?下面介绍 ↓↓↓↓↓↓↓↓

第2节 修改未登录访问其它资源的逻辑处理

1
2
3
4
5
6
7
8
9
10
实现此功能需要的步骤如下:
第一步: 添加依赖(json工具依赖)
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.32</version>
</dependency>
第二步: 实现AuthenticationEntryPoint接口,实现自定义的处理器
第三步: 将自定义的处理器注册到Spring Security的核心Filter中
第四步: 测试是否生效

第二步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 在未认证或者认证错误的情况下访问需要认证的资源时的处理类
*/
@Component //加入到IOC容器
public class LoginUnAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
/*
* 当访问一个需要认证的资源时因为当前用户没有认证或者认证失败,直接访问资源会交给此函数进行处理
* 因为架构是前后端分离的项目,所以给客户端的提示保持和控制器的返回值格式相同
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
Result result = Result.error("用户未认证或登录已过期,请重新登录后再访问");
//将消息json化
String json = JSONUtil.toJsonStr(result);
//送到客户端
response.getWriter().print(json);
}
}

第三步

1
将自定义的处理器注册到Spring Security的核心过滤器中

第四步

第3节 解决HTTP协议无状态

1
2
3
4
5
传统前后端不分离的架构采用Session+Cookie机制

现在前后端分离架构,采用token令牌方式

token生成采用JWT工具生成和校验,使用Redis数据库进行保存

3.1 Redis数据库安装和启动

1

3.2 JWT工具类编写

添加依赖

1
2
3
4
5
6
7
8
9
10
11
<!-- JWT工具类 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<!-- redis客户端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

工具类代码实现

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
/**
* JWT工具类
*/
public class JwtUtils {
private static Algorithm hmac256 = Algorithm.HMAC256("YLWTSMTJFYHDCMGSCWHSSYBZSDKC");
/**
* 生成token
* @param pub 负载
* @param expiresTime 过期时间(单位 毫秒)
* @return token
*/
public static String sign(String pub, Long expiresTime){
return JWT.create() //生成令牌函数
.withIssuer(pub) //自定义负载部分,其实就是添加Claim(jwt结构中的payload部分),可以通过源码查看
.withExpiresAt(new Date(System.currentTimeMillis()+expiresTime)) //添加过期时间
.sign(hmac256);
}
/**
* 校验token
*/
public static boolean verify(String token){
JWTVerifier verifier = JWT.require(hmac256).build();
//如果正确,直接代码向下执行,如果错误,抛异常
verifier.verify(token);
return true;
}
/**
* 从token中获取负载
* @param token 令牌
* @return 保存的负载
*/
public static String getClaim(String token){
DecodedJWT jwt = JWT.decode(token);
Claim iss = jwt.getClaim("iss");
return iss.asString();
}
}

3.3 Redis客户端实现

配置文件修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/rbac?characterEncoding=utf-8
username: root
password: root
redis:
host: 127.0.0.1 # redis服务器地址
password: # redis服务器密码,我这里没有设置密码
database: 0 # redis的库,我这里用0号库
mybatis:
mapper-locations: mapper/*.xml # 映射接口的xml文件
type-aliases-package: com.hs.entity # 类型别名
configuration:
map-underscore-to-camel-case: true # 驼峰命名

代码实现

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
/**
* Redis客户端
*/
@Component
public class RedisClient {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 保存数据
*/
public void set (String key,String value){
stringRedisTemplate.opsForValue().set(key,value);
}
/**
* 保存数据-过期时间
* @param key 键
* @param value 值
* @param time 过期时间,单位是 毫秒
*/
public void set (String key,String value,Long time){
stringRedisTemplate.opsForValue().set(key,value,time, TimeUnit.MILLISECONDS);
}
/**
* 通过键获取对应的值
* @param key 键
* @return
*/
public String get(String key){
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 通过键删除对应的值
* @param key 键
*/
public void del(String key){
stringRedisTemplate.delete(key);
}
/**
* 判断key是否存在
*/
public Boolean exists(String key){
return stringRedisTemplate.hasKey(key);
}
}

3.4 登录逻辑修改

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
@RestController
public class LoginController {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisClient redisClient;
/**
* 登录
* @param phone 手机号
* @param password 密码
* @return 响应结果
*/
@PostMapping(value = "/login")
public Result login(String phone,String password){
/*
* SpringSecurity 的认证逻辑
* 默认情况SpringSecurity内置了登录页面,内置了从页面获取数据,并将其数据送到SpringSecurity上下文的方式
* 当前前后端分离的逻辑,数据不再从页面获取,所以不能再使用内置逻辑,需要程序员自己实现将数据送到SpringSecurity上下文中
* 官网地址: https://docs.spring.io/spring-security/reference/5.8/servlet/authentication/passwords/form.html
*/
//封装用户名(手机号作为用户名)和密码
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(phone,password);
//调用认证管理中的认证方法,调用后可能出现异常,所以需要try...catch
try {
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果认证成功Authentication中就会有用户信息,否则为空
if(Objects.isNull(authenticate)){
//认证失败
return Result.error("认证失败,用户名或密码错误");
}
//登录成功将用户信息保存到redis中,以token作为key
LoginUserDetails principal = (LoginUserDetails) authenticate.getPrincipal();
if(Objects.isNull(principal)){
return Result.error("认证失败,用户名或密码错误");
}
//将用户信息json化
String json = JSONUtil.toJsonStr(principal);
//使用token作为redis的key 格式为 login:token
String token = JwtUtils.sign(principal.getUsername(), 1000 * 60 * 60 * 24 * 7L);//过期时间为7天
//将用户信息json化后保存到redis中
redisClient.set("login:token:"+token,json,1000*60 * 60 * 24 * 7L); //过期时间7天
Map<String,Object> map = new HashMap<>();
map.put("token",token);
return Result.ok(map);
}catch (RuntimeException e){
e.printStackTrace();
//认证失败
return Result.error("认证失败,用户名或密码错误");
}
}
}

3.5 其它资源访问基于token令牌方式

1
使用令牌方式,替换前后端不分离的session-cookie机制,解决HTTP无状态的问题

SpringSecurity运行原理

1
2
3
4
5
6
简单了解一下SpringSecurity的功能实现 
SpringSecurity的核心是Filter(过滤器),核心功能实现也是由一个个过滤器组成,详情请查看官网

官网:https://docs.spring.io/spring-security/reference/5.8/servlet/architecture.html#servlet-security-filters

下面是SpringSecurity内置的过滤器
1
2
3
4
简单画了一个SpringSecurity原理图,客戶端发送过来的请求会经过一个个的过滤器,每一个过滤器承担着不同的功能
例如UsernamePasswordAuthenticationFilter过滤器帮助我们校验账户和密码

现在我们要模拟session+cookie机制,通过token凭据实现权限控制,方式如下

基于token机制的实现

1
2
第一步: 自定义 Filter
第二步: 注册到SpringSecurity的Filter

第一步

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
/**
* 自定义过滤器,实现token令牌的判断
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisClient redisClient;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取请求头
String token = request.getHeader("token");
if(StringUtils.hasLength(token)){
//redis中获取用户信息
String key = "login:token:"+token;
String json = redisClient.get(key);
if(StringUtils.hasLength(json)){
//反序列化
LoginUserDetails user = JSONUtil.toBean(json, LoginUserDetails.class);
if(Objects.nonNull(user)){
//封装用户信息,送到下一个过滤器 UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
//将Redis数据库中的信息送到SpringSecurity上下文中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}else {
SecurityContextHolder.getContext().setAuthentication(null);
}
}
}
//放行,后面交给Spring Security 框架
filterChain.doFilter(request,response);
}
}

第二步

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
/**
* Spring Security配置
*/
@Configuration
public class SecurityConfig {

@Resource
private LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler;
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/*
* 密码加密和解密工具
*/
@Bean
public PasswordEncoder generalPasswordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* SpringSecurity过滤器
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable() //防止跨站请求伪造
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //取消session
.and()
.authorizeRequests()
.antMatchers("/login").permitAll() //登陆和未登录的人都可以访问访问
.anyRequest().authenticated();//除了上面设置的地址可以匿名访问,其它所有的请求地址需要认证访问
//将自定义的过滤器注册到SpringSecurity过滤器链中,并且设置到UsernamePasswordAuthenticationFilter前面
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//注册自定义的处理器(未认证用户访问需要认证资源的处理器)
http.exceptionHandling().authenticationEntryPoint(loginUnAuthenticationEntryPointHandler);
return http.build();
}
/**
* SpringSecurity 认证管理器
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}

测试验证

3.6 反复登录,多token解决

1
2
3
客户端发送的所有请求都需要带token,在进行登录时单独进行token的校验,如果登陆过,刷新token

在登录中添加一个校验逻辑,删除原来的key
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
@RestController
public class LoginController {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisClient redisClient;
/**
* 登录
* @param phone 手机号
* @param password 密码
* @return 响应结果
*/
@PostMapping(value = "/login")
public Result login(String phone, String password, HttpServletRequest request){
/*
* 登录前判断是否上次的登录未过期,如果未过期直接删除,重新登录生成新token
*/
String token_ = request.getHeader("token");
//判断是否存在
if(StringUtils.hasText(token_)){
String claim = JwtUtils.getClaim(token_);
if(StringUtils.hasText(claim)){
//校验是否是同一个账户
if(phone.equals(claim)){
//同一个账户,删除原来的登录状态
String key="login:token:"+token_;
redisClient.del(key);
}
}
}

/*
* SpringSecurity 的认证逻辑
* 默认情况SpringSecurity内置了登录页面,内置了从页面获取数据,并将其数据送到SpringSecurity上下文的方式
* 当前前后端分离的逻辑,数据不再从页面获取,所以不能再使用内置逻辑,需要程序员自己实现将数据送到SpringSecurity上下文中
* 官网地址: https://docs.spring.io/spring-security/reference/5.8/servlet/authentication/passwords/form.html
*/
//封装用户名(手机号作为用户名)和密码
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(phone,password);
//调用认证管理中的认证方法,调用后可能出现异常,所以需要try...catch
try {
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果认证成功Authentication中就会有用户信息,否则为空
if(Objects.isNull(authenticate)){
//认证失败
return Result.error("认证失败,用户名或密码错误");
}
//登录成功将用户信息保存到redis中,以token作为key
LoginUserDetails principal = (LoginUserDetails) authenticate.getPrincipal();
if(Objects.isNull(principal)){
return Result.error("认证失败,用户名或密码错误");
}
//将用户信息json化
String json = JSONUtil.toJsonStr(principal);
//使用token作为redis的key 格式为 login:token
String token = JwtUtils.sign(principal.getUsername(), 1000 * 60 * 60 * 24 * 7L);//过期时间为7天
//将用户信息json化后保存到redis中
redisClient.set("login:token:"+token,json,1000*60 * 60 * 24 * 7L); //过期时间7天
Map<String,Object> map = new HashMap<>();
map.put("token",token);
return Result.ok(map);
}catch (RuntimeException e){
e.printStackTrace();
//认证失败
return Result.error("认证失败,用户名或密码错误");
}
}
}

第六章 授权

1
授权的意义就是当一个用户登录成功后,此账户本身拥有的权限(例如 当前用户用哪些角色,当前角色都能干什么[删除|更新等])

第1节 将权限从数据库送到SpringSecurity上下文

1
2
3
4
5
6
7
8
9
10
11
从数据库中将用户信息送到上下文 在 UserDetailsService接口的实现类中

实现步骤:
第一步: mapper提供查询方法,将用户相关的权限信息查询到封装到UserDetails的对象中
通过用户ID查询角色名称列表
通过角色ID查询权限名称列表
第二步: 在UserDetailsService中进行封装
在UserDetailsService中调用mapper并且封装传递给 SpringSecurity 上下文,修改UserDetails结构添加属性等
第三步: 在控制器层进行注解控制(配置文件)
第四步: 开启注解配置否则不生效
第五步: 权限不够的处理器

1.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 用户角色关联表
*/
@Mapper
public interface UserRoleMapper {
/**
* 通过用户ID查询用户角色列表
* @param userId 用户ID
* @return 用户角色列表
*/
List<UserRole> getUserRolesByUserId(Long userId);
}

/**
* 操作数据库角色表
*/
@Mapper
public interface RoleMapper {
/**
* 批量查询
* @param roleIds 角色ID列表
* @return 角色列表
*/
List<Role> batchGetRolesByRoleIds(List<Long> roleIds);
}

/**
* 操作数据库角色权限表
*/
@Mapper
public interface RolePermissionMapper {

/**
* 通过角色ID列表查询权限角色权限列表
* @param roleIds 角色IDs
* @return 角色权限列表
*/
List<RolePermission> getRolePermissionsByRoleIds(List<Long> roleIds);
}

/**
* 操作数据库权限表
*/
@Mapper
public interface PermissionMapper {
/**
* 通过权限ID列表查询权限列表
* @param permissionIds 权限ID列表
* @return 权限列表
*/
List<Permission> batchGetPermissionsByPermissionIds(List<Long> permissionIds);
}

1.2 第二步

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
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
/**
* 封装用户信息
* SpringSecurity规定给他传递的用户信息必须是UserDetails接口的子类实例对象进行封装
*/
@Data
public class LoginUserDetails implements UserDetails {

private User user;

//角色名称列表,用于授权
private List<String> roleNames;
//权限名称列表,用户授权
private List<String> permissionNames;

public LoginUserDetails(User user, List<String> roleNames, List<String> permissionNames) {
this.user = user;
this.roleNames = roleNames;
this.permissionNames = permissionNames;
}

/**
* 暂时只实现认证,不实现授权,所以这边权限给空集合
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
//添加角色
if(!CollectionUtils.isEmpty(roleNames)){
//将角色设置到GrantedAuthority中,官网要求角色要加上前缀 ROLE_xxx 区分其它权限
for (String roleName : roleNames) {
authorities.add(new SimpleGrantedAuthority("ROLE_"+roleName));
}
}
//添加权限
if(!CollectionUtils.isEmpty(permissionNames)){
//将权限设置到GrantedAuthority中
for (String permissionName : permissionNames) {
authorities.add(new SimpleGrantedAuthority(permissionName));
}
}
return authorities;
}
//装配密码
@Override
public String getPassword() {
return user.getPassword();
}
//装配账户,这里用手机号作为登录账号
@Override
public String getUsername() {
return user.getPhone();
}
//账号是否过期,在数据库中没有设置,给默认值不过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//账号是否锁定,在数据库中没有设置,给默认值不锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
//密码是否过期,在数据库中没有设置,给默认值不过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//账号是否可用,在数据库中没有设置,给默认值可用
@Override
public boolean isEnabled() {
return true;
}
}

UserDetailsService修改

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
/**
* UserDetailsService是Spring Security提供的从数据库获取数据的核心接口
* 实现 UserDetailsService 重写里面的 loadUserByUsername方法,替换默认从内存中获取用户信息
* 具体loadUserByUsername方法中的逻辑可以参考InMemoryUserDetailsManager实现
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Resource
private UserRoleMapper userRoleMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private RolePermissionMapper rolePermissionMapper;
@Resource
private PermissionMapper permissionMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//通过用户名从数据库中查询用户信息(这个用户名从前端传递过来时可以使用手机号,邮箱或者其他用户账号)
User user = userMapper.getUserByPhone(username);
//判断当前账号是否存在
if(Objects.isNull(user)){
//如果为空,直接抛出异常
throw new UsernameNotFoundException(username);
}
/*
* 不为空说明数据库中存在,将信息送到SpringSecurity上下文中
* 参考InMemoryUserDetailsManager类中的loadUserByUsername方法逻辑
*/
List<UserRole> userRoles = userRoleMapper.getUserRolesByUserId(user.getUserId());
//角色名称列表
List<String> roleNames = new ArrayList<>();
//权限名称列表
List<String> permissionNames = new ArrayList<>();
if(!CollectionUtils.isEmpty(userRoles)){
List<Long> roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toList());
if(!CollectionUtils.isEmpty(roleIds)){
//查询角色信息
List<Role> roles = roleMapper.batchGetRolesByRoleIds(roleIds);
if(!CollectionUtils.isEmpty(roles)){
List<String> roleNameList = roles.stream().map(Role::getRoleName).collect(Collectors.toList());
roleNames.addAll(roleNameList);
}
//查询权限
List<RolePermission> rolePermissions = rolePermissionMapper.getRolePermissionsByRoleIds(roleIds);
if(!CollectionUtils.isEmpty(rolePermissions)){
List<Long> permissionIdList = rolePermissions.stream().map(RolePermission::getPermissionId).collect(Collectors.toList());
if(!CollectionUtils.isEmpty(permissionIdList)){
List<Permission> permissions = permissionMapper.batchGetPermissionsByPermissionIds(permissionIdList);
if(!CollectionUtils.isEmpty(permissions)){
List<String> permissionNameList = permissions.stream().map(Permission::getPermissionName).collect(Collectors.toList());
if(!CollectionUtils.isEmpty(permissionNameList)){
permissionNames.addAll(permissionNameList);
}
}
}
}
}
}
return new LoginUserDetails(user,roleNames,permissionNames);
}
}

1.3 第三步

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
// 在配置文件上添加注解,开启注解校验
//官网地址: https://docs.spring.io/spring-security/reference/5.8/servlet/authorization/method-security.html

/**
* 测试控制器
* 验证SpringSecurity是否生效
*/
@RestController
public class HelloController {
/**
* 测试方法1 -- 游客访问 -- 不登录也可以访问
*/
@GetMapping(value = "/test1")
public Result test1(){
System.out.println("test1...");
return Result.ok("test1...");
}

/**
* 测试方法2 -- 登录后才可以访问
*/
@GetMapping(value = "/test2")
public Result test2(){
System.out.println("test2...");
return Result.ok("test2...");
}

/**
* 测试方法3 -- 登录后,具有admin角色才能访问
*/
@PreAuthorize(value = "hasRole('admin')")
@GetMapping(value = "/test3")
public Result test3(){
System.out.println("test3...");
return Result.ok("test3...");
}

/**
* 测试方法4 -- 登录后,具有admin或者CEO角色才能访问
*/
@PreAuthorize(value = "hasAnyRole('admin','CEO')")
@GetMapping(value = "/test4")
public Result test4(){
System.out.println("test4...");
return Result.ok("test4...");
}

/**
* 测试方法5 -- 登录后,同时具有CTO和CFO角色才能访问
*/
@PreAuthorize(value = "hasRole('CTO') and hasRole('CEO')")
@GetMapping(value = "/test5")
public Result test5(){
System.out.println("test5...");
return Result.ok("test5...");
}
/**
* 测试方法6 -- 登录后,具有 user:add权限可以访问
*/
@PreAuthorize(value = "hasAuthority('user:add')")
@GetMapping(value = "/test6")
public Result test6(){
System.out.println("test6...");
return Result.ok("test6...");
}

/**
* 测试方法7 -- 登录后,具有 user:add 或者 user:del权限可以访问
*/
@PreAuthorize(value = "hasAnyAuthority('user:add','user:del')")
@GetMapping(value = "/test7")
public Result test7(){
System.out.println("test7...");
return Result.ok("test7...");
}

/**
* 测试方法8 -- 登录后,具有 user:add 和 user:del权限可以访问
*/
@PreAuthorize(value = "hasAuthority('user:add') and hasAuthority('user:del')")
@GetMapping(value = "/test8")
public Result test8(){
System.out.println("test8...");
return Result.ok("test8...");
}
}

1.4 第四步

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
/**
* Spring Security配置
*/
@Configuration
@EnableMethodSecurity(securedEnabled = true) //开发方法权限验证
public class SecurityConfig {

@Resource
private LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler;
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Resource
private LoginUnAccessDeniedHandler loginUnAccessDeniedHandler;
/*
* 密码加密和解密工具
*/
@Bean
public PasswordEncoder generalPasswordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* SpringSecurity过滤器
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable() //防止跨站请求伪造
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //取消session
.and()
.authorizeRequests()
.antMatchers("/login","/test1").permitAll() //登陆和未登录的人都可以访问访问
.anyRequest().authenticated();//除了上面设置的地址可以匿名访问,其它所有的请求地址需要认证访问
//将自定义的过滤器注册到SpringSecurity过滤器链中,并且设置到UsernamePasswordAuthenticationFilter前面
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//注册自定义的处理器(未认证用户访问需要认证资源的处理器)
http.exceptionHandling().authenticationEntryPoint(loginUnAuthenticationEntryPointHandler);
//注册自定义的处理器(认证后的用户访问需要认证资源时因为权限不足走的处理器)
http.exceptionHandling().accessDeniedHandler(loginUnAccessDeniedHandler);
return http.build();
}
/**
* SpringSecurity 认证管理器
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}

1.5 第五步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 如果登录成功,使用当前用户进行访问,发现权限不够,报权限错误,定义处理器进行处理

/**
* 权限不足处理器
* 用户登录成功,访问某一个资源时因为权限不足,报异常
*/
@Component
public class LoginUnAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
Result result = Result.error("权限不足,请重新授权。");
//将消息json化
String json = JSONUtil.toJsonStr(result);
//送到客户端
response.getWriter().print(json);
}
}

//自定义权限不足处理器后,需要进行注册,注册到SecurityConfig配置文件中

第七章 注销

1
2
注销比较简单,直接将redis数据清空了即可
SpringSecurity内置注销功能,咱们使用内置的注销,覆盖注销的逻辑即可
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
/**
* 注销成功的处理器
*/
@Component
public class LogoutStatusSuccessHandler implements LogoutSuccessHandler {
@Resource
private RedisClient redisClient;

@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String token = request.getHeader("token");
if(StringUtils.hasText(token)){
redisClient.del("login:token:"+token);
}
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
Result result = Result.ok(200,"注销成功");
//将消息json化
String json = JSONUtil.toJsonStr(result);
//送到客户端
response.getWriter().print(json);
}
}


/**
* Spring Security配置
*/
@Configuration
@EnableMethodSecurity(securedEnabled = true) //开发方法权限验证
public class SecurityConfig {

@Resource
private LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler;
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Resource
private LoginUnAccessDeniedHandler loginUnAccessDeniedHandler;
@Resource
private LogoutStatusSuccessHandler logoutStatusSuccessHandler;

/*
* 密码加密和解密工具
*/
@Bean
public PasswordEncoder generalPasswordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* SpringSecurity过滤器
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable() //防止跨站请求伪造
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //取消session
.and()
.authorizeRequests()
.antMatchers("/login","/logout","/test1").permitAll() //登陆和未登录的人都可以访问访问
.anyRequest().authenticated();//除了上面设置的地址可以匿名访问,其它所有的请求地址需要认证访问
//将自定义的过滤器注册到SpringSecurity过滤器链中,并且设置到UsernamePasswordAuthenticationFilter前面
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//注册自定义的处理器(未认证用户访问需要认证资源的处理器)
http.exceptionHandling().authenticationEntryPoint(loginUnAuthenticationEntryPointHandler);
//注册自定义的处理器(认证后的用户访问需要认证资源时因为权限不足走的处理器)
http.exceptionHandling().accessDeniedHandler(loginUnAccessDeniedHandler);
//注册自定义处理器(注销处理器,注销成功后删除redis中的数据)
http.logout().logoutSuccessHandler(logoutStatusSuccessHandler);
return http.build();
}
/**
* SpringSecurity 认证管理器
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}

第八章 总结

1
以上就是SpringSecurity的核心知识点,没什么好总结的,一切从官网出发. 你学废了吗 !!!!
--------------------------已经到底啦!--------------------------