Spring Security

以下基于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)

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值