文章目录
前言
本章内容详细介绍了RBAC模型和SpringSecurity框架的使用。
一、RBAC模型
1. 概述
- 在企业系统中,通过配置用户的功能权限可以解决不同的人分管不同业务的需求,基于RBAC模型,RBAC(Role Based Access Control)模型,它的中文是基于角色的访问控制,主要是将功能组合成角色,再将角色分配给用户,也就是说角色是功能的合集。
- 比如:企业A总共有12个功能,需要创建100个用户。这些用户包括财务管理、人事管理、销售管理等等。如果不引入基于角色的访问控制(RBAC)模型,我们就需要每创建一个用户就要分配一次功能,这将至少需要进行100次操作(每个用户只能拥有一个功能)。如果用户数量增加到1000甚至10000,并且一个用户可能会拥有多个功能,操作将会变得非常繁琐。如图:

- 经过多次操作后发现,有些人被分配了相同的功能。例如,A、B等10个用户都被分配了客户管理、订单管理和供应商管理这几个模块。其实我们完全可以将这几个功能模块组合成一个包,然后将整个包分配给需要的用户,这个包被称为角色。由于角色和功能之间的对应关系相对稳定,在分配权限时只需分配角色即可,如下图所示:

- 基于RBAC授权模式后,我们可以达到以下2个目标:
- 解耦用户和功能,降低操作错误率
- 降低功能权限分配的繁琐程度

2. ER图与关系梳理
在一个核心业务系统中,我们通常通过业务分析,从而抽离出数据库表,表确定之后我们会进一步分析表中应该有的字段,下面我们先看下RBAC模型涉及的业务ER图:

- 上图中清楚的描述用户、角色、资源、职位、部门之间的关系,同时我们进一步推导出以下结果:
- 用户与职位是N:1关系
- 用户与部门是N:1关系
- 用户与角色是N:N关系,则它们之间必然有一个中间表
- 角色与资源是N:N关系,则它们之间必然有一个中间表

3. 总结
- RBAC模型是基于角色的访问控制模型,该模型最常应用于系统权限设计中
- RBAC模型中,一般会有是三个基本对象,用户,角色和权限,他们三个分别都是多对多关系,那么也就意味着有两张中间表,所以数据库表层面共有五张表
二、SpringSecurity认证授权
1. 权限框架
- 权限管理是所有后台系统的都会涉及的一个重要组成部分,主要目的是对不同的人访问资源进行权限的控制,避免因权限控制缺失或操作不当引发的风险问题,如操作错误,隐私数据泄露等问题。
- 权限管理实现方式
- 方案一:使用拦截器(过滤器)+JWT 实现地址鉴权

- 方案二:使用权限框架 Apache Shiro
- Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份 认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。Shiro最大的特点是 不跟任何的框架或者容器捆绑,可以独立运行。
如果我们项目,没有使用到Spring框架,可以考虑使用Shiro。Shiro在小项目使用比较常见。 - Shiro 最大的问题在于和 Spring 家族的产品进行整合时较为不便。在Spring Boot 推出的很长一段时间里,Shiro 都没有提供相应的 starter,后来虽然有一个 shiro-spring-boot-web-starter 出来,但配置并没有简化多少。所以在 Spring Boot/Spring Cloud 技术栈的微服务项目中,Shiro 几乎不存在优势。
- Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份 认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。Shiro最大的特点是 不跟任何的框架或者容器捆绑,可以独立运行。
- 方案三:使用权限框架Spring Security
- Spring Security是一个功能强大且高度可定制的,主要负责为Java程序提供声明式的身份验证和访问控制的安全框架。其前身是Acegi Security,后来被收纳为Spring的一个子项目,并更名为了Spring Security。
- 优点:
Spring Security基于Spring开发,所以Spring Security与Spring更契合;
Spring Security功能比Shiro更加强大,尤其是在安全防护方面;
Spring Security社区资源比Shiro更加丰富;
Spring Boot/Spring Cloud环境中,更容易集成Spring Security;
Spring Security 具备良好的扩展性,可以满足自定义的要求;
Spring Security对 OAuth2框架支持很好,而Shiro则对 OAuth2 支持不够。
- 方案四:使用AOP实现方法鉴权
- 方案一:使用拦截器(过滤器)+JWT 实现地址鉴权
2. 核心概念
-
认证
大家可以简单的理解为:用户登录的行为就是认证(你是谁)
判断用户是否存在,判断用户密码是否正确

-
授权
授权就是用户登录后,给用户授予访问哪些资源的权限,这样用户登录后就只看相关权限资源。
我们有很多的资源api列表,那这个登录后的用户是否拥有这个api访问权限 -
鉴权:
用户通过认证并被授权后进行的一道安全检查,判断用户是否有权利执行或访问该资源(校验资源)


3. SpringSecurity
Spring Security 是 Spring家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
3.1 SpringSecurity入门
3.1.1. 简单登录
首先我们先进行一个简单的登录案例演示
- 在SpringBoot空项目中引入引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 定义一个Controller类
下图中仅仅在web包中存在一个HelloController类,类中仅仅定义了一个hello方法。

- 启动项目访问http://localhost:8080/hello 这时会自动跳转到一个默认登陆页面
该页面有框架提供,默认用户名为user,密码会在控制台打印


- 输入用户名user(默认值)和密码后,会再次跳回到hello的输入页面。
页面内容为:hellouser - 如果退出登录,则需要访问:http://localhost:8080/logout
到这里演示完成了,我们只是引入了一个依赖,什么代码都没写,就实现了一套简单的权限控制了,当然实际的项目,肯定不能这么做的。
3.1.2. 自定义登录页
运行完刚才的例子,你会有两个感觉,第一个是登录窗口出现的特别慢,第二个感觉是登录窗口特别简陋。实际上,更多的项目是用自己的登录页,而不是框架自带的页面!
接下来我们自定义登录页面。
- 下图红框则为自定义登录页面

- 这个时候继续创建对应的配置类,配置登录页

@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.formLogin() //自定义自己编写的登陆页面
.loginPage("/login.html") //登录页面设置 以resources包下的static为根
.loginProcessingUrl("/login") //登录访问路径 以端口号为根
.permitAll()//登录页和登录访问路径无需登录也可以访问
.and()//回到http
.authorizeRequests()
.antMatchers("/css/**","/images/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable(); //关闭csrf防护
return http.build();
}
}
代码分析
- @Configuration 注解表明这是一个配置类。
SecurityConfig 类包含了一个名为 securityFilterChain 的bean,该bean负责配置安全过滤器链(SecurityFilterChain),这是Spring Security用来保护HTTP请求的主要机制。- 在 securityFilterChain 方法中,通过 HttpSecurity 对象来定义安全规则。首先调用 formLogin() 来配置表单登录功能。
.loginPage(“/login.html”) 指定了自定义登录页面的位置。
.loginProcessingUrl(“/login”) 设置了处理登录请求的URL,当用户尝试访问受保护资源但尚未认证时,Spring Security会重定向到此URL。
.permitAll() 表示任何用户都可以访问登录页面和登录处理URL。- 接下来调用 .authorizeRequests() 来指定哪些URL需要被保护、哪些不需要。
.antMatchers(“/css/", "/images/”).permitAll() 允许未认证的用户访问CSS和图片资源,这对于前端开发是非常有用的,因为这些通常是静态资源,不应该受到安全限制。
.anyRequest().authenticated() 表示所有其他请求都需要经过身份验证。
.csrf().disable() 是用来禁用跨站请求伪造(CSRF)保护的。在生产环境中,禁用CSRF保护是不推荐的,因为它可以保护应用程序免受某些类型的攻击。如果你的应用程序确实需要CSRF保护,那么你应该保留这个保护措施,并且确保你的客户端请求正确地包含了CSRF令牌。- .and() 方法通常用于结束当前的配置块并回到父配置级别,允许你继续进行其他的配置。这是因为Spring Security的配置API是基于流式风格(Fluent API)
的设计,使得配置更加连贯和易读。
在这个例子中:
formLogin() 配置块包括了 .loginPage(“/login.html”), .loginProcessingUrl(“/login”), 和 .permitAll() 这些方法调用。当你调用 .and() 时,就结束了 formLogin() 的配置,并回到了 HttpSecurity 的配置级别。
接下来的 .authorizeRequests() 开始了一个新的配置块,用于定义哪些请求是公开的,哪些请求需要认证。当 .authorizeRequests() 配置块完成后,再次调用 .and() 回到 HttpSecurity 的配置级别。
最后,.csrf().disable(); 用于禁用CSRF保护。
- 修改HelloController
@RequestMapping("/hello")
public String hello(){
//认证成功,得到认证成功之后用户信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String userName = authentication.getName();
return "hello "+userName;
}
代码分析
- @RequestMapping(“/hello”):这个注解指定了一个HTTP请求处理器,它将处理所有发送到 /hello 路径的请求。
- public String hello():这是一个返回字符串的方法,通常情况下,这个字符串会被解析为视图的名称,用于渲染响应给客户端的HTML页面。
- 在方法内部,首先通过 SecurityContextHolder 获取当前上下文中的 Authentication 对象。
SecurityContextHolder 是Spring Security提供的一个线程绑定的上下文对象,它持有当前认证状态。- Authentication 对象代表了当前执行操作的主体(通常是用户)。从这个对象中,可以通过 getName() 方法获取用户的名称。
- 最后,方法返回一个字符串 "hello "+userName,这通常会被解析为一个视图名,并且 userName 可能会被作为模型属性传递给视图,以便在视图中显示用户的名称。
- 再次运行项目,我们会看到登录页面,变成下面这个样子啦

运行后效果如下:

3.1.3. SpringSecurity基本原理
Spring-Security其内部基础的处理方式就是通过过滤器来实现的,来我们看下刚才的例子用到的一些过滤器,如图所示:

这几个过滤器负责工作内容如下
- UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
- ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。
- FilterSecurityInterceptor:负责权限校验的过滤器。
当然,SpringSecurity过滤器不止这些的,下面我们把他们展示出来,不需要同学们记忆,只需了解即可!

3.2 认证
3.2.1. 基于内存模型实现认证
- 修改配置类 SecurityConfig,添加两个bean的配置
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password("123456")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("112233")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
代码分析
- PasswordEncoder Bean
定义了一个 PasswordEncoder bean,用于加密和验证用户的密码。使用了 NoOpPasswordEncoder,这是一种不进行任何加密的编码器,仅用于演示目的。
在实际应用中,应该使用更强的加密方式来保护用户密码。- UserDetailsService Bean
定义了一个 UserDetailsService bean,它负责加载用户信息。这里使用了 InMemoryUserDetailsManager 来在内存中管理用户。该信息用户后续认证授权- UserDetails:这是Spring Security中的一个UserDetailsService 接口实例,表示一个用户的信息。它包含了用户名、密码、权限等信息。
- User.builder():这是一个工厂方法,用于创建 UserDetails 的实例。这里使用了 User 类的 builder() 方法来构建用户信息。
4.1. username 和 password:分别设置了用户的用户名和密码。注意,这里的密码是以明文形式存储的,这在实际应用中是不可取的。应该使用加密后的密码来
提高安全性。
4.2 roles:设置了用户的权限角色。在这个例子中,“user”用户只有一个“USER”角色,而“admin”用户同时具有“USER”和“ADMIN”两个角色。
4.3 InMemoryUserDetailsManager:这是一个实现了 UserDetailsService 接口的类,用于在内存中管理用户。在这里,我们创建了两个用户并将它们传递给InMemoryUserDetailsManager 的构造函数。
- 再次测试,输入用户名 user 密码123456
运行后效果如下:

3.2.2. 基于JDBC数据库实现认证
在Spring Security框架中提供了一个UserDetailsService 接口,它的主要作用是提供用户详细信息。具体来说,当用户尝试进行身份验证时,UserDetailsService 会被调用,以获取与用户相关的详细信息。这些详细信息包括用户的用户名、密码、角色等
执行流程如下:

- 新创建一个UserDetailsServiceImpl,让它实现UserDetailsService ,代码如下

@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(username.equals("user")){
UserDetails user= User.builder()
.username("user") .password("$2a$10$8V3UHgnnI/3RCKhg5aklz.sw448DP4.x9P2hFl/fnw99QU86POlgm")
.roles("USER")
.build();
return user;
}
if(username.equals("admin")){
UserDetails admin= User.builder()
.username("admin") .password("$2a$10$pCQMCKRUi7iUXGBd14a3oetcdgD1MwgKLenxXHidq1pcfmuva1QjW")
.roles("ADMIN","USER")
.build();
return admin;
}
return null;
}
}
代码分析
- 当前对象需要让spring容器管理,所以在类上添加注解@Component
- loadUserByUsername方法的返回值,叫做UserDetails,这也是框架给提供了保存用户的类,并且也是一个接口,如果我们有自定义的用户信息存储,可以实现这个接口
- 既然以上能使用这个类来查询用户信息,那么我们之前在SecurityConfig中定义的用户信息,可以注释掉了,如下:
/*
@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password("$2a$10$2VCyByZ5oeiXCEN73wvBB.xpmJgPBbZVS/Aallmdyij2G7hmAKQTG")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("$2a$10$cRH8iMMh6XO0T.ssZ/8qVOo8ThWs/qfntIH3a7yfpbPd05h9ZGx8y")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}*/
- 重启项目,然后进行测试,发现跟之前没什么区别,一样是实现了安全校验
当然我们最终不能把用户静态的定义在代码中的,我们需要到数据库去查询用户,我们可以直接使用我们项目中的用户表,实现的步骤如下:
根据自的数据库和字段进行配置即可

- 改造UserDetailsServiceImpl
package com.zzyl.security.service;
import com.zzyl.security.entity.User;
import com.zzyl.security.entity.UserAuth;
import com.zzyl.security.mapper.UserMapper;
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.Component;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.SimpleTimeZone;
/**
* @author sjqn
* @date 2023/9/1
*/
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户(框架提供的User)
User user = userMapper.findByUsername(username);
if(user == null){
throw new RuntimeException("用户不存在或已被禁用");
}
//SimpleGrantedAuthorit是GrantedAuthority的实现类
//该类用于分组角色信息 User构造方法需要GrantedAuthority集合
SimpleGrantedAuthority user_role = new SimpleGrantedAuthority("user");
SimpleGrantedAuthority admin_role = new SimpleGrantedAuthority("admin");
List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
list.add(user_role);
list.add(admin_role);
return new org.springframework.security.core.userdetails.User(user.getUsername()
,user.getPassword()
, list);
}
}
- 上述代码中,返回的UserDetails或者是User都是框架提供的类,我们在项目开发的过程中,很多需求都是我们自定义的属性,我们可以自定义一个类,来实现UserDetails,在自己定义的类中,就可以扩展自己想要的内容,如下代码:
package com.zzyl.security.entity;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author sjqn
* @date 2023/9/1
*/
@Data
public class UserAuth implements UserDetails {
private String username; //固定不可更改
private String password;//固定不可更改
private String nickName; //扩展属性 昵称
private List<String> roles; //角色列表,这里用的是String集合,下面get方法中需要转换
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(roles

最低0.47元/天 解锁文章
1919

被折叠的 条评论
为什么被折叠?



