权限管理模块

登录相关

权限管理模块(基础版)

RBAC(Role-Base Access Control,基于角色的访问控制):是权限管理的常用方案。

核心:通过用户 - 角色 - 权限的三层关联,灵活分配和管理权限。并不将用户和全新啊直接绑定。适合多用户多权限场景。

模块设计与实现

以Java + Spring Boot + Spring Security为例

1、实体设计(库表设计)

需要具备3个实体(用户、角色、权限),以及2个关系表(用户-角色、角色-权限)。

java 复制代码
// 用户实体
@Data
public class User {
    private Long id;
    private String username;
    private String password; // 加密存储(如BCrypt)
    private Integer status; // 1-启用,0-禁用
    // 关联角色(一对多,通过user_role表)
    private List<Role> roles;
}

// 角色实体
@Data
public class Role {
    private Long id;
    private String name; // 角色名称(如"系统管理员")
    private String code; // 角色标识(如"ROLE_ADMIN",用于Spring Security)
    // 关联权限(一对多,通过role_permission表)
    private List<Permission> permissions;
}

// 权限实体
@Data
public class Permission {
    private Long id;
    private String name; // 权限名称(如"删除用户")
    private String code; // 权限标识(如"user:delete",用于权限校验)
    private Integer type; // 1-菜单,2-按钮(接口)
    private String url; // 接口URL(如"/api/user/delete")
    private Long parentId; // 父权限ID(用于菜单层级)
}
sql 复制代码
-- 用户-角色关联表
CREATE TABLE `user_role` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL,
  `role_id` bigint NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_user` (`user_id`),
  KEY `idx_role` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 角色-权限关联表
CREATE TABLE `role_permission` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `role_id` bigint NOT NULL,
  `perm_id` bigint NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_role` (`role_id`),
  KEY `idx_perm` (`perm_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2、Mapper层设计,提供数据库基本操作。

java 复制代码
public interface UserMapper {
    // 查询用户
    User selectByUsername(String username);
    // 查询用户的角色
    List<Role> selectRolesByUserId(Long userId);
    // 查询角色的权限
    List<Permission> selectPermissionsByRoleId(Long roleId);
}
public Interface RolePermissionMapper() {
	// CRUD
}

3、Service实现,提供查询用户权限,用户与角色,角色与权限的分配与修改。

用户登录时,查询权限,用于后续权限校验。

java 复制代码
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    
    public List<String> login(String username,String password) {
    	// 校验逻辑,判断用户是否存在
    	return 用户信息(包含权限列表)
    }
    
    // 根据用户名查询用户及其关联的角色和权限
    public User getUserWithRolesAndPermissions(String username) {
        User user = userMapper.selectByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        // 查询用户关联的角色
        List<Role> roles = userMapper.selectRolesByUserId(user.getId());
        // 为每个角色查询关联的权限
        for (Role role : roles) {
            List<Permission> permissions = userMapper.selectPermissionsByRoleId(role.getId());
            role.setPermissions(permissions);
        }
        user.setRoles(roles);
        return user;
    }
}
// 管理用户-角色 角色-权限
@Service
public class RolePermissionService {
    @Autowired
    private RolePermissionMapper rolePermissionMapper;
    
    // 给角色分配权限(先删除旧关联,再插入新关联)
    @Transactional
    public void assignPermissions(Long roleId, List<Long> permIds) {
        // 1. 删除该角色已有的所有权限关联
        rolePermissionMapper.deleteByRoleId(roleId);
        // 2. 插入新的权限关联
        if (permIds != null && !permIds.isEmpty()) {
            List<RolePermission> list = permIds.stream()
                .map(permId -> {
                    RolePermission rp = new RolePermission();
                    rp.setRoleId(roleId);
                    rp.setPermId(permId);
                    return rp;
                }).collect(Collectors.toList());
            rolePermissionMapper.batchInsert(list);
        }
    }
}

3、权限校验(限制到接口层面)

结合Spring Security实现接口访问时单独权限校验。

原理:

  • 将用户权限加载到上下文中。并通过注解或配置文件进行拦截。
  • Spring Security的授权流程通过 http.authorizeRequests() 对web请求进行授权保护。授权决策由 AccessDecisionManager 进行,它会对比当前访问资源所需的权限信息和用户信息中的权限信息。

步骤:

  1. 加载用户到Spring Security:
    实现UserDetailsService,将用户信息(包含角色和权限)转换为Security可识别的userDetails。
    UserDetailsService:从数据库中取出用户的账号密码。
java 复制代码
@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserService userService;
    
    // 将用户信息以及对应的角色,权限交由Security管理
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户及其角色、权限
        User user = userService.getUserWithRolesAndPermissions(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        
        // 提取角色(需加前缀"ROLE_",符合Security规范)
        List<String> roleCodes = user.getRoles().stream()
            .map(role -> "ROLE_" + role.getCode())
            .collect(Collectors.toList());
        
        // 提取权限标识(如"user:delete")
        List<String> permCodes = user.getRoles().stream()
            .flatMap(role -> role.getPermissions().stream())
            .map(Permission::getCode)
            .collect(Collectors.toList());
        
        // 合并角色和权限(Security将其统一视为"权限")
        List<String> authorities = new ArrayList<>();
        authorities.addAll(roleCodes);
        authorities.addAll(permCodes);
        
        // 返回Security用户对象。提供参数由Security转换用户与权限
        return org.springframework.security.core.userdetails.User
            .withUsername(user.getUsername())
            .password(user.getPassword())
            .authorities(authorities) // 角色和权限都作为authority
            .accountLocked(user.getStatus() == 0) // 状态为0时锁定
            .build();
    }
}
  1. 权限校验
  • 注解方式校验:在Controller方法上使用@PerAuthorize注解,指定所需要的权限。
java 复制代码
@RestController
@RequestMapping("/api/user")
public class UserController {
    // 需拥有"user:delete"权限才能访问
    @PreAuthorize("hasAuthority('user:delete')")
    @DeleteMapping("/{id}")
    public Result deleteUser(@PathVariable Long id) {
        // 业务逻辑
        return Result.success();
    }
    
    // 需拥有"ROLE_ADMIN"角色才能访问
    @PreAuthorize("hasRole('ADMIN')") // 自动拼接"ROLE_"前缀
    @GetMapping("/list")
    public Result getUserList() {
        // 业务逻辑
        return Result.success();
    }
}
  • 配置列中通过URL匹配指定权限:
java 复制代码
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // 开启@PreAuthorize注解
public class SecurityConfig {
    @Autowired
    private CustomUserDetailsService userDetailsService;
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
            // 访问"/api/admin/**"需ADMIN角色
            .requestMatchers("/api/admin/**").hasRole("ADMIN")
            // 访问"/api/user/delete"需"user:delete"权限
            .requestMatchers("/api/user/delete/**").hasAuthority("user:delete")
            // 其他接口允许认证用户访问
            .anyRequest().authenticated()
        );
        return http.build();
    }
}

4、前端权限适配(动态加载)

根据用户权限展示菜单和按钮。

原理:

  • 登陆后,获取当前用户权限列表(后端返回)
  • 渲染菜单/按钮时,只显示用户权限内的菜单/按钮。
java 复制代码
// 1. 登录后获取用户权限
async function login(username, password) {
  const res = await api.login({ username, password });
  const { permissions } = res.data; // 后端返回的权限列表(如["user:delete", "menu:user"])
  localStorage.setItem('permissions', JSON.stringify(permissions));
}

// 2. 权限判断工具函数
function hasPermission(permission) {
  const permissions = JSON.parse(localStorage.getItem('permissions') || '[]');
  return permissions.includes(permission);
}

// 3. 动态渲染按钮(使用自定义指令)
Vue.directive('perm', {
  inserted(el, binding) {
    if (!hasPermission(binding.value)) {
      el.remove(); // 无权限则移除按钮
    }
  }
});

// 4. 在模板中使用
<template>
  <button v-perm="'user:delete'">删除用户</button>
</template>

优化点:

  • 角色继承:支持角色间的父子关系,需要在role表中添加Parent_id字段。
  • 数据权限:在功能权限基础上,控制数据可见范围(员工只能查看本人数据)。可通过在权限表中添加data_scope字段(限制全部/本部门/个人),结合SQL拦截器实现。
  • 权限缓存:可以将用户权限缓存到Redis中(key用户名,value权限列表),减少数据库压力,提高性能。(注意每次修改权限后需要考虑缓存一致性)。

前后端用户验证

交互中保证同一用户 的核心:身份标识与验证机制。通过不同技术保证每次请求来自同一个用户,防止身份假冒以及会话混乱。

实现方式

1、 Cookie + Session的传统方案(适用于Web端)

最早且最成熟的方案,依赖服务器的会话存储和客户端的Cookie传递标识。
核心流程

  • 用户登录验证:
    • 登录验证通过后,在服务器内存/数据库中创建一个session(包含用户标识登录状态等信息),生成唯一的SESSIONID。
    • 后端通过Set-Cookie响应头返回客户端,客户端自动将其保存到Cookie中(通常设置HttpOnly和Secure属性)。
  • 后续请求身份确认
    • 客户端每次请求时,浏览器自动子啊请求头中Cookie字段按中携带SessionID。
    • 后端通过SessionID查询服务器的Session数据,若存在且有效(未过期),则任务是同一用户。

相关配置:

  • HttpOnly: true:禁止 JavaScript 读取 Cookie,防止 XSS 攻击窃取SessionID;
  • Secure: true:仅在 HTTPS 协议下传输 Cookie,避免明文泄露;
  • SameSite: Strict/Lax:限制 Cookie 跨域发送,防止 CSRF 攻击;
  • 会话超时机制:如 30 分钟无操作自动失效,降低SessionID被盗用的风险。

2、基于Token的无状态方案(适用于多端应用)

Token方案不依赖服务器存储会话,而是通过加密令牌传递用户信息,更适合前后端分离,移动端、小程序等场景。
核心流程

  • 生成Token:
    • 用户登陆验证通过后,后端使用密钥生成一个加密token(常见为JWT格式),包含用户Id、过期时间、签名等信息(不包含敏感数据)。
    • 后端将Token返回给客户端,客户端存储在LocalStorage、SessionStorage或App本地存储。
  • 携带Token请求
    • 客户端每次请求时,在HTTP请求头中(如Authorization:Bearer<.token>) 中携带token。
    • 后端验证Token签名(确保未篡改)和过期时间,解析出用户Id,确认是同一用户。
      JWT(JSON Web Token)示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.

eyJ1c2VySWQiOjEsIm5hbWUiOiJKb2huIiwiZXhwIjoxNzIwMDAwMDAwfQ.

SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

第一部分:算法声明(HS256);

第二部分:用户信息(用户 ID、过期时间);

第三部分:签名(用密钥生成,确保内容未被篡改)。

相关配置

  • 短期有效期:Token 过期时间设为 15-30 分钟,降低被盗用后的风险;
  • 刷新 Token 机制:同时返回access_token(短期)和refresh_token(长期,如 7 天),过期后用refresh_token重新获取access_token,避免频繁登录;
  • Token 黑名单:用户登出或密码修改时,将旧 Token 加入黑名单(Redis 存储),强制失效。

3、基于设备指纹与生物识别的增强方案(高安全性场景)

在金融、支付等安全性高的场景中,需要结合设备信息或生物特性辅助验证"同一用户"。

  • 设备指纹验证:
    • 后端收集客户端信息(CPU信息、操作系统等),生成唯一"设备指纹"
    • 登录时,将用户ID+设备指纹绑定,后续请求若设备指纹不符(如同一账号在新设备登录),触发二次验证(如短信验证)。
  • 生物识别辅助:
    • 移动端App可结合指纹识别,面部识别,通过后才允许携带Token请求敏感接口。
    • 即使Token泄露,没有生物特征同样无法完成操作。

4、跨域场景下身份有效。

当前后端域名不同,需要配置确保身份标识能跨域传递。

  • Cookie跨域:
    • 后端设置Access-Control-Allow-Credentials: true响应头;
    • 前端请求时携带credentials: 'include'参数(如 Axios 配置);
    • Cookie 设置domain: .parent.com(主域名一致时),允许子域名共享。
  • Token跨域
    • 无跨域限制,只在请求投中正确携带Token即可(不受Cookie同源策略影响)。

补充

Cookie同源策略:是浏览器的一种安全机制,用于防止不同源的网页之间相互访问数据,从而保护用户信息的安全。所谓"同源",指的是两个网页的协议、域名和端口都相同。

作用:

同源策略的主要目的是防止恶意网站窃取用户数据。例如,如果用户登录了一个银行网站A,然后又访问了另一个网站B,如果没有同源策略,网站B可以读取网站A的Cookie,从而获取用户的敏感信息。

常见的攻击手段及防御手段

针对身份盗用的典型攻击(XSS、CSRF、重放攻击等),需要针对性防护。

1、防御XSS攻击(防止标识被窃取)

XSS攻击通过注入恶意脚本窃取前端存储的标识 (如LocalStorage中的Token,document.cookie)。

预防:

  • 前端输入过滤:对用户输入内容(评论、表单)进行HTML转义(如>转义为& lt/ ); 使用框架自带的安全渲染(如React的JSX自动转义,VUE的v-text)。
  • 后端输出编码:返回给前端的数据中,对HTML/JS特殊字符编码,避免直接渲染未处理的用户输入。
  • 启用CSP(内容安全策略):

2、防御CSRF攻击(防止身份被滥用)

CSRF攻击:利用用户已登录的身份,诱导用户在第三方网站上发起恶意请求(如转账),防护措施:

  • SameSite Cookie:通过SameSite-Strict限制Cookie仅在同域请求中携带,彻底阻止跨域CSRF。
  • CSFR Token:对敏感操作(如表单操作、转账),后端生成随机CSFR Token(绑定Session),前端表单携带该Token,后端验证Token的有效性后才处理请求。
    例:前端表单隐藏字段,后端对比 Session 中的 Token。

3、防止重放攻击(防止标识被重复使用)

攻击者窃取Token后重复发送请求(如重复下单),防护措施:

  • Token短期有效+刷新时间
    • assess_token(访问令牌)有效期15~20分钟,用于日常请求;
    • refresh token(刷新令牌)有效期7天,用于过期后获取access_token,且刷新时验证设备信息(如设备信息);
    • 每次刷新后,旧access_token立即失效,refresh_token采用"一次性"机制(使用后立即失效,返回新的refresh_token)。
  • 请求时间戳+nonce
    前端请求时携带timestamp(当前时间戳)和nonce(随机字符串,仅用一次)后端验证:
    • 时间戳与服务器时间 不差5分钟(防止过期请求)。
    • nonce在Redis记录,已使用过的nonce直接拒绝(防止重复请求)。