登录相关
权限管理模块(基础版)
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 进行,它会对比当前访问资源所需的权限信息和用户信息中的权限信息。
步骤:
- 加载用户到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();
}
}
- 权限校验
- 注解方式校验:在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直接拒绝(防止重复请求)。