以下基于Spring Boot进行演示
目录
1、简介
2、入门
3、内存配置
4、角色控制
5、连接数据库
6、动态权限
1、简介
spring Security是一个功能强大、可高度定制的身份验证和访问控制框架。它是保护基于Spring的应用程序的事实标准。
Spring Security是一个面向Java应用程序提供身份验证和安全性的框架。与所有Spring项目一样,Spring Security的真正威力在于它可以轻松地扩展以满足定制需求。
springsecurity底层实现为一条过滤器链,就是用户请求进来,判断有没有请求的权限,抛出异常,重定向跳转。
2、入门
1、导入依赖
<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.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2、配置文件
spring:
thymeleaf:
cache: false
3、index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
欢迎
</body>
</html>
4、测试 ,当在浏览器输入localhost:8080
的时候,会进入到Spring Security的默认登录页面,默认的账号是user
,密码已经输出到控制台了。
5、登录成功后,才会进入首页
6、如果想要自己设置账号密码的话,可以在yml中进行配置
spring:
thymeleaf:
cache: false
security:
user:
name: admin
password: admin
7、这样一来,我们的账号和密码就变成了admin
,再次启动项目进行测试,输入账号密码登录
8、可以看到,同样是登录成功的
9、在项目启动类SpringBootApplication
注解上添加排除security框架的代码,便可去掉安全校验,项目重启之后,清除浏览器缓存,不用登陆即可访问首页
// 排除security安全验证,需要注意的是,如果写了配置类,这种排除方法将会失效
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
3、内存配置
除了以上两种配置账号密码的方式,我们还可以基于内存配置用户名和密码,只需要加入一个配置类即可。这个配置类需要继承 WebSecurityConfigurerAdapter
类,并且需要重写此类中的configure()
方法,值得一提的是,这里有2个重载的方法,我们需要实现的是参数为AuthenticationManagerBuilder
类的方法,另外一个方法是授权方法。
package com.th.t1.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/*super.configure(auth);*/ //需要注意的是,这一句要把它删除,否则它使用的还是默认或配置文件中的账号密码
auth.inMemoryAuthentication() //将用户信息存在内存中
.withUser("a") //用户名
.password(passwordEncoder().encode("123")) //密码,需要进行加密处理
.roles(); //角色,这里暂时没有赋予角色
auth.inMemoryAuthentication()
.withUser("b")
.password(passwordEncoder().encode("123"))
.roles();
auth.inMemoryAuthentication()
.withUser("c")
.password(passwordEncoder().encode("123"))
.roles();
}
/**
* security5版本之后要求密码必须是密文加密,不然会报错
* 这里使用的是BCryptPasswordEncoder加密类,把它注入到了容器中
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
其余部分与入门操作是一样的,这里就不进行赘述,完善好之后,开始进行测试。首先使用账号a进行登录。
可以看到是成功的,接下来依次使用b、c账号进行登录,同样也是可以登录的。
4、角色控制
代码
配置类
package com.th.t1.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableGlobalMethodSecurity(prePostEnabled = true) //启动角色认证
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/*super.configure(auth);*/ //需要注意的是,这一句要把它删除,否则它使用的还是默认或配置文件中的账号密码
auth.inMemoryAuthentication() //将用户信息存在内存中
.withUser("a") //用户名
.password(passwordEncoder().encode("123")) //密码,需要进行加密处理
.roles("admin","user"); //这里给了两个角色
auth.inMemoryAuthentication()
.withUser("b")
.password(passwordEncoder().encode("123"))
.roles("user"); //这里只有一个角色身份
auth.inMemoryAuthentication()
.withUser("c")
.password(passwordEncoder().encode("123"))
.roles("admin");
}
/**
* security5版本之后要求密码必须是密文加密,不然会报错
* 这里使用的是BCryptPasswordEncoder加密类,把它注入到了容器中
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
SecurityController
package com.th.t1.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class SecurityController {
/**
* 管理员和普通用户都可以访问
* @return
*/
@RequestMapping("/adminAndUser")
@PreAuthorize("hasAnyRole('admin','user')") //限定只有拥有admin和user角色的用户可以访问
public String adminAndUser(){
return "adminAndUser";
}
/**
* 只有管理员可以访问
* @return
*/
@RequestMapping("/admin")
@PreAuthorize("hasAnyRole('admin')") //限定只有拥有admin角色的用户可以访问
public String admin(){
return "admin";
}
/**
* 只有普通用户可以访问
* @return
*/
@RequestMapping("/user")
@PreAuthorize("hasAnyRole('user')") //只有拥有user角色的用户可以访问
public String user(){
return "user";
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="/adminAndUser">管理员和普通用户</a><br/>
<a href="/admin">管理员</a><br/>
<a href="/user">普通用户</a><br/>
</body>
</html>
adminAndUser.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>管理员和普通用户</h1>
</body>
</html>
admin.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>管理员</h1>
</body>
</html>
user.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>普通用户</h1>
</body>
</html>
测试
启动项目,在浏览器输入localhost:8080
,会进入登录页面,首先使用a账号登录
进入到首页
由于a账号拥有所有角色,所以全部的链接都是可以点进去的,如下:
接下来我们使用b账号登录,由于b账号只有user的权限,所以管理员页面是无法进入的,会出现403错误,即不允许访问,如下:
最后使用c账号进行测试,c账号只有管理员的权限,所以普通用户页面是无法进入的,这里就不进行演示了。
小结
经过测试,可以发现Spring Security与过滤器拦截器类似,而角色控制则是条件,没有满足条件即没有相对应的角色,就将请求过滤掉。
5、连接数据库
以上的内容使用的用户信息都不是持久化的,且无法存储大量的用户数据,接下来我们将进行更高层次的应用,从数据库中读取用户信息,并且基于RBAC进行角色的访问控制。(关于RBAC需要叙述的东西太多,这里就不进行讲解了)
数据库
RBAC模型最少需要五张表,当然,根据业务需求可以在此基础上进行拓展,这里就以最原始的RBAC进行讲解。(这里使用的规则是一个用户可以拥有多个角色,一个角色可以拥有多个权限)
用户表
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
角色表
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
用户角色表
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` INT NOT NULL AUTO_INCREMENT,
`user_id` INT NULL DEFAULT NULL,
`role_id` INT NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
权限表
DROP TABLE IF EXISTS `access`;
CREATE TABLE `access` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
角色权限表
DROP TABLE IF EXISTS `role_access`;
CREATE TABLE `role_access` (
`id` INT NOT NULL AUTO_INCREMENT,
`role_id` INT NULL DEFAULT NULL,
`access_id` INT NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
插入数据
#用户表,密码是加密过后的123,加密方式是Spring Security的BCryptPasswordEncoder
INSERT INTO `user` VALUES (1, '张三', '$2a$10$CgjOS./10OlTFB3N2Eo1LOmZfD2kE48eljSOB.OotifcIqHfOj/RC');
INSERT INTO `user` VALUES (2, '李四', '$2a$10$iQr6NMAk.AzHgbsVMezg1efJhGdaRsCBwpQo/FCgri2fFfVOoyjLK');
INSERT INTO `user` VALUES (3, '王五', '$2a$10$XWZL441Z0V4fGaB5IXHPcOXArsmC4BY0Iy6ggmyxfPk4hgXMpRHqC');
#角色表
INSERT INTO `role` VALUES (1, 'user');
INSERT INTO `role` VALUES (2, 'admin');
INSERT INTO `role` VALUES (3, 'guest');
#用户角色表
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (2, 2, 2);
INSERT INTO `user_role` VALUES (3, 3, 3);
#权限表
INSERT INTO `access` VALUES (1, 'v1');
INSERT INTO `access` VALUES (2, 'v2');
INSERT INTO `access` VALUES (3, 'v3');
#角色权限表
INSERT INTO `role_access` VALUES (1, 1, 1);
INSERT INTO `role_access` VALUES (2, 2, 2);
INSERT INTO `role_access` VALUES (3, 3, 3);
代码
为了便于理解,下面的代码会拆分的比较细。
依赖
<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.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
pojo层
实体类依据表进行创建,使用Lombok的@Data
注解,以下是一个具体的例子,剩余四个实体类大家可以照此例进行创建。
package com.th.rbac.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
import org.springframework.security.core.userdetails.UserDetails;
@Data
public class User{
private long id;
private String name;
private String password;
}
dao层
由于使用的是Mybatis-plus,所以这里只需要继承BaseMapper
就可以了,下面是一个具体的例子:
package com.th.rbac.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.th.rbac.pojo.User;
public interface UserMapper extends BaseMapper<User> {
}
service层
由于只是进行简单的测试,所以并没有创建接口,而是直接写的实现类,需要继承mybatisplus中的ServiceImpl
,示例如下
package com.th.rbac.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.th.rbac.mapper.UserMapper;
import com.th.rbac.pojo.User;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> {
}
controller层
package com.th.rbac.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SecurityController {
@RequestMapping("/v1")
public String v1() {
return "v1";
}
@RequestMapping("/v2")
public String v2() {
return "v2";
}
@RequestMapping("/v3")
public String v3() {
return "v3";
}
}
首页
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="/v1">v1</a><br/>
<a href="/v2">v2</a><br/>
<a href="/v3">v3</a><br/>
</body>
</html>
启动类
package com.th.rbac;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan(basePackages = "com.th.rbac.mapper") //扫描数据层,将其注入到Spring容器中
public class RBACApplication {
public static void main(String[] args) {
SpringApplication.run(RBACApplication.class, args);
}
}
至此,一个项目的框架就搭建好了,接下来就可以开始将Security集成到这个案例中了。由于我们需要从数据库中读取数据,所以需要在service层创建一个类,并且继承UserDetailsService
,实现loadUserByUsername
方法,如下:
package com.th.rbac.service;
import com.th.rbac.mapper.*;
import com.th.rbac.pojo.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class RBACUserDetailsService implements UserDetailsService {
//接下来就是核心部分,我们需要做的就是根据用户名拿到用户信息
//然后通过用户信息,拿到该用户的角色
//最后通过角色信息,拿到该角色所拥有的权限
//在这过程中将依次讲解五张表的作用和关系
@Resource
private UserMapper userMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private RoleAccessMapper roleAccessMapper;
@Resource
private UserRoleMapper userRoleMapper;
@Resource
private AccessMapper accessMapper;
//重写loadUserByUsername方法
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询账户,这里的用户名是SpringSecurity自动帮我们获取到的
Map<String, Object> map = new HashMap<>();
map.put("name", username);
List<User> users = userMapper.selectByMap(map); //这里是mybatisplus的方法,根据条件查询集合
User user = users.get(0); //一般用户名是唯一的,所以查出来只会有一条数据
if (user != null) {
//拿到用户的数据之后,接下来要拿到该用户的角色
//但是用户表和角色表之间是没有任何的直接关联的
//但是它们之间有一个中间表,用户角色表,我们可以通过它,间接的拿到角色id
//根据用户id,查询用户角色关系表,这样我们就拿到了该用户所拥有的所有角色信息了
Map<String, Object> role = new HashMap<>();
role.put("user_id", user.getId());
List<UserRole> userRoles = userRoleMapper.selectByMap(role);
//拿到关系表中的数据之后,可以通过角色id拿到角色的详细信息
List<Role> listRole = new ArrayList<>();
for (UserRole userRole : userRoles) {
Role role1 = roleMapper.selectById(userRole.getRoleId());
listRole.add(role1);
}
//为了拿到角色所拥有的权限,我们需要借助中间表——角色权限表
//需要注意的是,我们的用户可能拥有多种角色,每种角色又拥有自己对应的权限
//所以这里使用集合套集合的方式进行存储数据
List<List<RoleAccess>> roleAccesses = new ArrayList<>();
for (Role role1 : listRole) {
Map<String, Object> access = new HashMap<>();
access.put("role_id", role1.getId());
List<RoleAccess> roleAccesses1 = roleAccessMapper.selectByMap(access);
roleAccesses.add(roleAccesses1);
}
//接下来使用权限id查询该角色所拥有的权限信息
//使用双重循环读取权限信息
List<Access> accesses = new ArrayList<>();
for (List<RoleAccess> roleAccess : roleAccesses) {
for (RoleAccess access : roleAccess) {
Access access1 = accessMapper.selectById(access.getAccessId());
accesses.add(access1);
}
}
//这个集合存放的是用户所拥有的角色和权限
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
//将该用户拥有的角色信息全部存放到集合中
//需要注意的是,为了区分开角色和权限,在角色前我们需要加上 ROLE_
listRole.stream().forEach((e) -> {
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + e.getName());
grantedAuthorities.add(authority);
});
//将权限信息全部存放到集合中
accesses.stream().forEach((e) -> {
GrantedAuthority authority = new SimpleGrantedAuthority(e.getName());
grantedAuthorities.add(authority);
});
//将用户名,密码,权限返回
return new org.springframework.security.core.userdetails.User(user.getName(),
user.getPassword(),
grantedAuthorities);
}
return null;
}
}
接下来就是老生常谈的配置类了,需要重写两个方法:
- 授权:configure(HttpSecurity http)
- 认证:configure(AuthenticationManagerBuilder auth)
package com.th.rbac.config;
import com.th.rbac.service.RBACUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private RBACUserDetailsService rbacUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/*super.configure(auth);*/ //将这一句删除,否则使用的还是默认的用户
//这里使用userDetailsService方法,将我们的userService实例作为参数,随后在对密码的编码进行设定
//需要注意的是,加密方式必须要和创建用户的加密方式一致
//使用Spring Security自带的加密类是极好的,因为它不像MD5那样,加密后的字符串是相同的
auth.userDetailsService(rbacUserDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
/*super.configure(http);*/ //将这句删除
http
.authorizeRequests() //请求授权规则的开始
.antMatchers("/").permitAll() //允许所有人访问 /
.antMatchers("/v1").hasRole("user") //必须拥有user角色才能访问 /v1,需要注意的是hasRole会在角色自动加上前缀 ROLE_,这就是在RBACUserDetailsService 类中,为了区分角色和权限加 ROLE_ 的原因
.antMatchers("/v1").hasAuthority("v1") //必须拥有v1权限才能访问 /v1
.antMatchers("/v2").hasRole("admin")
.antMatchers("/v2").hasAuthority("v2")
.antMatchers("/v3").hasRole("guest")
.antMatchers("/v3").hasAuthority("v3")
.and() //开启一个新的设置项
.formLogin(); //设置登录表单
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
测试
接下来启动项目,访问localhost:8080
,如下:
首先我们点击v1,会弹出登录页面,使用张三的账号登录
登录是可以访问的
接下来我们访问localhost:8080/v2
,因为张三账号是没有权限的,所以会报403错误,访问v3同样如此
接下来我们看看另一种情况,在数据库中给张三用户添加admin
的角色。
INSERT `user_role` VALUES(4,1,2);
如此一来张三便拥有了admin的角色,可以访问v2了。重新登录,访问v2如下:
最后我们给予user角色v3的权限
INSERT `role_access` VALUES(4,1,3);
重新登录,访问v3如下:
如果想要移除掉张三的角色或者某个角色的权限,只需要移除掉对应关系表中的数据即可
小结
通过上述案例,我们已经了解RBAC在Spring Security中的具体实现过程,可以通过对数据库的修改,来赋予或移除用户角色和权限,当然这种模型是有缺陷的,比如在这个案例中,user角色拥有v2的权限,而admin角色也有v2的权限,两个角色之间拥有相同的权限,这就造成了权限的重叠,在实际情况中,可以根据自己的需求去设置其中用户-角色-权限之间的关系,比如角色之间的权限不能相同,又比如用户只能拥有一个角色等等。
常用方法
在配置类中还有一些比较常用的方法,如下:
方法名 | 说明 |
---|---|
authorizeRequests | 请求授权规则的开始 |
antMatchers(String…) | 访问的路径需要进行权限的限制 |
permitAll() | 所有用户都可以访问的页面 |
hasRole(String) | 如果用户具备给定角色就允许访问 |
hasAuthority(String) | 判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建 User 对象时指定的 |
hasAnyAuthority(String…) | 如果用户具备给定权限中某一个,就允许访问 |
hasAnyRole(String…) | 如果用户具备给定角色的任意一个,就允许被访问 |
hasIpAddress(String) | 如果请求是指定的 IP 就运行访问,可以通过 request.getRemoteAddr()获取 ip 地址,需要注意的是在本机进行测试时 localhost 和 127.0.0.1 输出的 ip地址是不一样的 |
注意:hasRole和hasAuthority的不同之处在于,hasRole是判断用户是否有这个角色,会自动加上前缀,而hasAuthority则是判断用户是否有权限,不会自动加上前缀。
功能
http.logout().logoutSuccessUrl("/"); //开启注销功能
http.csrf().disable(); //关闭防止csrf功能 解决注销退出报错
http.rememberMe().rememberMeParameter("remeber"); //开启记住我功能,默认保存在cookies中14天
//没有授权 默认回到登陆界面
http.formLogin()
.loginPage("/toLogin")
.usernameParameter("user")
.passwordParameter("pwd")
.loginProcessingUrl("/login");
6、动态权限
在上一章节,我们以RBAC为基础实现了一个简单的案例,极大的方便了我们对用户进行角色和权限的授予移除,但是我们会发现,授权规则写死在configure(HttpSecurity http)
中,想要修改规则,就必须要修改源码才行,这很不方便,我们想要的是动态的去管理每一个请求的权限,即在不需要改变源码的情况下,改变授权规则。
这里有两种实现方案,一种是地址和角色的固定变化不大的场景下,可以从数据库中读取地址,通过HttpSecurity
对象映射角色,但这种方案不太好在项目运行期间动态改变授权规则
。还有一种方案就是实现FilterInvocationSecurityMetadataSource
接口,根据当前访问的url返回该url所具有的全部角色。后者更为灵活,但每次访问一次接口都会获取全部的角色权限,肯定性能有所损失,不过这种性能的损失是可以弥补的,比如将角色权限信息放入redis,当然,这里并不讲解如何解决性能问题。
方案一
修改权限表,在权限表中增加一个url字段,将路径与权限关联起来,这里我们继续沿用之前的案例,修改后,权限表如下:
接下来,我们只需要修改配置类即可,无需改动其他地方,修改后的配置类如下:
package com.th.rbac.config;
import com.th.rbac.mapper.AccessMapper;
import com.th.rbac.mapper.RoleAccessMapper;
import com.th.rbac.pojo.Access;
import com.th.rbac.service.RBACUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
import java.util.List;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private RBACUserDetailsService rbacUserDetailsService;
@Resource
private AccessMapper accessMapper;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(rbacUserDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests= http.authorizeRequests();
//查询权限表
List<Access> accesses = accessMapper.selectList(null);
//将路径和权限循环配对起来
//其实就是相当于我们不用手动的一个一个去写了,本质上还是与写死规则类似
accesses.forEach(m-> {
authorizeRequests.antMatchers(m.getUrl()).hasAnyAuthority(m.getName());
});
//首页所有人都可以访问
authorizeRequests
.antMatchers("/").permitAll()
.and()
.formLogin();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
测试
使用李四的账号进行登录
因为李四的账号只有admin角色,而admin角色只有v2权限,所以只能访问到v2页面
接下来我们假设因为业务需求,v3这个权限要取消掉,/v3
所对应的权限变为v2,修改数据库,如下:
不要重新运行项目,直接使用李四账号重新登录
这个时候我们会发现一个问题,在数据库中,李四的账号明明已经拥有了/v3路径的权限,却仍旧无法访问/v3
这就是上面所说的第一种实现方案的缺陷了,让我们来了解到底是为什么。我们在onfigure(HttpSecurity http)
方法中增加一条输出语句,如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests= http.authorizeRequests();
List<Access> accesses = accessMapper.selectList(null);
//这里输出数据库中的权限表信息
System.out.println("权限:"+accesses);
for (Access access : accesses) {
authorizeRequests.antMatchers(access.getUrl()).hasAuthority(access.getName());
}
authorizeRequests
.antMatchers("/").permitAll()
.and()
.formLogin();
}
接下来运行程序,观察控制台,可以看到,程序一运行,控制台就输出了信息,这意味着我们的程序在运行的时候,授权规则就已经定死了,即便我们修改了数据库,它使用的授权规则也不会改变,我们只有重启项目,让程序读取数据库才能应用新的授权规则,这样虽然无需改变源码,但缺陷也很明显。
方案二
接下来我们使用第二种方案实现动态权限,相比第一种方案,第二种方案的实现难度要高一些。下面创建一个类实现FilterInvocationSecurityMetadataSource
接口:
package com.th.rbac.service;
import com.th.rbac.mapper.AccessMapper;
import com.th.rbac.pojo.Access;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import javax.annotation.Resource;
import java.util.*;
@Component
public class RBACFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Resource
private AccessMapper accessMapper;
//用来实现ant风格的Url匹配
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//获取当前请求的Url
String requestUrl = ((FilterInvocation) object).getRequestUrl();
//这里输出一下路径,以便进行观察
System.out.println("当前请求的Url:"+requestUrl);
//获取权限表中所有的数据
List<Access> accesses = accessMapper.selectList(null);
List<ConfigAttribute> list = new ArrayList<>();
for (Access access : accesses) {
//判断当前访问的路径与数据库中的某个路径是否相等
if (antPathMatcher.match(access.getUrl(),requestUrl)){
//如果两者相等,则将资源路径需要的权限放入集合中
ConfigAttribute configAttribute = new SecurityConfig(access.getName());
list.add(configAttribute);
}
}
return list;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return false;
}
}
还需创建一个CustomAccessDecisionManager
用来实现AccessDecisionManager
:
package com.th.rbac.service;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
@Component
public class RBACAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
//configAttributes 角色权限表,用户登陆时所拥有的权限和角色集合
//authentication 访问资源时所需要的权限或角色集合
//思路便是:拿到访问该资源需要的权限,然后拿到该用户所拥有的权限
//循环判断该用户所拥有的权限是否包含了访问该资源的权限
//如果包含了,则证明该用户有权限,允许访问,没有包含,则用户没有权限,不允许访问
//这里输出一下,以便观察执行的先后顺序
System.out.println("访问该资源需要的权限:"+authentication);
//这一层循环获取的是用户的权限
for (ConfigAttribute configAttribute : configAttributes) {
//获取访问资源时所需要的权限或角色
Collection<? extends GrantedAuthority> accesses = authentication.getAuthorities();
//这一层循环,获取的是访问该路径需要的权限
for (GrantedAuthority grantedAuthority : accesses) {
//将两者进行判断
if (configAttribute.getAttribute().equals(grantedAuthority.getAuthority())){
return;
}
}
}
throw new AccessDeniedException("权限不足");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return false;
}
@Override
public boolean supports(Class<?> clazz) {
return false;
}
}
修改WebSecurityConfig
package com.th.rbac.config;
import com.th.rbac.service.RBACAccessDecisionManager;
import com.th.rbac.service.RBACFilterInvocationSecurityMetadataSource;
import com.th.rbac.service.RBACUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private RBACUserDetailsService rbacUserDetailsService;
@Autowired
private RBACFilterInvocationSecurityMetadataSource rbacFilterInvocationSecurityMetadataSource;
@Autowired
private RBACAccessDecisionManager rbacAccessDecisionManager;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(rbacUserDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
//这个方法的大概意思就是说,将我们自己重写的两个类加入到过滤链条中,需要重写postProcess方法
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(rbacFilterInvocationSecurityMetadataSource);
object.setAccessDecisionManager(rbacAccessDecisionManager);
return object;
}
})
.antMatchers("/").permitAll()
.and()
.formLogin();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
测试
如此一来,我们就实现了第二种动态权限管理的方案,废话不多说,运行项目使用李四的账号登录:
李四的账号只有访问v2的权限:
假设因为一些业务需求,我们需要抛弃掉v3这个权限,访问/v3
路径的权限变成v2,修改数据库:
不需要重新登录,不需要重启项目,我们直接访问/v3
,发现可以访问了
如此一来我们就实现了动态的修改授权规则,接下来查看控制台
我们可以发现,当我们访问/v2
时,先进入的是FilterInvocationSecurityMetadataSource
,到数据库进行一次查询操作,将该资源路径需要的权限存放到一个集合中,然后就会进入AccessDecisionManager
,它会拿到这个集合,并且还会拿到用户所拥有的权限和角色集合,将两者进行比较就能得出该用户能不能访问该路径了。
小结
讲到这里,上述所说的增加系统资源消耗的问题也暴露出来了,我们每一次访问资源路径,都会到数据库里查一次,在高并发下,很容易就会让系统崩溃掉。其实可以把这些东西放入到Redis中,当然,这里只是提供一个思路,并不进行性能调优的讲解。
另外,为了便于讲解,以上的案例只是一个阉割版的动态权限管理,在实际开发中,需要考虑的问题更多,不过依旧是在此基础上进行拓展。
最后,这个简单的案例已经发到gitee上去了,可以下载进行学习:https://gitee.com/thlyj/rbac
另外,Spring Security是可以和shiro进行对比学习的,有兴趣的话,可以去看看我的另一篇博客:Shiro动态权限(整合Spring Boot)