前言
权限系统设计基本上是所有项目中都会涉及的一个重要部分。通过权限系统,我们将对用户角色、功能模块访问进行限制,从而保证系统安全性。本文将介绍中大型项目中常用的一套权限系统设计方案,通过 SpringSecurity 安全管理框架,并结合 RBAC 模型进行数据模型设计,可以完成一个较为完整的权限系统。
Spring Security
Spring Security 是 Spring 中的安全管理框架,Spring Security 为 Java 应用提供了身份认证(Authentication)和访问控制(Authorization)功能。
Spring Security 的核心特点
-
与 Spring 生态高度集成。
-
支持丰富的认证方式,可扩展性强。
-
灵活授权模型,将角色(Role)与权限(Authority)分离。
-
强大的安全防护。
使用方法
(使用方法介绍将在实践中进行超详细介绍,请继续阅读)
RBAC
Role-Based Access Control,RBAC 模型是一种静态权限模型,RBAC 模型为用户添加一个或多个角色类型,通过角色类型这一中间层,将用户和权限解耦,以便于进行权限控制。在 RBAC 模型中,每个角色下面对应着不同的权限组,通过创建不同的角色,将权限进行划分。
RBAC 中的重要角色
-
用户(User):系统的使用者。
-
角色(Role):代表一系列权限的集合。
-
权限(Permission):具体操作的访问能力。
4 级 RBAC 模型
RBAC0
RBAC 中最基础的模型,只包含定义中的 用户-角色-权限 基本组件。
RBAC1
角色继承模型,基于 RBAC0 添加层级关系,可以让一个角色继承另一个或一些角色的权限。
层级关系种类很多,基础的包含:树形结构、网格结构(多继承)。
RBAC2
约束规则,基于 RBAC0 添加角色的约束关系。
约束规则包括:角色互斥(两角色不能同时赋给一个用户)、基数约束(角色赋给用户的数量有限)等。
RBAC3
RBAC 完整模型,结合了 RBAC1 和 RBAC2,同时拥有层级关系和约束规则。
RBAC 模型的优缺点
优点
-
通过对用户和权限进行解耦,便于进行权限集中控制。
-
结构性强,通过应用 RBAC1 模型,便于控制层级关系间的权限控制。
缺点
-
权限颗粒度较粗,根据角色划分而非功能。
-
角色爆炸(Role Proliferation),因为需求权限颗粒度较细,从而导致创建大量单一权限角色用于单一功能控制导致角色数量暴增。
-
缺乏灵活性,项目初期定义的角色可能后续不满足项目需求。
基于 Spring Security + RBAC 模型的权限系统设计实践
实践场景说明
各个系统中均离不开用户管理模块,下面根据用户管理模块中的权限校验系统提出以下需求:
-
动态分配用户权限,可以选择让用户调用哪些 api。
-
设定权限集合,简化后续给予同类用户权限操作。
-
权限高度定制化,可容纳毫无关联的 api 在统一权限集合内。
实践场景解析
实践场景要求我们基于用户模块去做一套完整的权限校验系统,要求权限可以高度定制化。因此,我们可以考虑采用本文前半节所阐述的技术方案 Spring Security + RBAC 模型。通过使用 Spring Security 进行用户身份认证和权限校验操作,应用 RBAC 模型进行数据模型抽象处理,适应 Spring Security 中对于权限校验的要求。
技术选型
名称 | 版本 | 说明 |
---|---|---|
Spring Boot | 3.1.5 | Spring Boot 主版本 |
JDK | 17 | 项目使用的 Java 版本 |
Spring Boot Starter Web | 3.1.5 | Web 应用支持 |
Spring Boot Starter Security | 3.1.5 | 安全框架支持 |
MyBatis-Plus | 3.5.4 | MyBatis 增强框架 |
MySQL | 8.0.33 | MySQL 数据库驱动 |
Lombok | 1.18.28 | 代码简化工具 |
Knife4j | 4.3.0 | API 文档生成工具 |
JJWT API | 0.11.5 | JWT 令牌生成与解析 |
JJWT Impl | 0.11.5 | JWT 核心实现 |
JJWT Jackson | 0.11.5 | JWT 的 JSON 处理支持 |
实施步骤
1、数据库设计
-
sys_user:用户表,用于存储用户信息。
-
sys_role:角色表,用于存储角色信息。
-
sys_permission:系统权限表,用于存储系统中权限。
-
user_role:用户角色关联表。
-
role_permission:角色权限关联表。
2、编写 Demo 框架
demo 框架不做详细解释,只介绍包与包下文件作用,后文会对重点代码进行详细解读(详细实现请查看文末源码链接)。
java.djhhh.securityrbacdemo
-
controller(业务控制层)
-
UserController:用于暴露 User 相关接口。
-
-
mapper(数据访问层)
-
RoleMapper:用于进行 Role 数据访问。
-
UserMapper:用于进行 User 数据访问。
-
-
service(服务层)
-
impl(实现)
-
UserServiceImpl:用户服务实现。
-
-
UserService:用户服务接口。
-
-
model(数据模型)
-
dto(数据传输对象)
-
UserDTO:用于登录数据封装。
-
-
entity(实体)
-
User:用户实体。
-
Permission:权限实体。
-
Role:角色实体。
-
-
vo(视图)
-
UserVO:用户视图
-
UserPermissinVO:用户权限视图。
-
-
-
result(结果模型)
-
Result:结果类封装,用于全局返回值统一。
-
ResultCodeEnum:结果枚举值。
-
-
security(安全框架)
-
permissionconstants(权限常量)
-
UserPerssionConstant:用户权限模块
-
-
JwtAuthenticationFilter:JWT 校验过滤器。
-
SecurityConfig:Spring Security 安全配置类
-
-
utils(工具)
-
JwtTokenUtil:JWT 工具类。
-
-
SecurityRbacDemoApplication:项目启动类。
resources
-
application.yml:配置文件。
3、配置文件编写
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/authentication-system-demo?useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: roothikari:maximum-pool-size: 20minimum-idle: 5security:user:name: adminpassword: 123456mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: truespringdoc:swagger-ui:path: /swagger-ui.htmltags-sorter: alphaoperations-sorter: alphaapi-docs:path: /v3/api-docsdefault-consumes-media-type: application/jsondefault-produces-media-type: application/jsonknife4j:enable: truesetting:language: zh-CNenable-footer: falsejwt:secret: pKQzTRvVpla2baIvNh6oWNrBWRuCidWiexpiration: 216000
4、梳理 Spring Security 实现流程
-
实现 User 实体类,让 User 类继承 UserDetails 类,并重写内部方法以便后续 Spring Security 进行权限校验使用。
-
实现 UserServcie,让 UserService 类实现 UserDetailsService 类,实现 loadUserByUsername 方法。
-
实现 SecurityConfig 配置类,内部实现安全过滤器配置,完成权限校验基本实现。
-
Spring Security中两个特性:
-
用户认证已经通过配置项实现,可以自行进行用户认证授权操作。
-
访问控制还未实现,现在用户已经知道自己可以进行什么操作,该步骤应实现访问控制。
-
5、实现 User 实体类
-
getAuthorities:该方法是 User 中的重点方法,用作获取用户的权限列表。由于本系统采用的模型为 RBAC 模型,所以需要通过在 role 中再拿一次 permission。
-
(其余看注释即可)
java">@Data
@TableName("sys_user")
public class User implements UserDetails {@TableId(type = IdType.AUTO)private Long id;private String username;private String password;private Boolean enabled;@TableField(exist = false)private List<Role> roles;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return roles.stream().map(Role::getPermissions) // 获取每个角色的权限列表.filter(Objects::nonNull) // 过滤掉 permissions 为 null 的角色.flatMap(Collection::stream) // 展开所有权限对象.filter(Objects::nonNull) // 过滤掉 null 的权限对象.map(Permission::getCode) // 提取权限码.filter(code -> code != null && !code.isBlank()) // 过滤空权限码.map(SimpleGrantedAuthority::new) // 转换为 Spring Security 权限对象.collect(Collectors.toList());}@Overridepublic String getPassword() {return this.password;}@Overridepublic String getUsername() {return this.username;}/*** 用户是否过期*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** 用户是否锁定*/@Overridepublic boolean isAccountNonLocked() {return true;}/*** 用户凭证是否过期*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 用户是否启用*/@Overridepublic boolean isEnabled() {return this.enabled;}
}
6、实现 UserServcie 服务
-
loadUserByUsername:该方法为 UserService 中重点要实现的方法,该方法是 Spring Security 用户认证的核心实现,负责根据用户名加载用户信息以及关联的角色和权限。
java">@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {@Resourceprivate final UserMapper userMapper;@Resourceprivate final RoleMapper roleMapper;/*** UserDetailsService 类的 loadUserByUsername 方法是 Spring Security 用户认证的核心实现。* 负责根据用户名加载用户信息及其关联的角色和权限。* @param username 用户名* @return 用户*/@Overridepublic User loadUserByUsername(String username) {User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));if (user == null) throw new UsernameNotFoundException("用户不存在");List<Role> roles = userMapper.selectRolesByUserId(user.getId());user.setRoles(roles != null ? roles : Collections.emptyList());user.getRoles().forEach(role -> {List<Permission> permissions = roleMapper.selectPermissionsByRoleId(role.getId());role.setPermissions(permissions != null ? permissions : Collections.emptyList());});return user;}//(其余代码略...)
}
7、实现 SecurityConfig 配置类
该配置类为该方案的核心,需要仔细研究(代码中含详细注解)。
-
passwordEncoder:用于密码加密。
-
jwtAuthenticationFilter:引入 JWT 校验链。
-
filterChain:安全过滤器链配置(该配置项的核心,下面进行详细介绍)。
-
.authorizeHttpRequests:请求授权规则配置。
-
.requestMatchers:配置无需认证的白名单。
-
.permitAll():调用后用于通过 requestMatchers 进行的白名单配置
-
.anyRequest().authenticated():声明其余请求要进行权限校验。
-
-
formLogin:登录配置(示例中使用自定义登录配置,给定配置以提醒此处可用)。
-
logout:登出配置(示例中未实现,扩展中会给出实现思路)。
-
sessionManagement:将 Session 设置为无状态。
-
addFilterBefore(jwtAuthenticationFilter):添加 JWT 校验。
-
expectionHandling:异常处理。
-
authenticationEntryPoint:未认证处理。
-
accessDenieHandler:权限不足处理。
-
-
cors:跨域配置。
-
csrf:CSRF 防护。
-
java">/*** Spring Security 安全配置类*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {private final JwtTokenUtil jwtTokenUtil;private final UserDetailsService userDetailsService;@Autowiredpublic SecurityConfig(JwtTokenUtil jwtTokenUtil, UserDetailsService userDetailsService) {this.jwtTokenUtil = jwtTokenUtil;this.userDetailsService = userDetailsService;}private final ObjectMapper objectMapper = new ObjectMapper();/*** 密码编码器 Bean* 使用 BCrypt 强哈希算法加密密码*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** JWT链引入* @return*/@Beanpublic JwtAuthenticationFilter jwtAuthenticationFilter() {return new JwtAuthenticationFilter(jwtTokenUtil, userDetailsService);}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}/*** 安全过滤器链配置(核心安全规则)*/@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http//--- 请求授权规则 ---.authorizeHttpRequests(auth -> auth// 白名单路径(无需认证).requestMatchers("/index.html","/user/logout", //注册API"/user/login", //注册API"/user/register", //注册API"/doc.html", // Swagger 文档页"/webjars/**", // Swagger WebJars 资源"/v3/api-docs/**", // OpenAPI 文档端点"/swagger-ui/**", // Swagger UI 资源"/swagger-resources/**", // Swagger 资源配置"/favicon.ico", // 网站图标"/login.html", // 自定义登录页(根据实际路径调整)"/css/**", // CSS 静态资源"/js/**" // JavaScript 静态资源).permitAll()// 其他所有请求需要认证.anyRequest().authenticated())//--- 表单登录配置 ---// 如果不适用JWT的话 可以参照此方案实施
// .formLogin(form -> form
// .defaultSuccessUrl("/index.html")
// .loginProcessingUrl("/user/login") // 表单提交地址
// // 登录成功处理(可跳转页面或返回 JSON)
// .successHandler((request, response, authentication) -> {
// // 示例:返回 JSON 响应(适合前后端分离)
// response.setContentType("application/json;charset=UTF-8");
// response.getWriter().write(
// objectMapper.writeValueAsString(Result.ok("登录成功"))
// );
// })
// // 登录失败处理
// .failureHandler((request, response, exception) -> {
// String errorMessage;
// if (exception instanceof BadCredentialsException) {
// errorMessage = "密码错误";
// } else if (exception instanceof UsernameNotFoundException) {
// errorMessage = "用户不存在";
// } else {
// errorMessage = "登录失败";
// }
// response.setContentType("application/json;charset=UTF-8");
// response.setStatus(401);
// response.getWriter().write(
// objectMapper.writeValueAsString(Result.fail(401, errorMessage))
// );
// })
// .permitAll() // 允许所有人访问登录页
// )//--- 注销配置 ---
// .logout(logout -> logout
// .logoutUrl("/user/logout") // 注销请求地址
// )//--- 会话管理 ---.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态)//--- 添加JWT校验 ---.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)//--- 异常处理 ---.exceptionHandling(exceptions -> exceptions// 未认证处理(访问需要登录的资源).authenticationEntryPoint((request, response, ex) -> {response.setContentType("application/json;charset=UTF-8");response.setStatus(401);response.getWriter().write(objectMapper.writeValueAsString(Result.fail(401, "请先登录")));})// 权限不足处理.accessDeniedHandler((request, response, ex) -> {response.setContentType("application/json;charset=UTF-8");response.setStatus(403);response.getWriter().write(objectMapper.writeValueAsString(Result.fail(403, "权限不足")));}))//--- CORS 跨域配置(按需启用)---.cors(cors -> cors.configurationSource(corsConfigurationSource()))//--- CSRF 防护(传统 Web 应用建议启用)---.csrf(AbstractHttpConfigurer::disable); // 禁用 CSRFreturn http.build();}/*** CORS 跨域配置(生产环境应缩小范围)*/@Beanpublic CorsConfigurationSource corsConfigurationSource() {CorsConfiguration config = new CorsConfiguration();config.setAllowedOrigins(List.of("http://localhost:8080")); // 允许的前端地址config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));config.setAllowCredentials(true); // 允许携带 Cookieconfig.setAllowedHeaders(List.of("*"));UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", config);return source;}
}
8、访问控制
通过在方法上添加下面示例中的注解,即可进行自动进行访问控制。
(permission替换为你在sys_permissions设定的权限。)
java">@PreAuthorize("hasAuthority('permission')")
测试
Demo中引入knife4j用作API文档工具,用此进行测试。
登录
获取JWT令牌用作认证
获取用户权限列表
权限通过状态下:
若无权限状态下:
(此处测试只进行简单展示,具体还需要自己上手实践才能感受到)
扩展
对权限进行集中管理
通过对权限进行集中管理,既可以保证之后项目维护起来比较容易,又可以通过使用注解的方式,简化项目中的权限标识。
操作如下:
-
通过在sercurity包下创建一个permissionconstants包,用于集中管理权限。
-
在permissionconstants包下创建实例中的用户权限类,实现如下:
java">// 用户模块 public final class UserPermissionConstant {/*** 用户读权限*/@Target(ElementType.METHOD) // 仅用于方法@Retention(RetentionPolicy.RUNTIME)@PreAuthorize("hasAuthority('user:read')")public @interface UserRead {String description() default "用户读权限";}/*** 用户写权限*/@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@PreAuthorize("hasAuthority('user:write')")public @interface UserWrite {String description() default "用户写权限";} }
-
后续使用的时候只需要通过类似如下注解即可。
java">@UserPermissionConstant.UserRead
实现登出功能
JWT本身是无状态的。因此,如果我们想要使登录状态失效(也就是实现登出),本身上也是要违背一部分JWT无状态这一特性的。给出以下实现方案:
-
通过Redis添加黑名单机制,将登出的JWT存放在Redis中,增强对JWT的验证机制即可实现登出功能。
总结
本文介绍了一种权限校验的实现方式,通过Spring Security + RBAC模型进行权限系统设计,可以实现大部分应用场景下的权限校验功能。
Demo 代码 GitHub 源地址:https://github.com/Djhhhhhh/security-rbac-demo