当前位置: 首页 > news >正文

Java 项目 HTTP+WebSocket 统一权限控制实战

在现代 Java 后端项目中,HTTP 接口作为同步通信的核心载体,WebSocket 作为实时双向通信的关键技术,两者共存已成常态。无论是电商平台的实时订单推送、社交应用的即时聊天,还是物联网系统的设备数据同步,都离不开这两种通信方式的配合。

但权限控制作为系统安全的第一道防线,在混合通信场景下却容易出现 “断层”:HTTP 接口的权限校验可以依赖成熟的 Spring Security 拦截器,而 WebSocket 的长连接特性让传统的请求级校验机制失效;若分别设计两套权限体系,不仅开发维护成本高,还可能导致权限规则不一致、安全漏洞等问题。

本文将从底层逻辑出发,结合 RBAC 权限模型,搭建一套 “认证统一、授权统一、鉴权灵活” 的权限控制架构,同时覆盖 HTTP 接口和 WebSocket 通信的全场景。所有示例基于 JDK 17、Spring Boot 3.2.5 实现,代码严格遵循《阿里巴巴 Java 开发手册(嵩山版)》,可直接复制到项目中运行。

一、核心挑战:HTTP 与 WebSocket 的权限控制差异

要做好混合场景的权限控制,首先要明确两者的本质差异 —— 通信模式的不同直接决定了权限校验的时机、方式和难点。

1.1 核心差异对比

特性HTTP 接口WebSocket
通信模式短连接、请求 - 响应模式长连接、双向全双工通信
连接生命周期单次请求建立,响应后断开一次握手建立,持续保持到主动关闭
权限校验时机每次请求均可独立校验仅握手阶段可拦截,后续消息无天然拦截点
身份传递方式请求头(Authorization)、Cookie 等握手请求头、URL 参数、Token 协商
状态维护无状态(依赖 Cookie/Token 维持身份)有状态(连接建立后保持会话)
鉴权核心难点批量接口的权限规则统一管理连接建立后的消息级权限校验、Token 过期处理

1.2 权限控制流程差异

HTTP 接口权限控制流程
WebSocket 权限控制流程

从流程可以看出:HTTP 的权限控制是 “请求级” 的,每一次请求都要经过完整校验;而 WebSocket 是 “连接级 + 消息级” 的双重校验 —— 连接建立时的认证决定是否允许接入,消息传输时的鉴权决定是否允许操作。

二、基础架构设计:统一权限控制体系

要解决两套通信方式的权限协同问题,核心是搭建 “统一认证、统一授权、分场景鉴权” 的架构。整体设计遵循以下原则:

  1. 认证统一:HTTP 和 WebSocket 共用一套身份认证机制(基于 JWT),避免重复开发;
  2. 授权统一:基于 RBAC 模型(用户 - 角色 - 权限),权限规则集中管理,支持接口级、功能级权限控制;
  3. 鉴权灵活:HTTP 采用 “拦截器 + 注解” 鉴权,WebSocket 采用 “握手拦截 + 消息拦截” 鉴权,适配各自通信特性;
  4. 状态可控:长连接的用户身份、权限信息与连接绑定,支持动态权限刷新、连接销毁时的资源释放。

2.1 整体架构图

2.2 核心技术选型

技术组件版本号作用
JDK17基础开发环境
Spring Boot3.2.5项目基础框架
Spring Security6.2.4安全框架,提供 HTTP 权限控制支持
Spring WebSocket6.2.4WebSocket 通信支持
MyBatis-Plus3.5.5持久层框架,简化数据库操作
MySQL8.0.36权限数据存储数据库
Redis7.2.4权限缓存、Token 黑名单存储
JWT(io.jsonwebtoken)0.11.5无状态身份认证
Lombok1.18.30简化 Java 代码编写
FastJSON22.0.49JSON 序列化 / 反序列化
Swagger3(SpringDoc)2.3.0接口文档自动生成
Guava33.2.0集合工具类支持

三、基础环境搭建:数据库与依赖配置

3.1 数据库设计(RBAC 模型)

基于 RBAC(Role-Based Access Control)角色基础访问控制模型,设计 5 张核心表,覆盖用户、角色、权限的全关联关系。

3.1.1 数据库表 SQL(MySQL 8.0)
-- 用户表
CREATE TABLE `sys_user` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',`username` varchar(50) NOT NULL COMMENT '用户名',`password` varchar(100) NOT NULL COMMENT '加密后的密码(BCrypt)',`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用,1-正常',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';-- 角色表
CREATE TABLE `sys_role` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',`role_name` varchar(50) NOT NULL COMMENT '角色名称',`role_code` varchar(50) NOT NULL COMMENT '角色编码',`remark` varchar(200) DEFAULT NULL COMMENT '角色描述',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_role_code` (`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统角色表';-- 权限表(资源表)
CREATE TABLE `sys_permission` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '权限ID',`perm_name` varchar(50) NOT NULL COMMENT '权限名称',`perm_code` varchar(100) NOT NULL COMMENT '权限编码(如:sys:user:list)',`resource_type` varchar(20) NOT NULL COMMENT '资源类型:HTTP-HTTP接口,WS-WebSocket消息',`resource_path` varchar(200) NOT NULL COMMENT '资源路径(HTTP接口URL/WS消息类型)',`remark` varchar(200) DEFAULT NULL COMMENT '权限描述',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_perm_code` (`perm_code`),KEY `idx_resource_type` (`resource_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统权限表';-- 用户-角色关联表
CREATE TABLE `sys_user_role` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '关联ID',`user_id` bigint NOT NULL COMMENT '用户ID',`role_id` bigint NOT NULL COMMENT '角色ID',PRIMARY KEY (`id`),UNIQUE KEY `uk_user_role` (`user_id`,`role_id`),KEY `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户-角色关联表';-- 角色-权限关联表
CREATE TABLE `sys_role_permission` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '关联ID',`role_id` bigint NOT NULL COMMENT '角色ID',`perm_id` bigint NOT NULL COMMENT '权限ID',PRIMARY KEY (`id`),UNIQUE KEY `uk_role_perm` (`role_id`,`perm_id`),KEY `idx_perm_id` (`perm_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色-权限关联表';-- 初始化测试数据
INSERT INTO `sys_user` (`username`, `password`, `nickname`) VALUES 
('admin', '$2a$10$eCwW6fN9x7w9G4Z7y8V6u5t4s3r2q1p0o9n8m7l6k5j4i3h2g1f', '系统管理员'),
('test', '$2a$10$eCwW6fN9x7w9G4Z7y8V6u5t4s3r2q1p0o9n8m7l6k5j4i3h2g1f', '测试用户');INSERT INTO `sys_role` (`role_name`, `role_code`, `remark`) VALUES 
('超级管理员', 'ADMIN', '拥有所有权限'),
('普通用户', 'USER', '仅拥有基础权限');INSERT INTO `sys_permission` (`perm_name`, `perm_code`, `resource_type`, `resource_path`, `remark`) VALUES 
('查询用户列表', 'sys:user:list', 'HTTP', '/api/v1/users', 'HTTP接口-查询用户列表'),
('发送WebSocket消息', 'sys:ws:send', 'WS', 'SEND_MSG', 'WebSocket消息-发送消息'),
('接收WebSocket通知', 'sys:ws:receive', 'WS', 'RECEIVE_NOTIFY', 'WebSocket消息-接收通知');INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES 
(1, 1), -- admin关联ADMIN角色
(2, 2); -- test关联USER角色INSERT INTO `sys_role_permission` (`role_id`, `perm_id`) VALUES 
(1, 1), (1, 2), (1, 3), -- ADMIN角色拥有所有权限
(2, 3); -- USER角色仅拥有接收通知权限
3.1.2 表设计说明
  • 权限表(sys_permission)通过resource_type区分 HTTP 和 WebSocket 资源,resource_path对应具体的接口 URL 或消息类型;
  • 密码采用 BCrypt 加密(Spring Security 默认加密方式),测试数据的密码统一为123456
  • 所有关联表都有唯一索引,避免重复关联;
  • 索引设计优化查询效率,尤其是权限校验时的高频查询。

3.2 Maven 依赖配置(pom.xml)

<?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>3.2.5</version><relativePath/></parent><groupId>com.ken</groupId><artifactId>http-ws-auth-demo</artifactId><version>0.0.1-SNAPSHOT</version><name>http-ws-auth-demo</name><description>HTTP+WebSocket统一权限控制示例</description><properties><java.version>17</java.version><mybatis-plus.version>3.5.5</mybatis-plus.version><jwt.version>0.11.5</jwt.version><fastjson2.version>2.0.49</fastjson2.version><springdoc.version>2.3.0</springdoc.version><guava.version>33.2.0</guava.version></properties><dependencies><!-- Spring Boot核心依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</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-data-redis</artifactId></dependency><!-- 数据库相关 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus.version}</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- 工具类 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version><scope>provided</scope></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>${guava.version}</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>${jwt.version}</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>${jwt.version}</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>${jwt.version}</version><scope>runtime</scope></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>${fastjson2.version}</version></dependency><!-- 接口文档 --><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>${springdoc.version}</version></dependency><!-- 测试依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>
</project>

3.3 核心配置文件(application.yml)

spring:# 数据库配置datasource:url: jdbc:mysql://localhost:3306/auth_demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driver# Redis配置redis:host: localhostport: 6379password:database: 0timeout: 3000ms# MyBatis-Plus配置
mybatis-plus:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.ken.auth.entityconfiguration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:id-type: autologic-delete-field: isDeletedlogic-delete-value: 1logic-not-delete-value: 0# JWT配置
jwt:secret: 78a8854d-3b7e-455a-89a9-7c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0dexpiration: 3600000 # Token有效期1小时(毫秒)refresh-expiration: 86400000 # 刷新Token有效期24小时header: Authorizationprefix: Bearer# 服务器配置
server:port: 8080servlet:context-path: /# SpringDoc(Swagger3)配置
springdoc:api-docs:path: /api-docsswagger-ui:path: /swagger-ui.htmloperationsSorter: methodpackages-to-scan: com.ken.auth.controller

四、核心组件开发:统一认证与授权

4.1 实体类设计

4.1.1 用户实体(SysUser.java)
package com.ken.auth.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;/*** 系统用户实体* @author ken*/
@Data
@TableName("sys_user")
public class SysUser {/*** 用户ID*/@TableId(type = IdType.AUTO)private Long id;/*** 用户名*/private String username;/*** 密码(BCrypt加密)*/private String password;/*** 昵称*/private String nickname;/*** 状态:0-禁用,1-正常*/private Integer status;/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;
}
4.1.2 角色实体(SysRole.java)
package com.ken.auth.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;/*** 系统角色实体* @author ken*/
@Data
@TableName("sys_role")
public class SysRole {/*** 角色ID*/@TableId(type = IdType.AUTO)private Long id;/*** 角色名称*/private String roleName;/*** 角色编码*/private String roleCode;/*** 角色描述*/private String remark;/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;
}
4.1.3 权限实体(SysPermission.java)
package com.ken.auth.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;/*** 系统权限实体* @author ken*/
@Data
@TableName("sys_permission")
public class SysPermission {/*** 权限ID*/@TableId(type = IdType.AUTO)private Long id;/*** 权限名称*/private String permName;/*** 权限编码*/private String permCode;/*** 资源类型:HTTP-HTTP接口,WS-WebSocket消息*/private String resourceType;/*** 资源路径(HTTP接口URL/WS消息类型)*/private String resourcePath;/*** 权限描述*/private String remark;/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;
}
4.1.4 用户角色关联实体(SysUserRole.java)
package com.ken.auth.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;/*** 用户-角色关联实体* @author ken*/
@Data
@TableName("sys_user_role")
public class SysUserRole {/*** 关联ID*/@TableId(type = IdType.AUTO)private Long id;/*** 用户ID*/private Long userId;/*** 角色ID*/private Long roleId;
}
4.1.5 角色权限关联实体(SysRolePermission.java)
package com.ken.auth.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;/*** 角色-权限关联实体* @author ken*/
@Data
@TableName("sys_role_permission")
public class SysRolePermission {/*** 关联ID*/@TableId(type = IdType.AUTO)private Long id;/*** 角色ID*/private Long roleId;/*** 权限ID*/private Long permId;
}
4.1.6 自定义用户详情(LoginUser.java)

用于 Spring Security 和 JWT 中存储用户身份与权限信息:

package com.ken.auth.entity;import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Set;/*** 登录用户详情* @author ken*/
@Data
public class LoginUser implements UserDetails {/*** 用户ID*/private Long userId;/*** 用户名*/private String username;/*** 密码*/private String password;/*** 昵称*/private String nickname;/*** 角色编码集合*/private Set<String> roleCodes;/*** 权限编码集合*/private Set<String> permCodes;/*** 账户状态:true-正常,false-禁用*/private boolean enabled;/*** Spring Security权限集合*/private Collection<? extends GrantedAuthority> authorities;@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}
}

4.2 JWT 工具类(JwtUtils.java)

提供 Token 生成、校验、解析等核心功能,是统一认证的基础:

package com.ken.auth.utils;import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson2.JSON;
import com.ken.auth.entity.LoginUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;import javax.crypto.SecretKey;
import java.util.Collection;
import java.util.Date;
import java.util.Set;
import java.util.stream.Collectors;/*** JWT工具类* @author ken*/
@Slf4j
@Component
public class JwtUtils {/*** JWT密钥(至少32位)*/@Value("${jwt.secret}")private String secret;/*** Token有效期(毫秒)*/@Value("${jwt.expiration}")private long expiration;/*** Token请求头*/@Value("${jwt.header}")private String header;/*** Token前缀*/@Value("${jwt.prefix}")private String prefix;/*** 生成Token* @param authentication 认证信息* @return JWT Token*/public String generateToken(Authentication authentication) {LoginUser loginUser = (LoginUser) authentication.getPrincipal();SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());// 构建JWT Tokenreturn Jwts.builder()// 存入用户ID.claim("userId", loginUser.getUserId())// 存入用户名.claim("username", loginUser.getUsername())// 存入昵称.claim("nickname", loginUser.getNickname())// 存入角色编码.claim("roleCodes", JSON.toJSONString(loginUser.getRoleCodes()))// 存入权限编码.claim("permCodes", JSON.toJSONString(loginUser.getPermCodes()))// 签发时间.setIssuedAt(new Date())// 过期时间.setExpiration(new Date(System.currentTimeMillis() + expiration))// 签名算法.signWith(key, SignatureAlgorithm.HS256).compact();}/*** 从Token中获取用户信息* @param token JWT Token* @return 登录用户详情*/public LoginUser getLoginUser(String token) {Claims claims = parseClaims(token);LoginUser loginUser = new LoginUser();// 解析用户IDloginUser.setUserId(claims.get("userId", Long.class));// 解析用户名loginUser.setUsername(claims.get("username", String.class));// 解析昵称loginUser.setNickname(claims.get("nickname", String.class));// 解析角色编码(JSON字符串转Set)String roleCodesJson = claims.get("roleCodes", String.class);Set<String> roleCodes = JSON.parseObject(roleCodesJson, Set.class);loginUser.setRoleCodes(roleCodes);// 解析权限编码(JSON字符串转Set)String permCodesJson = claims.get("permCodes", String.class);Set<String> permCodes = JSON.parseObject(permCodesJson, Set.class);loginUser.setPermCodes(permCodes);// 设置权限(Spring Security需要)Collection<GrantedAuthority> authorities = permCodes.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());loginUser.setAuthorities(authorities);// 设置账户状态为正常loginUser.setEnabled(true);return loginUser;}/*** 验证Token有效性* @param token JWT Token* @return true-有效,false-无效*/public boolean validateToken(String token) {try {SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);return true;} catch (MalformedJwtException e) {log.error("无效的JWT Token:{}", e.getMessage());} catch (ExpiredJwtException e) {log.error("JWT Token已过期:{}", e.getMessage());} catch (UnsupportedJwtException e) {log.error("不支持的JWT Token:{}", e.getMessage());} catch (IllegalArgumentException e) {log.error("JWT Token为空:{}", e.getMessage());} catch (Exception e) {log.error("JWT Token验证失败:{}", e.getMessage());}return false;}/*** 解析Token中的Claims* @param token JWT Token* @return Claims对象*/private Claims parseClaims(String token) {SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());try {return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();} catch (ExpiredJwtException e) {return e.getClaims();}}/*** 从请求头中提取Token* @param authHeader 请求头中的Authorization值* @return 纯Token字符串(去除前缀)*/public String extractToken(String authHeader) {if (StringUtils.hasText(authHeader) && authHeader.startsWith(prefix)) {return authHeader.substring(prefix.length() + 1);}return null;}/*** 获取Token剩余有效期(秒)* @param token JWT Token* @return 剩余秒数,过期返回0*/public long getRemainingTime(String token) {try {Claims claims = parseClaims(token);Date expiration = claims.getExpiration();long remaining = expiration.getTime() - System.currentTimeMillis();return Math.max(0, remaining / 1000);} catch (Exception e) {return 0;}}
}

4.3 数据访问层(Mapper)

基于 MyBatis-Plus 实现,简化数据库操作:

4.3.1 SysUserMapper.java
package com.ken.auth.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ken.auth.entity.SysUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.Set;/*** 用户Mapper* @author ken*/
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {/*** 根据用户名查询用户* @param username 用户名* @return 用户实体*/SysUser selectByUsername(@Param("username") String username);/*** 根据用户ID查询角色编码* @param userId 用户ID* @return 角色编码集合*/Set<String> selectRoleCodesByUserId(@Param("userId") Long userId);/*** 根据用户ID查询权限编码* @param userId 用户ID* @return 权限编码集合*/Set<String> selectPermCodesByUserId(@Param("userId") Long userId);
}
4.3.2 SysUserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ken.auth.mapper.SysUserMapper"><select id="selectByUsername" resultType="com.ken.auth.entity.SysUser">SELECT id, username, password, nickname, status, create_time, update_timeFROM sys_userWHERE username = #{username}</select><select id="selectRoleCodesByUserId" resultType="java.lang.String">SELECT sr.role_codeFROM sys_role srJOIN sys_user_role sur ON sr.id = sur.role_idWHERE sur.user_id = #{userId}</select><select id="selectPermCodesByUserId" resultType="java.lang.String">SELECT sp.perm_codeFROM sys_permission spJOIN sys_role_permission srp ON sp.id = srp.perm_idJOIN sys_user_role sur ON srp.role_id = sur.role_idWHERE sur.user_id = #{userId}</select></mapper>

其他 Mapper(SysRoleMapper、SysPermissionMapper 等)仅需继承 BaseMapper,无需额外方法,MyBatis-Plus 自动提供 CRUD 功能:

package com.ken.auth.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ken.auth.entity.SysRole;
import org.apache.ibatis.annotations.Mapper;/*** 角色Mapper* @author ken*/
@Mapper
public interface SysRoleMapper extends BaseMapper<SysRole> {
}

4.4 服务层实现

4.4.1 用户服务(SysUserService.java)
package com.ken.auth.service;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Sets;
import com.ken.auth.entity.LoginUser;
import com.ken.auth.entity.SysUser;
import com.ken.auth.mapper.SysUserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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 org.springframework.util.ObjectUtils;import java.util.Set;/*** 用户服务实现* @author ken*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SysUserService extends ServiceImpl<SysUserMapper, SysUser> implements UserDetailsService {private final SysUserMapper sysUserMapper;/*** 根据用户名查询用户详情(Spring Security认证用)* @param username 用户名* @return 用户详情* @throws UsernameNotFoundException 用户名不存在异常*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 查询用户信息SysUser sysUser = sysUserMapper.selectByUsername(username);if (ObjectUtils.isEmpty(sysUser)) {log.error("用户名[{}]不存在", username);throw new UsernameNotFoundException("用户名或密码错误");}// 校验用户状态if (sysUser.getStatus() != 1) {log.error("用户[{}]已被禁用", username);throw new UsernameNotFoundException("用户已被禁用");}// 查询用户角色编码Set<String> roleCodes = sysUserMapper.selectRoleCodesByUserId(sysUser.getId());if (ObjectUtils.isEmpty(roleCodes)) {roleCodes = Sets.newHashSet();}// 查询用户权限编码Set<String> permCodes = sysUserMapper.selectPermCodesByUserId(sysUser.getId());if (ObjectUtils.isEmpty(permCodes)) {permCodes = Sets.newHashSet();}// 构建LoginUser对象LoginUser loginUser = new LoginUser();loginUser.setUserId(sysUser.getId());loginUser.setUsername(sysUser.getUsername());loginUser.setPassword(sysUser.getPassword());loginUser.setNickname(sysUser.getNickname());loginUser.setRoleCodes(roleCodes);loginUser.setPermCodes(permCodes);loginUser.setEnabled(true);return loginUser;}/*** 根据用户ID查询用户权限编码* @param userId 用户ID* @return 权限编码集合*/public Set<String> getUserPermCodes(Long userId) {return sysUserMapper.selectPermCodesByUserId(userId);}
}
4.4.2 权限校验服务(PermissionService.java)

核心服务,提供统一的权限校验逻辑,供 HTTP 和 WebSocket 调用:

package com.ken.auth.service;import com.google.common.collect.Sets;
import com.ken.auth.entity.SysPermission;
import com.ken.auth.mapper.SysPermissionMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;import java.util.Set;/*** 权限校验服务* @author ken*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PermissionService {private final SysPermissionMapper sysPermissionMapper;private final SysUserService sysUserService;/*** 校验用户是否拥有指定权限* @param userId 用户ID* @param permCode 权限编码* @return true-有权限,false-无权限*/public boolean hasPermission(Long userId, String permCode) {if (ObjectUtils.isEmpty(userId) || permCode == null) {return false;}// 查询用户拥有的权限Set<String> userPermCodes = sysUserService.getUserPermCodes(userId);if (CollectionUtils.isEmpty(userPermCodes)) {return false;}// 超级管理员拥有所有权限(角色编码为ADMIN)Set<String> roleCodes = sysUserService.getById(userId).getRoleCodes();if (roleCodes.contains("ADMIN")) {return true;}// 校验权限return userPermCodes.contains(permCode);}/*** 根据资源类型和路径获取权限编码* @param resourceType 资源类型(HTTP/WS)* @param resourcePath 资源路径(URL/消息类型)* @return 权限编码,无匹配返回null*/@Cacheable(value = "permCache", key = "#resourceType + ':' + #resourcePath")public String getPermCodeByResource(String resourceType, String resourcePath) {if (ObjectUtils.isEmpty(resourceType) || ObjectUtils.isEmpty(resourcePath)) {return null;}// 构造查询条件SysPermission query = new SysPermission();query.setResourceType(resourceType);query.setResourcePath(resourcePath);// 查询权限(MyBatis-Plus条件查询)SysPermission permission = sysPermissionMapper.selectOne(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>(query));return ObjectUtils.isEmpty(permission) ? null : permission.getPermCode();}/*** 校验HTTP接口权限* @param userId 用户ID* @param requestUrl 请求URL* @return true-有权限,false-无权限*/public boolean checkHttpPermission(Long userId, String requestUrl) {// 获取接口对应的权限编码String permCode = getPermCodeByResource("HTTP", requestUrl);if (permCode == null) {// 无对应权限配置,默认允许访问(可根据业务调整为拒绝)return true;}// 校验权限return hasPermission(userId, permCode);}/*** 校验WebSocket消息权限* @param userId 用户ID* @param msgType 消息类型* @return true-有权限,false-无权限*/public boolean checkWsPermission(Long userId, String msgType) {// 获取消息对应的权限编码String permCode = getPermCodeByResource("WS", msgType);if (permCode == null) {// 无对应权限配置,默认拒绝访问(WebSocket消息建议严格控制)return false;}// 校验权限return hasPermission(userId, permCode);}
}

4.5 Spring Security 配置(SecurityConfig.java)

配置 HTTP 接口的认证逻辑,禁用默认表单登录,启用 JWT 认证:

package com.ken.auth.config;import com.ken.auth.filter.JwtAuthenticationFilter;
import com.ken.auth.handler.AccessDeniedHandlerImpl;
import com.ken.auth.handler.AuthenticationEntryPointImpl;
import com.ken.auth.service.SysUserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;/*** Spring Security配置* @author ken*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // 启用方法级权限注解
@RequiredArgsConstructor
public class SecurityConfig {private final SysUserService sysUserService;private final JwtAuthenticationFilter jwtAuthenticationFilter;private final AuthenticationEntryPointImpl authenticationEntryPoint;private final AccessDeniedHandlerImpl accessDeniedHandler;/*** 密码加密器* @return BCryptPasswordEncoder*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 认证提供者* @return DaoAuthenticationProvider*/@Beanpublic DaoAuthenticationProvider authenticationProvider() {DaoAuthenticationProvider provider = new DaoAuthenticationProvider();// 设置用户详情服务provider.setUserDetailsService(sysUserService);// 设置密码加密器provider.setPasswordEncoder(passwordEncoder());return provider;}/*** 认证管理器* @param config 认证配置* @return AuthenticationManager* @throws Exception 异常*/@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}/*** 安全过滤器链* @param http HttpSecurity* @return SecurityFilterChain* @throws Exception 异常*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http// 禁用CSRF(前后端分离项目无需CSRF保护).csrf(csrf -> csrf.disable())// 禁用会话(JWT无状态认证).sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))// 配置请求授权规则.authorizeHttpRequests(auth -> auth// 放行登录接口.requestMatchers("/api/v1/auth/login").permitAll()// 放行Swagger3接口.requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/api-docs/**").permitAll()// 其他所有请求需要认证.anyRequest().authenticated())// 配置异常处理器.exceptionHandling(ex -> ex// 未认证异常处理器.authenticationEntryPoint(authenticationEntryPoint)// 未授权异常处理器.accessDeniedHandler(accessDeniedHandler))// 添加JWT认证过滤器(在用户名密码认证过滤器之前).addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);// 设置认证提供者http.authenticationProvider(authenticationProvider());return http.build();}
}

4.6 异常处理器

4.6.1 未认证异常处理器(AuthenticationEntryPointImpl.java)
package com.ken.auth.handler;import com.alibaba.fastjson2.JSON;
import com.ken.auth.common.Result;
import com.ken.auth.common.ResultCode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;import java.io.IOException;
import java.io.PrintWriter;/*** 未认证异常处理器(401)* @author ken*/
@Slf4j
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {log.error("未认证:{}", authException.getMessage());// 设置响应头response.setContentType("application/json;charset=utf-8");response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);// 构建响应结果Result<?> result = Result.error(ResultCode.UNAUTHORIZED, "用户未登录或Token失效");// 输出响应PrintWriter out = response.getWriter();out.write(JSON.toJSONString(result));out.flush();out.close();}
}
4.6.2 未授权异常处理器(AccessDeniedHandlerImpl.java)
package com.ken.auth.handler;import com.alibaba.fastjson2.JSON;
import com.ken.auth.common.Result;
import com.ken.auth.common.ResultCode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;import java.io.IOException;
import java.io.PrintWriter;/*** 未授权异常处理器(403)* @author ken*/
@Slf4j
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {log.error("未授权:{}", accessDeniedException.getMessage());// 设置响应头response.setContentType("application/json;charset=utf-8");response.setStatus(HttpServletResponse.SC_FORBIDDEN);// 构建响应结果Result<?> result = Result.error(ResultCode.FORBIDDEN, "无权限访问");// 输出响应PrintWriter out = response.getWriter();out.write(JSON.toJSONString(result));out.flush();out.close();}
}

4.7 通用结果类

4.7.1 结果码枚举(ResultCode.java)
package com.ken.auth.common;/*** 结果码枚举* @author ken*/
public enum ResultCode {SUCCESS(200, "操作成功"),ERROR(500, "操作失败"),UNAUTHORIZED(401, "未认证"),FORBIDDEN(403, "未授权"),NOT_FOUND(404, "资源不存在");private final int code;private final String msg;ResultCode(int code, String msg) {this.code = code;this.msg = msg;}public int getCode() {return code;}public String getMsg() {return msg;}
}
4.7.2 响应结果类(Result.java)
package com.ken.auth.common;import lombok.Data;import java.io.Serializable;/*** 统一响应结果* @author ken*/
@Data
public class Result<T> implements Serializable {private static final long serialVersionUID = 1L;/*** 响应码*/private int code;/*** 响应信息*/private String msg;/*** 响应数据*/private T data;/*** 成功响应(无数据)* @param <T> 数据类型* @return Result<T>*/public static <T> Result<T> success() {return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), null);}/*** 成功响应(带数据)* @param data 响应数据* @param <T> 数据类型* @return Result<T>*/public static <T> Result<T> success(T data) {return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), data);}/*** 错误响应* @param code 响应码* @param msg 响应信息* @param <T> 数据类型* @return Result<T>*/public static <T> Result<T> error(int code, String msg) {return new Result<>(code, msg, null);}/*** 错误响应(基于ResultCode)* @param resultCode 结果码枚举* @param <T> 数据类型* @return Result<T>*/public static <T> Result<T> error(ResultCode resultCode) {return new Result<>(resultCode.getCode(), resultCode.getMsg(), null);}/*** 错误响应(基于ResultCode,自定义消息)* @param resultCode 结果码枚举* @param msg 自定义消息* @param <T> 数据类型* @return Result<T>*/public static <T> Result<T> error(ResultCode resultCode, String msg) {return new Result<>(resultCode.getCode(), msg, null);}
}

五、HTTP 接口权限控制实现

HTTP 接口的权限控制采用 “拦截器认证 + 注解鉴权” 的双重机制:拦截器负责验证 Token 有效性、解析用户身份;注解(@PreAuthorize)负责细粒度的权限校验。

5.1 JWT 认证拦截器(JwtAuthenticationFilter.java)

package com.ken.auth.filter;import com.ken.auth.entity.LoginUser;
import com.ken.auth.utils.JwtUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;import java.io.IOException;/*** JWT认证拦截器* 每次HTTP请求都会经过该拦截器,验证Token并设置认证信息* @author ken*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {private final JwtUtils jwtUtils;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {try {// 从请求头中获取TokenString authHeader = request.getHeader(jwtUtils.getHeader());String token = jwtUtils.extractToken(authHeader);// Token不为空且未认证if (StringUtils.hasText(token) && SecurityContextHolder.getContext().getAuthentication() == null) {// 验证Token有效性if (jwtUtils.validateToken(token)) {// 解析用户信息LoginUser loginUser = jwtUtils.getLoginUser(token);// 构建认证TokenUsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());// 设置请求详情authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));// 将认证信息存入SecurityContextSecurityContextHolder.getContext().setAuthentication(authenticationToken);log.info("用户[{}]认证通过,请求URL:{}", loginUser.getUsername(), request.getRequestURI());}}} catch (Exception e) {log.error("JWT认证失败:{}", e.getMessage());}// 继续执行过滤链filterChain.doFilter(request, response);}
}

5.2 接口权限注解使用示例

5.2.1 认证控制器(AuthController.java)

提供登录接口,生成 JWT Token:

package com.ken.auth.controller;import com.ken.auth.common.Result;
import com.ken.auth.entity.LoginUser;
import com.ken.auth.utils.JwtUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.util.StringUtils;import java.util.HashMap;
import java.util.Map;/*** 认证控制器* @author ken*/
@Slf4j
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
@Tag(name = "认证接口", description = "用户登录、Token刷新等接口")
public class AuthController {private final AuthenticationManager authenticationManager;private final JwtUtils jwtUtils;/*** 登录请求参数*/@lombok.Datapublic static class LoginRequest {private String username;private String password;}/*** 登录响应结果*/@lombok.Datapublic static class LoginResponse {private String token;private Long userId;private String username;private String nickname;}/*** 用户登录* @param loginRequest 登录参数(用户名、密码)* @return 登录结果(包含Token)*/@PostMapping("/login")@Operation(summary = "用户登录", description = "输入用户名和密码,获取JWT Token")public Result<LoginResponse> login(@RequestBody LoginRequest loginRequest) {// 校验参数if (!StringUtils.hasText(loginRequest.getUsername())) {return Result.error(400, "用户名不能为空");}if (!StringUtils.hasText(loginRequest.getPassword())) {return Result.error(400, "密码不能为空");}try {// 构建认证TokenUsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());// 执行认证(调用SysUserService.loadUserByUsername)Authentication authentication = authenticationManager.authenticate(authenticationToken);// 将认证信息存入SecurityContextSecurityContextHolder.getContext().setAuthentication(authentication);// 生成JWT TokenLoginUser loginUser = (LoginUser) authentication.getPrincipal();String token = jwtUtils.generateToken(authentication);// 构建响应结果LoginResponse loginResponse = new LoginResponse();loginResponse.setToken(token);loginResponse.setUserId(loginUser.getUserId());loginResponse.setUsername(loginUser.getUsername());loginResponse.setNickname(loginUser.getNickname());log.info("用户[{}]登录成功", loginUser.getUsername());return Result.success(loginResponse);} catch (Exception e) {log.error("用户[{}]登录失败:{}", loginRequest.getUsername(), e.getMessage());return Result.error(401, "用户名或密码错误");}}
}
5.2.2 用户控制器(UserController.java)

使用@PreAuthorize注解实现接口级权限控制:

package com.ken.auth.controller;import com.ken.auth.common.Result;
import com.ken.auth.entity.LoginUser;
import com.ken.auth.entity.SysUser;
import com.ken.auth.service.SysUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;/*** 用户控制器* @author ken*/
@Slf4j
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Tag(name = "用户接口", description = "用户查询、修改等接口,包含权限控制示例")
public class UserController {private final SysUserService sysUserService;/*** 查询用户列表(需要sys:user:list权限)* @return 用户列表*/@GetMapping@Operation(summary = "查询用户列表", description = "需要sys:user:list权限才能访问")@PreAuthorize("hasPermission('', 'sys:user:list')") // 权限注解,校验sys:user:list权限public Result<List<SysUser>> listUsers() {// 获取当前登录用户信息LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();log.info("用户[{}]查询用户列表", loginUser.getUsername());// 查询所有用户(实际业务中应添加分页)List<SysUser> userList = sysUserService.list();return Result.success(userList);}/*** 获取当前登录用户信息(无需特定权限)* @return 当前用户信息*/@GetMapping("/current")@Operation(summary = "获取当前登录用户信息", description = "所有已登录用户均可访问")public Result<LoginUser> getCurrentUser() {LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();log.info("用户[{}]查询当前登录信息", loginUser.getUsername());return Result.success(loginUser);}
}

5.3 权限注解说明

  • @PreAuthorize("hasPermission('', 'sys:user:list')"):方法执行前校验权限,hasPermission是自定义的权限表达式(需配置);
  • 若需要角色校验,可使用@PreAuthorize("hasRole('ADMIN')")@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
  • 权限表达式支持复杂逻辑,如@PreAuthorize("hasPermission('', 'sys:user:list') and hasRole('ADMIN')")

5.4 自定义权限表达式配置(PermissionConfig.java)

package com.ken.auth.config;import com.ken.auth.entity.LoginUser;
import com.ken.auth.service.PermissionService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;/*** 权限表达式配置* 自定义hasPermission表达式,支持权限校验* @author ken*/
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class PermissionConfig {private final PermissionService permissionService;/*** 自定义方法安全表达式处理器* @return MethodSecurityExpressionHandler*/@Beanpublic MethodSecurityExpressionHandler methodSecurityExpressionHandler() {DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler() {@Overrideprotected PermissionEvaluationContext createEvaluationContextInternal(Authentication auth, Object target) {PermissionEvaluationContext context = new PermissionEvaluationContext(auth, target);// 注入权限服务context.setPermissionService(permissionService);return context;}};return handler;}/*** 自定义权限评估上下文*/public static class PermissionEvaluationContext extends org.springframework.security.access.expression.method.MethodSecurityEvaluationContext {private PermissionService permissionService;public PermissionEvaluationContext(Authentication authentication, Object target) {super(authentication, target);}public void setPermissionService(PermissionService permissionService) {this.permissionService = permissionService;}/*** 自定义hasPermission方法,供@PreAuthorize注解使用* @param target 目标资源(可空)* @param permCode 权限编码* @return true-有权限,false-无权限*/public boolean hasPermission(Object target, String permCode) {Authentication auth = SecurityContextHolder.getContext().getAuthentication();if (auth == null || !(auth.getPrincipal() instanceof LoginUser loginUser)) {return false;}// 调用权限服务校验权限return permissionService.hasPermission(loginUser.getUserId(), permCode);}}
}

六、WebSocket 权限控制实现

WebSocket 的权限控制需解决两个核心问题:连接建立时的身份认证消息交互时的权限校验。由于 WebSocket 是长连接,传统的请求级拦截机制不再适用,需通过握手拦截器和消息拦截器实现全流程控制。

6.1 WebSocket 核心配置(WebSocketConfig.java)

package com.ken.auth.config;import com.ken.auth.interceptor.WsHandshakeInterceptor;
import com.ken.auth.interceptor.WsMessageInterceptor;
import com.ken.auth.handler.WsMessageHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;/*** WebSocket 配置类* 注册 WebSocket 处理器和拦截器,配置连接路径* @author ken*/
@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {private final WsMessageHandler wsMessageHandler;private final WsHandshakeInterceptor wsHandshakeInterceptor;private final WsMessageInterceptor wsMessageInterceptor;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {// 注册 WebSocket 端点,允许客户端通过 /ws 路径连接registry.addHandler(wsMessageHandler, "/ws")// 允许跨域请求(生产环境需限制具体域名).setAllowedOrigins("*")// 添加握手拦截器(连接建立前的认证).addInterceptors(wsHandshakeInterceptor)// 支持 SockJS 降级(兼容不支持 WebSocket 的浏览器).withSockJS();// 注册消息拦截器(所有消息都会经过该拦截器)registry.addHandler(wsMessageHandler, "/ws").addInterceptors(wsMessageInterceptor);}
}

6.2 握手拦截器(WsHandshakeInterceptor.java)

握手拦截器在 WebSocket 连接建立前(HTTP 升级为 WebSocket 协议的握手阶段)进行拦截,验证用户身份并绑定到会话中。

package com.ken.auth.interceptor;import com.ken.auth.entity.LoginUser;
import com.ken.auth.utils.JwtUtils;
import com.ken.auth.manager.WsSessionManager;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;import java.util.Map;/*** WebSocket 握手拦截器* 在连接建立前验证 Token,解析用户信息并绑定到会话* @author ken*/
@Slf4j
@Component
@RequiredArgsConstructor
public class WsHandshakeInterceptor implements HandshakeInterceptor {private final JwtUtils jwtUtils;private final WsSessionManager wsSessionManager;/*** 握手前拦截(核心认证逻辑)* @param request 握手请求* @param response 握手响应* @param handler WebSocket 处理器* @param attributes 会话属性(可存储用户信息)* @return true-允许握手,false-拒绝握手*/@Overridepublic boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,WebSocketHandler handler, Map<String, Object> attributes) {try {// 从请求中提取 Token(支持请求头或 URL 参数)String token = extractToken(request);if (token == null) {log.warn("WebSocket 握手失败:未携带 Token");return false;}// 验证 Token 有效性if (!jwtUtils.validateToken(token)) {log.warn("WebSocket 握手失败:Token 无效或已过期");return false;}// 解析用户信息LoginUser loginUser = jwtUtils.getLoginUser(token);if (loginUser == null) {log.warn("WebSocket 握手失败:用户信息解析失败");return false;}// 将用户信息存入会话属性(后续消息处理可获取)attributes.put("loginUser", loginUser);log.info("用户[{}] WebSocket 握手认证通过", loginUser.getUsername());return true;} catch (Exception e) {log.error("WebSocket 握手异常:{}", e.getMessage());return false;}}/*** 握手后处理(通常无需操作)*/@Overridepublic void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,WebSocketHandler handler, Exception exception) {// 从会话属性中获取用户信息ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;Map<String, Object> attributes = servletRequest.getServletRequest().getAttribute("javax.websocket.server.ServerEndpointConfig").getUserProperties();LoginUser loginUser = (LoginUser) attributes.get("loginUser");if (loginUser != null) {// 记录连接建立日志log.info("用户[{}] WebSocket 连接已建立", loginUser.getUsername());}}/*** 从请求中提取 Token(优先从请求头,其次从 URL 参数)* @param request 握手请求* @return Token 字符串或 null*/private String extractToken(ServerHttpRequest request) {// 尝试从请求头获取String token = jwtUtils.extractToken(request.getHeaders().getFirst(jwtUtils.getHeader()));if (token != null) {return token;}// 尝试从 URL 参数获取(格式:ws://localhost:8080/ws?token=xxx)if (request instanceof ServletServerHttpRequest servletRequest) {return servletRequest.getServletRequest().getParameter("token");}return null;}
}

6.3 WebSocket 会话管理器(WsSessionManager.java)

管理所有活跃的 WebSocket 会话,实现用户与会话的绑定,方便后续消息推送和权限验证。

package com.ken.auth.manager;import com.ken.auth.entity.LoginUser;
import jakarta.websocket.Session;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;/*** WebSocket 会话管理器* 存储用户与 WebSocket 会话的映射关系,支持会话的添加、移除和查询* @author ken*/
@Slf4j
@Component
public class WsSessionManager {/*** 用户会话映射(userId -> Session)* 使用 ConcurrentHashMap 保证线程安全*/private final Map<Long, Session> userSessionMap = new ConcurrentHashMap<>();/*** 添加会话(连接建立时调用)* @param loginUser 用户信息* @param session WebSocket 会话*/public void addSession(LoginUser loginUser, Session session) {if (loginUser == null || session == null) {return;}// 存储会话,并添加关闭监听器(会话关闭时自动移除)userSessionMap.put(loginUser.getUserId(), session);session.addMessageHandler(session1 -> removeSession(loginUser.getUserId()));log.info("用户[{}]的 WebSocket 会话已添加,当前在线用户数:{}",loginUser.getUsername(), userSessionMap.size());}/*** 移除会话(连接关闭时调用)* @param userId 用户ID*/public void removeSession(Long userId) {if (userId == null) {return;}Session session = userSessionMap.remove(userId);if (session != null) {log.info("用户[{}]的 WebSocket 会话已移除,当前在线用户数:{}",userId, userSessionMap.size());}}/*** 根据用户ID获取会话* @param userId 用户ID* @return WebSocket 会话或 null*/public Session getSession(Long userId) {return userId == null ? null : userSessionMap.get(userId);}/*** 获取所有在线用户ID* @return 用户ID集合*/public Set<Long> getOnlineUserIds() {return userSessionMap.keySet();}/*** 获取所有在线用户的会话* @return 会话集合*/public Set<Session> getAllSessions() {return Set.copyOf(userSessionMap.values());}/*** 判断用户是否在线* @param userId 用户ID* @return true-在线,false-离线*/public boolean isOnline(Long userId) {return userId != null && userSessionMap.containsKey(userId);}
}

6.4 消息模型(WsMessage.java)

定义 WebSocket 消息的统一格式,包含消息类型、内容、发送者等核心字段。

package com.ken.auth.entity;import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;import java.time.LocalDateTime;/*** WebSocket 消息模型* 统一消息格式,便于序列化和解析* @author ken*/
@Data
public class WsMessage {/*** 消息ID(唯一标识)*/private String messageId;/*** 消息类型(与权限表的 resource_path 对应)* 例如:SEND_MSG-发送消息,RECEIVE_NOTIFY-接收通知*/private String msgType;/*** 消息内容(JSON格式字符串)*/private String content;/*** 发送者用户ID*/@JSONField(serialize = false) // 序列化时忽略(由服务端填充)private Long senderId;/*** 发送者用户名*/@JSONField(serialize = false)private String senderName;/*** 接收者用户ID(null表示广播)*/private Long receiverId;/*** 消息发送时间*/@JSONField(format = "yyyy-MM-dd HH:mm:ss")private LocalDateTime sendTime;
}

6.5 消息拦截器(WsMessageInterceptor.java)

在消息发送和接收时进行拦截,校验用户是否有权限操作该类型的消息,是 WebSocket 权限控制的核心环节。

package com.ken.auth.interceptor;import com.alibaba.fastjson2.JSON;
import com.ken.auth.entity.LoginUser;
import com.ken.auth.entity.WsMessage;
import com.ken.auth.service.PermissionService;
import jakarta.websocket.Session;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;import java.io.IOException;/*** WebSocket 消息拦截器* 拦截所有 WebSocket 消息,校验消息级权限* @author ken*/
@Slf4j
@Component
@RequiredArgsConstructor
public class WsMessageInterceptor extends TextWebSocketHandler {private final PermissionService permissionService;/*** 消息接收前拦截(核心鉴权逻辑)* @param session WebSocket 会话* @param message 接收的消息* @throws Exception 异常*/@Overridepublic void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {try {// 从会话中获取用户信息(握手阶段已存入)LoginUser loginUser = (LoginUser) session.getAttributes().get("loginUser");if (loginUser == null) {log.warn("WebSocket 消息拦截:未获取到用户信息,拒绝处理");session.close(CloseStatus.POLICY_VIOLATION.withReason("未认证"));return;}// 解析消息内容String payload = message.getPayload();WsMessage wsMessage = JSON.parseObject(payload, WsMessage.class);if (wsMessage == null || !org.springframework.util.StringUtils.hasText(wsMessage.getMsgType())) {log.warn("用户[{}]发送无效消息:{}", loginUser.getUsername(), payload);session.sendMessage(new TextMessage(JSON.toJSONString("消息格式错误:缺少msgType")));return;}// 校验消息权限(调用统一权限服务)boolean hasPermission = permissionService.checkWsPermission(loginUser.getUserId(), wsMessage.getMsgType());if (!hasPermission) {log.warn("用户[{}]无权限发送消息类型[{}]", loginUser.getUsername(), wsMessage.getMsgType());session.sendMessage(new TextMessage(JSON.toJSONString("无权限操作该类型消息")));session.close(CloseStatus.POLICY_VIOLATION.withReason("无权限"));return;}// 权限校验通过,继续处理消息(调用实际处理器)super.handleTextMessage(session, message);} catch (Exception e) {log.error("WebSocket 消息拦截异常:{}", e.getMessage());session.close(CloseStatus.SERVER_ERROR.withReason("处理消息失败"));}}/*** 连接关闭时处理*/@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) {LoginUser loginUser = (LoginUser) session.getAttributes().get("loginUser");if (loginUser != null) {log.info("用户[{}]的 WebSocket 连接已关闭,原因:{}", loginUser.getUsername(), status.getReason());}}
}

6.6 消息处理器(WsMessageHandler.java)

负责实际的消息处理逻辑,根据消息类型分发处理,如发送私信、广播通知等。

package com.ken.auth.handler;import com.alibaba.fastjson2.JSON;
import com.ken.auth.entity.LoginUser;
import com.ken.auth.entity.WsMessage;
import com.ken.auth.manager.WsSessionManager;
import jakarta.websocket.Session;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;import java.io.IOException;
import java.time.LocalDateTime;
import java.util.UUID;/*** WebSocket 消息处理器* 处理实际的消息逻辑,如发送消息、广播通知等* @author ken*/
@Slf4j
@Component
@RequiredArgsConstructor
public class WsMessageHandler extends TextWebSocketHandler {private final WsSessionManager wsSessionManager;/*** 连接建立时调用(注册会话)*/@Overridepublic void afterConnectionEstablished(WebSocketSession session) {// 从会话中获取用户信息LoginUser loginUser = (LoginUser) session.getAttributes().get("loginUser");if (loginUser != null) {// 将会话添加到管理器wsSessionManager.addSession(loginUser, session);try {// 发送连接成功消息WsMessage welcomeMsg = new WsMessage();welcomeMsg.setMessageId(UUID.randomUUID().toString());welcomeMsg.setMsgType("CONNECT_SUCCESS");welcomeMsg.setContent("WebSocket 连接成功,当前在线用户数:" + wsSessionManager.getOnlineUserIds().size());welcomeMsg.setSendTime(LocalDateTime.now());session.sendMessage(new TextMessage(JSON.toJSONString(welcomeMsg)));} catch (IOException e) {log.error("发送连接成功消息失败:{}", e.getMessage());}}}/*** 处理接收到的消息*/@Overridepublic void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {LoginUser loginUser = (LoginUser) session.getAttributes().get("loginUser");if (loginUser == null) {return;}// 解析消息WsMessage wsMessage = JSON.parseObject(message.getPayload(), WsMessage.class);wsMessage.setSenderId(loginUser.getUserId());wsMessage.setSenderName(loginUser.getUsername());wsMessage.setSendTime(LocalDateTime.now());if (wsMessage.getMessageId() == null) {wsMessage.setMessageId(UUID.randomUUID().toString());}log.info("用户[{}]发送消息:{}", loginUser.getUsername(), JSON.toJSONString(wsMessage));// 根据消息类型处理switch (wsMessage.getMsgType()) {case "SEND_MSG":handleSendMessage(wsMessage);break;case "RECEIVE_NOTIFY":handleReceiveNotify(wsMessage, session);break;default:session.sendMessage(new TextMessage(JSON.toJSONString("不支持的消息类型:" + wsMessage.getMsgType())));}}/*** 处理发送消息(单聊)* @param message 消息对象*/private void handleSendMessage(WsMessage message) throws IOException {Long receiverId = message.getReceiverId();if (receiverId == null) {throw new IllegalArgumentException("接收者ID不能为空");}// 获取接收者会话WebSocketSession receiverSession = (WebSocketSession) wsSessionManager.getSession(receiverId);if (receiverSession == null || !receiverSession.isOpen()) {// 接收者不在线,可根据业务逻辑处理(如存入离线消息表)WebSocketSession senderSession = (WebSocketSession) wsSessionManager.getSession(message.getSenderId());senderSession.sendMessage(new TextMessage(JSON.toJSONString("用户[ID:" + receiverId + "]不在线")));return;}// 发送消息给接收者receiverSession.sendMessage(new TextMessage(JSON.toJSONString(message)));// 发送成功回执给发送者WebSocketSession senderSession = (WebSocketSession) wsSessionManager.getSession(message.getSenderId());WsMessage ackMsg = new WsMessage();ackMsg.setMessageId(UUID.randomUUID().toString());ackMsg.setMsgType("SEND_ACK");ackMsg.setContent("消息已发送给用户[ID:" + receiverId + "]");ackMsg.setSendTime(LocalDateTime.now());senderSession.sendMessage(new TextMessage(JSON.toJSONString(ackMsg)));}/*** 处理接收通知(广播)* @param message 消息对象* @param session 发送者会话*/private void handleReceiveNotify(WsMessage message, WebSocketSession session) throws IOException {// 广播消息给所有在线用户(除发送者自己)for (Session targetSession : wsSessionManager.getAllSessions()) {if (!targetSession.getId().equals(session.getId()) && targetSession.isOpen()) {targetSession.getBasicRemote().sendText(JSON.toJSONString(message));}}// 发送广播成功回执WsMessage ackMsg = new WsMessage();ackMsg.setMessageId(UUID.randomUUID().toString());ackMsg.setMsgType("BROADCAST_ACK");ackMsg.setContent("通知已广播给所有在线用户");ackMsg.setSendTime(LocalDateTime.now());session.sendMessage(new TextMessage(JSON.toJSONString(ackMsg)));}/*** 连接关闭时清理资源*/@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) {LoginUser loginUser = (LoginUser) session.getAttributes().get("loginUser");if (loginUser != null) {wsSessionManager.removeSession(loginUser.getUserId());}}
}

七、完整测试与验证

为验证权限控制效果,我们通过实际测试场景对比不同用户的权限表现,确保 HTTP 和 WebSocket 权限控制生效。

7.1 测试准备

  1. 启动服务:确保 MySQL、Redis 正常运行,启动 Spring Boot 应用;
  2. 获取 Token
    • 使用 admin 用户登录(用户名:admin,密码:123456),获取 Token;
    • 使用 test 用户登录(用户名:test,密码:123456),获取 Token。

7.2 HTTP 接口权限测试

7.2.1 测试接口:查询用户列表(/api/v1/users)
  • 权限要求:需要 sys:user:list 权限(仅 admin 拥有)。
  • 测试步骤
    1. 使用 admin 的 Token 调用接口,预期返回 200 成功;
    2. 使用 test 的 Token 调用接口,预期返回 403 无权限。
7.2.2 测试接口:获取当前用户信息(/api/v1/users/current)
  • 权限要求:无需特定权限,已登录用户均可访问。
  • 测试步骤
    1. 使用 admin 或 test 的 Token 调用接口,预期均返回 200 成功。

7.3 WebSocket 权限测试

使用 WebSocket 客户端工具(如 wscat)连接测试:

7.3.1 连接认证测试
# 使用无效 Token 连接(预期失败)
wscat -c "ws://localhost:8080/ws?token=invalid"
# 输出:连接被拒绝(握手失败)# 使用 admin 的 Token 连接(预期成功)
wscat -c "ws://localhost:8080/ws?token=admin_token_here"
# 输出:收到 "WebSocket 连接成功" 消息# 使用 test 的 Token 连接(预期成功)
wscat -c "ws://localhost:8080/ws?token=test_token_here"
# 输出:收到 "WebSocket 连接成功" 消息
7.3.2 消息权限测试
  • 发送消息(SEND_MSG):需要 sys:ws:send 权限(仅 admin 拥有)。

    json

    // admin 发送消息(预期成功)
    {"msgType": "SEND_MSG","content": "Hello","receiverId": 2
    }
    // 输出:收到 "消息已发送" 回执// test 发送消息(预期失败)
    {"msgType": "SEND_MSG","content": "Hi","receiverId": 1
    }
    // 输出:收到 "无权限" 消息,连接被关闭
    
  • 接收通知(RECEIVE_NOTIFY):需要 sys:ws:receive 权限(admin 和 test 均拥有)。

    // admin 发送广播(预期成功)
    {"msgType": "RECEIVE_NOTIFY","content": "系统通知:服务器将重启"
    }
    // 输出:test 客户端收到广播消息// test 发送广播(预期成功)
    {"msgType": "RECEIVE_NOTIFY","content": "我上线了"
    }
    // 输出:admin 客户端收到广播消息
    

八、进阶优化与注意事项

8.1 性能优化

  1. 权限缓存:通过 Redis 缓存用户权限(@Cacheable 注解已实现),减少数据库查询;
  2. 会话管理:使用 ConcurrentHashMap 存储会话,确保线程安全的同时提升查询效率;
  3. 批量操作:WebSocket 广播时采用批量发送,减少 I/O 次数。

8.2 安全增强

  1. Token 黑名单:维护已注销但未过期的 Token 黑名单,防止被盗用;
    // Redis 存储黑名单,key: blacklist:{token}, value: 过期时间
    public boolean isTokenBlacklisted(String token) {return redisTemplate.hasKey("blacklist:" + token);
    }
    
  2. 消息加密:对敏感消息内容进行加密传输(如 AES 加密);
  3. 频率限制:限制单位时间内的消息发送次数,防止恶意攻击。

8.3 动态权限刷新

当用户权限发生变化时,需实时更新缓存和会话中的权限信息:

/*** 刷新用户权限(权限变更时调用)* @param userId 用户ID*/
public void refreshUserPermission(Long userId) {// 清除权限缓存redisTemplate.delete("permCache::user:" + userId);// 重新加载权限Set<String> newPerms = sysUserService.getUserPermCodes(userId);// 更新会话中的权限信息WebSocketSession session = (WebSocketSession) wsSessionManager.getSession(userId);if (session != null) {LoginUser loginUser = (LoginUser) session.getAttributes().get("loginUser");loginUser.setPermCodes(newPerms);}
}

8.4 常见问题与解决方案

  1. WebSocket 连接超时

    • 原因:服务器或负载均衡器对长连接有超时限制;
    • 解决:客户端定期发送心跳消息(如每 30 秒发送一次 PING 类型消息)。
  2. 权限缓存不一致

    • 原因:权限变更后缓存未及时更新;
    • 解决:权限修改接口添加缓存清理逻辑,或设置合理的缓存过期时间。
  3. 大量连接导致内存溢出

    • 原因:会话未及时清理,积累过多;
    • 解决:设置连接最大空闲时间,超时自动关闭;定期检查并清理无效会话。

九、总结

本文基于 RBAC 模型,构建了一套同时支持 HTTP 和 WebSocket 的统一权限控制体系,核心要点包括:

  1. 认证统一:通过 JWT 实现两种通信方式的身份认证,避免重复开发;
  2. 授权统一:基于用户 - 角色 - 权限关系,集中管理权限规则;
  3. 鉴权适配:HTTP 采用拦截器 + 注解鉴权,WebSocket 采用握手拦截 + 消息拦截鉴权,适配各自通信特性;
  4. 实战验证:通过完整代码示例和测试场景,验证了权限控制的有效性。

在实际项目中,可根据业务需求扩展权限粒度(如数据级权限),或集成 OAuth2.0/OpenID Connect 实现更复杂的认证场景。权限控制是系统安全的基石,合理设计并严格执行,才能有效防范未授权访问风险。

http://www.dtcms.com/a/576839.html

相关文章:

  • Tomcat日志配置与优化指南
  • 技术演进中的开发沉思-174 java-EJB:分布式通信
  • HarmonyOS实战项目:AI健康助手(影像识别与健康分析)
  • 利用 AWS Lambda 与 EventBridge 优化低频 Java 作业的云计算成本
  • 工业和信息化部网站备案管理系统公司网站维护怎么维护
  • 深入理解 Spring Boot 中的 Redis 缓存集成:从基础配置到高可用实践
  • 辽宁网站建站优化公司怎么在网上做装修网站
  • 界面控件Telerik UI for WPF 2025 Q3亮点 - 集成AI编码助手
  • 拦截adb install/uninstall安装 - 安装流程分析
  • 【小技巧】PyCharm建立项目,VScode+CodeX+WindowsPowerShell开发Python pyQT6
  • DevExpress WPF中文教程:Data Grid - 如何使用虚拟源?(五)
  • AI SQL助手本地搭建(附源码)
  • Zabbix企业级分布式监控系统(下)
  • 『Linux升级路』解析环境变量
  • 浏览器能正常访问URL获取JSON,但是pycharm里调不通
  • AI代码开发宝库系列:PDF文档解析MinerU
  • 校园招聘seo行业网
  • 开发网站的技术路线博达高校网站群建设教程
  • 物联网运维中基于联邦学习的跨设备隐私保护与协同优化技术
  • 物联网AI模组:连接与智能的融合
  • 【底层机制】ART虚拟机深度解析:Android运行时的架构革命
  • 嵌入式硬件:如何理解高频电子线路,从入门开始
  • 物联网赋能校园共享站:打造24小时一站式服务新体验!
  • 萤石开放平台申请物联网卡指南
  • 矩阵在密码学的应用——希尔密码详解
  • 20251106给荣品RD-RK3588-MID开发板跑Rockchip的原厂Android13系统时适配AP6275P模块的WIFI【使用荣品的DTS】
  • 学校网站代码模板成都的网站建设开发公司哪家好
  • 怎么制作自己的小网站低代码前端开发平台
  • Elastic Stack 或 ELK —— 日志管理与数据分析方案
  • 手机、平板、电脑如何投屏画面到电视?ToDesk远程控制TV版教程分享