SpringSecurity从入门到精通
0. 简介
- Spring Security和Shiro比较。中大型的项目都是使用SpringSecurity做安全框架,小项目使用Shiro比较多,因为它比Spring Security上手更加简单
- 认证与授权:
- 认证:验证当前访问系统的是不是本系统的用户,并确认具体是那个用户
- 授权:经过认证后判断当前用户是否具有权限进行某个操作
1. 快速入门
1.1 准备工作
- 创建SpringBoot项目,导入相关的依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
1.2 引入Spring Security
- 导入Spring Security依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 导入Security依赖之后,再次访问后端请求时会默认打开一个登录页面
- 此处默认的用户名(Username)是
user
,密码(Password)会在后台打印出来一长串字符,登录后就能够正常访问请求 - Security默认的登出网址
/logout
2. 认证
2.1 登录校验流程
2.2 原理
SpringSecurity原理是一个过滤器链,内部包含了提供各种功能的过滤器。
UsernamePasswordAuthenticationFilter
:负责处理我们在登录页面填写的用户名密码后的登录请求ExceptionTranslationFilter
:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationExceptionFilterSecurityInterceptor
:负责权限校验的过滤器
认证流程详解:
- 提交用户名密码
- 封装Authentication对象,只有用户名密码,没有权限
- 调用authenticate方法进行认证
- 调用DaoAuthenticationProvider的authenticate方法进行认证
- 调用loadUserByUsername方法查询用户
- 根据用户名查询用户及对应的权限信息
- 把对应的用户信息包括权限信息封装成UserDetail对象
- 通过PasswordEncoder对比UserDetails中的密码和Authentication的密码是否一致
- 如果正确就把UserDetails中的权限信息设置到Authentication对象中
- 如果上一步返回了Authentication对象就是用SecurityContextHolder.getContext().setAuthentication()方法存储该对象,其他过滤器会通过SecurityContextHolder获取当前用户信息
2.3 准备工作
- 登录:
- 自定义登录接口,调用ProviderManager的方法进行认证,认证通过把用户信息存入redis
- 自定义UserDetailsService,在这个实现类中查询数据库
- 校验:
- 定义jwt认证过滤器,获取token,解析token,获取其中的userid,从redis获取用户信息
- 将用户信息存入SecurityContextHolder
- RedisJson序列化配置:
- RedisConfig相关配置信息
- JWT配置工具类
- Redis工具类
- Web响应工具类
- 导入MybatisPlus相关依赖,在mysql数据库中创建出对应的表结构,导入junit进行测试
2.4 解决问题
- 自定义UserDetailsServiceImpl替换InMemoryuserDetailsManager实现类,自定义
loadUserByUsername
的具体方法实现
- 自定义LoginUser替换UserDetails实现类。主要替换其中的权限控制
getAuthorities
、用户名和密码获取的方法
- 密码加密存储方法,默认使用的PasswordEncoder要求数据库密码格式为
{id}password
,他会根据id判断密码的加密方式。我们一般使用SpringSecurity提供的BCryptPasswordEncoder,可以自定义一个Security配置类,继承WebSecurityConfigurerAdapter - BCryptPasswordEncoder中的加密过程
- PasswordEncoder加密方式测试
- jwt使用
- SecurityConfig配置相关信息
- Login服务验证用户信息
- 认证过滤器
- 在SecurityConfig配置中,加入之前定义的认证过滤器
void configure(HttpSecurity http) throws Exception{
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
- 注销登录(删除redis中缓存的用户登录信息)
3. 授权
3.1 授权基本流程
- 权限系统作用:
- 例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书的功能,不可以看到并使用添加书籍信息、删除书籍信息等功能。如果是一个图书馆管理员帐号登录了,就能看到并使用添加书籍信息,删除书籍信息功能。
- 不同的用户可以使用不同的功能。
- 不能只依赖前端去判断用户的权限来选择显示哪些菜单按钮,如果有人知道了对应功能的接口地址也可以不通过前端,直接去发送请求实现相关功能,“前端防君子,后端防小人”。
- 授权基本流程
- SpringSecurity默认使用FilterSecurityInterceptor进行权限验证,其慧聪SecurityContext获取Authentication,然后获取其中的权限信息,当前用户是否拥有访问当前资源所需的权限。
- 项目中需要把当前登录用户的权限信息也存入Authentication
- 设置我们的资源所需要的权限即可
3.2 授权实现
- 权限信息:RBAC权限模型
- Mapper与SQL的编写:
- 后端请求方法权限定义
- 查询用户权限信息并封装进Authentication中
4、自定义失败处理
-
希望在认证失败或授权失败的情况下能和我们的接口一样返回相同结构的json,就可以让前端对响应进行统一的处理,Security帮忙实现了这一功能。
-
认证或授权过程中的异常会被ExceptionTranslationFilter捕获到,在ExceptionTranslationFilter判断是认证异常还是授权失败;
- 认证异常会被封装然后调用AuthenticationEntryPoint对象的方法进行异常处理
- 授权失败会被封装然后调用AccessDeniedHandler对象的方法进行异常处理
-
所有自定义异常处理,只需要自己实现AuthenticationEntryPoint和AccessDeniedHandler然后配置给Security即可
-
定义认证异常处理类
-
配置认证异常、权限异常处理器
-
当出现认证异常、权限异常时的响应结果就会变成全局定义的处理器
5、跨域问题
- 跨域出现原因:浏览器同源策略,要求源相同才能正常使用通信,包括协议、域名、端口号一致
- SpringBoot后端请求需要配置跨域策略,Security需要再配置一次跨域
- SpringBoot跨域(config/CorsConfig.java)
- SpringSecurity跨域(config/SecurityConfig.java)
6、其他权限校验方法
hasAuthority()
方法是即使执行SecurityExpressionRoot的hashAuthority方法hasAnyAuthority()
方法可以传入多个权限,有其中任意一个权限即可访问资源hasRole()
要求有对应的角色才可以访问,其中的权限字段前边会拼接一个 "ROLE_",上边两个不会拼接字符串
7、自定义权限校验方法
- 自定义方法
- 具体应用细节(在SPEL表达式中使用@ex相当于获取容器中的bean名字为ex的对象,再调用对应方法)
8、基于配置的权限控制
- 在SecurityConfig配置类中编辑方法,添加想要配置的方法以及对应的权限信息
9、CSRF攻击
- 所谓CSRF,指跨站请求伪造,是web常见的攻击之一
- SpringSecurity防止CSRF攻击的方式是通过csrf_token,后端生成一个csrf_token,前端发起请求的时候携带这个csrf_token,后端会有过滤器进行校验,没有携带或者是伪造的就不允许访问
- Security中已经自定义了一个token来验证身份,不需要一个额外的csrf_token来防止攻击,所以配置中
http.csrf().disable()
进行了关闭
SQL语句编写
-- user表建立
CREATE TABLE `sys_user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
`password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` CHAR(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
`email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱',
`phonenumber` VARCHAR(32) DEFAULT NULL COMMENT '手机号',
`sex` CHAR(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` VARCHAR(128) DEFAULT NULL COMMENT '头像',
`user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
`create_by` BIGINT(20) DEFAULT NULL COMMENT '创建人的用户id',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`del_flag` INT(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'
-- menu表建立
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
`path` varchar(200) DEFAULT NULL COMMENT '路由地址',
`component` varchar(255) DEFAULT NULL COMMENT '组件路径',
`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
`create_by` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint(20) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`del_flag` int(11) DEFAULT 0 COMMENT '是否删除(0未删除 1已删除)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE = InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT = '菜单表';
-- role表创建
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL,
`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
`status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
`del_flag` int(1) DEFAULT 0 COMMENT 'del_flag',
`create_by` bigint(200) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint(200) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE = InnoDB AUTO_INCREMENT = 3 DEFAULT CHARSET=utf8mb4 COMMENT = '角色表';
-- role_menu表建立
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`role_id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`menu_id` bigint NOT NULL DEFAULT 0 COMMENT '菜单id',
PRIMARY KEY (`role_id`, `menu_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 2 DEFAULT CHARSET=utf8mb4;
-- user_role表建立
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`role_id` bigint(200) NOT NULL DEFAULT 0 COMMENT '角色id',
PRIMARY KEY (`user_id`, `role_id`)
) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)