SpringSecurity通常用于B端用户的认证和授权。比如在后端管理界面,不同的管理员有不同的权限。 认证解决的是登录-你是谁的问题; 授权解决的是权限-你能做什么的问题。
SringSecurity使用
搭建springboot工程,引入security的包,编写一个controller,启动项目。 访问controller时会有个会被security拦截,跳转默认登录页面。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.0</version>
</dependency>
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/demo")
public class SecurityController {
@GetMapping("/hello")
public String hello() {
return "hello security!";
}
}
下面使用security来做一个完整的项目,先做好准备工作:
1、: 准备一个自己项目的前端界面和资源
2:添加pom依赖:
<?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.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>sringboot_demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sringboot_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>
<!--自带的log4j依赖有漏洞,需要使用log4j2-->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--log4j2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</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>
<!--添加thymeleaf依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--添加lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--添加mp 依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<!--添加mysql 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.21</version>
</dependency>
<!--添加redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--基于redis实现session的依赖 -->
<!-- <dependency>-->
<!-- <groupId>org.springframework.session</groupId>-->
<!-- <artifactId>spring-session-data-redis</artifactId>-->
<!-- </dependency>-->
<!--添加thymeleaf为SpringSecurity提供的标签 依赖 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.0</version>
</plugin>
</plugins>
</build>
</project>
3:建立数据库表:
/*权限表*/
DROP TABLE IF EXISTS `t_permission`; CREATE TABLE `t_permission`(`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号', `permission_name` varchar(30) CHARACTER
SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限名称', `permission_tag` varchar(30) CHARACTER
SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限标签', `permission_url` varchar(100) CHARACTER
SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限地址', PRIMARY KEY(`ID`) USING BTREE) ENGINE= InnoDB AUTO_INCREMENT= 9 CHARACTER
SET= utf8 COLLATE= utf8_general_ci ROW_FORMAT= Compact;
/*角色表*/
DROP TABLE IF EXISTS `t_role`; CREATE TABLE `t_role`(`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号', `ROLE_NAME` varchar(30) CHARACTER
SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名称', `ROLE_DESC` varchar(60) CHARACTER
SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色描述', PRIMARY KEY(`ID`) USING BTREE) ENGINE= InnoDB AUTO_INCREMENT= 6 CHARACTER
SET= utf8 COLLATE= utf8_general_ci ROW_FORMAT= Compact;
/*用户表*/
DROP TABLE IF EXISTS `t_user`; CREATE TABLE `t_user`(`id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) CHARACTER
SET utf8 COLLATE utf8_bin NULL DEFAULT NULL, `password` varchar(100) CHARACTER
SET utf8 COLLATE utf8_bin NULL DEFAULT NULL, `status` int(1) NULL DEFAULT NULL COMMENT '用户状态1-启用 0-关闭', PRIMARY KEY(`id`) USING BTREE) ENGINE= InnoDB AUTO_INCREMENT= 6 CHARACTER
SET= utf8 COLLATE= utf8_bin ROW_FORMAT= Compact;
/*角色与权限关系表*/
DROP TABLE IF EXISTS `t_role_permission`; CREATE TABLE `t_role_permission`(`RID` int(11) NOT NULL COMMENT '角色编号', `PID` int(11) NOT NULL COMMENT '权限编号', PRIMARY KEY(`RID`, `PID`) USING BTREE, INDEX `FK_Reference_12`(`PID`) USING BTREE, CONSTRAINT `FK_Reference_11` FOREIGN KEY(`RID`) REFERENCES `t_role`(`ID`) ON
DELETE RESTRICT ON
UPDATE RESTRICT, CONSTRAINT `FK_Reference_12` FOREIGN KEY(`PID`) REFERENCES `t_permission`(`ID`) ON
DELETE RESTRICT ON
UPDATE RESTRICT) ENGINE= InnoDB CHARACTER
SET= utf8 COLLATE= utf8_general_ci ROW_FORMAT= Compact;
/*用户与角色关系表*/
DROP TABLE IF EXISTS `t_user_role`; CREATE TABLE `t_user_role` ( `UID` int(11) NOT NULL COMMENT '用户编号', `RID` int(11) NOT NULL COMMENT '角色编号', PRIMARY KEY (`UID`, `RID`) USING BTREE, INDEX `FK_Reference_10`(`RID`) USING BTREE, CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `t_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT, CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `t_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
为了测试,再加一张商品表:
/*商品表*/
DROP TABLE IF EXISTS `t_product`; CREATE TABLE `t_product` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '商品名称', `price` decimal(10, 2) NULL DEFAULT NULL COMMENT '商品价格', `stock` int(11) NULL DEFAULT NULL COMMENT '库存', `is_show` tinyint(4) NULL DEFAULT NULL COMMENT '是否展示', `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;
整合thymeleaf这里不说了,实际项目是前后端分离。
下面正式开始学习security的使用:
一:基本原理:
security就是运用一系列的过滤器,实现不同情况的拦截校验功能。
具体的过滤器这里先不说。
二:认证
认证的意思就是,对所有请求拦截,判断是否登录,如果没有,定位到登录页面。
security有两种认证方式:HttpBasic认证和form表单认证。 http这种很简陋,就不用说了。 看一下formLogin的认证。
1、表单认证:
controller:
在confifig包下编写SecurityConfiguration配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* http请求方法
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 开启表单认证
.loginPage("/login.html") //认证失败跳转登录页面
.and().authorizeRequests()
.antMatchers("/login.html").permitAll() //放行登录页面
.anyRequest().authenticated(); //所有请求都要认证
}
}
过滤器UsernamePasswordAuthenticationFilter就是处理表单登录的
可以看到默认是form表单中的登录url,以及用户名密码输入框的name属性值。是可以修改的:
假如登录后跳转的是同源页面,页面可能无法展示,需要加载同源域名下iframe页面,需要继续在configure方法中:
对了,还要添加一下登录成功后的跳转路径:
上面的登录认证的用户名密码都是基于框架 ,实际我们要使用数据库的user。需要实现security的一个UserDetailsService接口, 重写这个接口里面 loadUserByUsername即可
在SecurityConfifiguration类中指定自定义用户认证
另外,如果密码要使用加密,数据库中也要存密文。
在登录成功后,前端html中可能会需要获取昵称头像等做展示,也就是需要User信息。 如果前端页面也整合了security标签,那就直接获取security框架的UserDetails,因为在登录成功后,框架会存储用户信息。 获取的方式有几种:
2、记住我: remember-me
记住我的功能,可以不用每次都输入用户名和密码。
security实现记住我的原理是,在登录的时候,UsernamePasswordAuthenticationFilter认证成功后会调用RememberMeService->TokenRepository,生成一个token存入数据库(关联着用户信息),同时会写到cookie中。 下次登录时候, 会进入RemeberMeAuthticationFilter这个过滤器中,通过cookie从数据库取出该Token对应的用户名以及其他信息放入UserDetailService。
第一次启动会自动创建表persistent_logins,并在登录后插入信息:
但是,记住我功能有风险。 如果cookie被截取,就可以在其他地方利用cookie,访问需要登录后才能访问的接口。 所以要不就不使用该功能,要不就在重要的接口中加入认证:
如果登录是来源于remember-me功能,就报错。
3、验证码
验证码是为了保证人为操作而非机器,在经过UsernamePasswordAuthenticationFilter登录认证之前,就需要先通过验证码校验,所以可以自己写一个验证码的filter,加入到前面。
securityConfig中注入并添加
4、session管理
默认使用Spring的session来管理。
securityConfig中还可以控制:失效后跳转登录页面; 同一账号同一时间的在线个数(多设备);
如果在集群环境下,session的管理就应该放到redis等公共的地方实现共享,只需要在配置文件配置:
spring.session.store-type=redis
7、CSRF(Cross-site request forgery),中文名称:跨站请求伪造
security中的csrf防御机制:
使用方式: 后端开启,前端也要配置
说到csrf, 再说一下跨域。 跨域其实是对浏览器的一种保护,如果产生了跨域,服务器在返回结果时就会被浏览器拦截,导致响应结果不可用。
哪些情况会产生跨域:
端口号不同,也会跨域。
security开启跨域支持:
三: 授权:
springSecurity提供了很多内置表达式来控制权限,原理是首先需要告诉security当前的用户有哪些权限,然后在需要鉴权的接口或者其地方,使用表达式来告知,访问当前资源需要什么权限,再判断用户的权限中是否满足。
这些表达式具体使用有几种方式:
1、基于url的表达式: 对指定的url做限制
在securityConfig中增加配置,如
如果没有权限会报错,需要给出友情提示,可以自定义提示:
最后需要在用户登录认证的方法loadUserByUsername(String username)中,给用户添加相关的权限。
2、在表达式中使用bean授权:
就是写一个bean,在方法中写权限校验逻辑。 然后,还是在securityConfig中指定ur指定使用bean的方法来校验。
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
/**
* 自定义bean授权
*/
@Component
public class MyAuthorizationService {
/**
* 检查用户是否有权限
*
* @param authentication 认证信息
* @param request 请求对象
* @return
*/
public boolean check(Authentication authentication, HttpServletRequest request) {
UserDetails principal = (UserDetails) authentication.getPrincipal();
String username = principal.getUsername();
// 获取用户权限的集合
Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>)
principal.getAuthorities();
// 如果用户名为admin 直接返回true
if ("admin".equalsIgnoreCase(username)) {
return true;
} else {
// 获取请求路径
String requestURI = request.getRequestURI();
if (requestURI.contains("/user")) {
// 循环判断用户的权限集合是否包含ROLE_ADMIN
for (GrantedAuthority authority : authorities) {
if ("ROLE_ADMIN".equals(authority.getAuthority())) {
return true;
}
}
}
if (requestURI.contains("/product")) {
// 循环判断用户的权限集合是否包含ROLE_PRODUCT
for (GrantedAuthority authority : authorities) {
if ("ROLE_PRODUCT".equals(authority.getAuthority())) {
return true;
}
}
}
}
return false;
}
/**
* 检查ID是否大于10
*
* @param authentication 认证信息
* @param request 请求对象
* @return
*/
public boolean check(Authentication authentication, HttpServletRequest request, Integer id) {
if (id > 10) {
return false;
}
return true;
}
}
实际上,在项目中,url很多。 如果都在securityConfig类中来配置,一点也不实际。 所以上面两种配置方式大多数情况不太实用。
3、Method安全表达式:
首先,在SecurityConfig配置类上开启方法级别的注解支持
在需要权限控制的接口路径上添加相关的注解:
@PostAuthorize:方法执行后再进行权限验证,适合验证带有返回值的权限
上面讲解了几种权限配置认证的方式,说明了什么样的资源需要如何来限制。 原理都是用配置去对比User中的权限是否匹配。
4、RBAC权限控制模型
当然了,抛开上面几种方式来说,其实现在用的最为广泛的,是基于RBAC数据模型(Role-Based Access Control),也就是文章开头准备的5张表,关系如下:
用户有哪些角色,每个角色可以操作什么权限,都是多对多的关系。
所以:
1、用户登录的时候,就需要根据用户id,去数据库查询所有的权限:
2、在SecurityConfig配置文件中,项目启动的时候,需要把所有的权限从数据加载到框架中:
权限信息Permission中,记录了资源url,权限tag(如ROLE_ADMIN),权限名称等信息。这样,框架中有了所有的权限信息,就可以去匹配user中权限的信息了。
当然了,如果没有前后端没有分离,可能还会自己去写前端,前端也可以用security控制。
下一节,看看security源码。