3、用户认证与权限管理:RBAC权限模型设计、JWT令牌认证实现、用户注册与登录API、角色与权限的CRUD、中间件权限校验

好,咱们直接进入正题。这一章聊的是ERP系统的「门禁系统」——用户认证与权限管理。说白了,就是解决两个问题:你是谁?你能干什么?

我在做第一个ERP项目时,权限这块吃了大亏。当时图省事,直接在用户表里加了个「is_admin」字段。结果上线第三天,一个普通员工误操作删了整张订单表……嗯,从那以后,我再也不敢轻视权限设计了。

3.1 RBAC权限模型设计

RBAC,全称是Role-Based Access Control,基于角色的访问控制。为什么选它?因为现实世界就是这么运作的。

你想想看,公司里不会直接给张三分配「可以查看财务报表」的权限,而是说「张三你是财务经理,财务经理这个角色自然拥有查看报表的权限」。这就是RBAC的核心思想:用户 ↔ 角色 ↔ 权限。

核心三要素:

  • 用户(User):系统的操作者,比如张三、李四
  • 角色(Role):权限的集合,比如财务经理、仓库管理员
  • 权限(Permission):具体的操作许可,比如「创建订单」、「删除用户」

我个人习惯用五张表来实现:

-- 用户表
CREATE TABLE `sys_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL,
  `password` varchar(255) NOT NULL,
  `status` tinyint(1) DEFAULT '1',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`)
);

-- 角色表
CREATE TABLE `sys_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `code` varchar(50) NOT NULL,
  `description` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_code` (`code`)
);

-- 权限表
CREATE TABLE `sys_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `code` varchar(50) NOT NULL,
  `type` tinyint(1) DEFAULT '1' COMMENT '1:菜单 2:按钮 3:API',
  `parent_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
);

-- 用户角色关联表
CREATE TABLE `sys_user_role` (
  `user_id` int(11) NOT NULL,
  `role_id` int(11) NOT NULL,
  PRIMARY KEY (`user_id`, `role_id`)
);

-- 角色权限关联表
CREATE TABLE `sys_role_permission` (
  `role_id` int(11) NOT NULL,
  `permission_id` int(11) NOT NULL,
  PRIMARY KEY (`role_id`, `permission_id`)
);

避坑指南:我曾经在权限表里只存了「菜单权限」,结果发现后端API完全没控制。用户虽然看不到菜单,但直接调接口照样能操作。所以权限一定要覆盖到API层面。

3.2 JWT令牌认证实现

JWT,全称JSON Web Token。为什么不用传统的Session?因为ERP系统往往是前后端分离的,Session的跨域问题太头疼了。

JWT的结构很简单:Header.Payload.Signature。说白了就是三段Base64编码的字符串,用点号连起来。

我建议用Python的PyJWT库来实现,代码量很少:

import jwt
from datetime import datetime, timedelta

# 密钥,生产环境一定要用环境变量
SECRET_KEY = "your-secret-key-here"

def create_token(user_id, username, roles):
    """生成JWT令牌"""
    payload = {
        'user_id': user_id,
        'username': username,
        'roles': roles,
        'exp': datetime.utcnow() + timedelta(hours=8),  # 8小时过期
        'iat': datetime.utcnow()  # 签发时间
    }
    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
    return token

def verify_token(token):
    """验证JWT令牌"""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        return payload
    except jwt.ExpiredSignatureError:
        return None  # 令牌过期
    except jwt.InvalidTokenError:
        return None  # 无效令牌

注意:JWT的Payload是Base64编码,不是加密!千万不要把密码等敏感信息放进去。我曾经见过有人把用户手机号直接放JWT里,这相当于把密码写在脸上。

3.3 用户注册与登录API

注册和登录,这是最基础的两个接口。但基础不代表简单,里面坑不少。

先看注册接口:

@app.route('/api/auth/register', methods=['POST'])
def register():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    
    # 参数校验
    if not username or not password:
        return jsonify({'code': 400, 'msg': '用户名和密码不能为空'}), 400
    
    if len(password) < 6:
        return jsonify({'code': 400, 'msg': '密码长度不能少于6位'}), 400
    
    # 检查用户名是否已存在
    if User.query.filter_by(username=username).first():
        return jsonify({'code': 400, 'msg': '用户名已存在'}), 400
    
    # 密码加密存储
    hashed_password = generate_password_hash(password)
    
    # 创建用户,默认分配一个普通角色
    user = User(username=username, password=hashed_password)
    db.session.add(user)
    db.session.flush()  # 获取user.id
    
    # 分配默认角色
    default_role = Role.query.filter_by(code='ROLE_USER').first()
    if default_role:
        user_role = UserRole(user_id=user.id, role_id=default_role.id)
        db.session.add(user_role)
    
    db.session.commit()
    
    return jsonify({'code': 200, 'msg': '注册成功'}), 200

再看登录接口:

@app.route('/api/auth/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    
    # 查询用户
    user = User.query.filter_by(username=username).first()
    if not user:
        return jsonify({'code': 401, 'msg': '用户名或密码错误'}), 401
    
    # 验证密码
    if not check_password_hash(user.password, password):
        return jsonify({'code': 401, 'msg': '用户名或密码错误'}), 401
    
    # 检查用户状态
    if user.status == 0:
        return jsonify({'code': 403, 'msg': '账号已被禁用'}), 403
    
    # 获取用户角色
    roles = [ur.role.code for ur in user.roles]
    
    # 生成令牌
    token = create_token(user.id, user.username, roles)
    
    return jsonify({
        'code': 200,
        'data': {
            'token': token,
            'user': {
                'id': user.id,
                'username': user.username
            }
        }
    }), 200

个人经验:登录接口一定要返回「用户名或密码错误」,不要具体说是用户名错了还是密码错了。这是安全常识,防止攻击者通过错误信息猜出有效用户名。

3.4 角色与权限的CRUD

这部分是管理后台的核心功能。说白了就是增删改查,但有几个细节要注意。

我直接给一个角色管理的示例:

@app.route('/api/roles', methods=['GET'])
def get_roles():
    """获取角色列表"""
    roles = Role.query.all()
    return jsonify({
        'code': 200,
        'data': [{
            'id': r.id,
            'name': r.name,
            'code': r.code,
            'description': r.description,
            'permission_count': len(r.permissions)
        } for r in roles]
    })

@app.route('/api/roles', methods=['POST'])
def create_role():
    """创建角色"""
    data = request.get_json()
    
    # 检查角色编码是否唯一
    if Role.query.filter_by(code=data['code']).first():
        return jsonify({'code': 400, 'msg': '角色编码已存在'}), 400
    
    role = Role(
        name=data['name'],
        code=data['code'],
        description=data.get('description', '')
    )
    db.session.add(role)
    db.session.commit()
    
    return jsonify({'code': 200, 'msg': '创建成功'}), 200

@app.route('/api/roles/<int:role_id>/permissions', methods=['PUT'])
def assign_permissions(role_id):
    """给角色分配权限"""
    data = request.get_json()
    permission_ids = data.get('permission_ids', [])
    
    role = Role.query.get(role_id)
    if not role:
        return jsonify({'code': 404, 'msg': '角色不存在'}), 404
    
    # 先清除原有权限,再重新分配
    role.permissions = []
    for pid in permission_ids:
        perm = Permission.query.get(pid)
        if perm:
            role.permissions.append(perm)
    
    db.session.commit()
    
    return jsonify({'code': 200, 'msg': '权限分配成功'}), 200

我曾经踩过的坑:删除角色时忘了检查是否有用户关联。结果删了一个角色,导致一批用户突然失去了所有权限。所以删除角色前一定要先检查用户关联,或者做软删除。

3.5 中间件权限校验

最后一步,也是最关键的一步——在请求到达业务逻辑之前,先拦住它检查权限。

我习惯用装饰器来实现,这样代码最干净:

from functools import wraps

def require_permission(permission_code):
    """权限校验装饰器"""
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            # 从请求头获取令牌
            token = request.headers.get('Authorization')
            if not token:
                return jsonify({'code': 401, 'msg': '未登录'}), 401
            
            # 验证令牌
            payload = verify_token(token)
            if not payload:
                return jsonify({'code': 401, 'msg': '令牌无效或已过期'}), 401
            
            # 查询用户权限
            user_id = payload['user_id']
            user = User.query.get(user_id)
            if not user:
                return jsonify({'code': 401, 'msg': '用户不存在'}), 401
            
            # 检查是否有超级管理员角色
            if 'ROLE_ADMIN' in [r.code for r in user.roles]:
                return f(*args, **kwargs)
            
            # 检查具体权限
            user_permissions = set()
            for role in user.roles:
                for perm in role.permissions:
                    user_permissions.add(perm.code)
            
            if permission_code not in user_permissions:
                return jsonify({'code': 403, 'msg': '权限不足'}), 403
            
            return f(*args, **kwargs)
        return decorated_function
    return decorator

# 使用示例
@app.route('/api/orders', methods=['POST'])
@require_permission('order:create')
def create_order():
    # 只有拥有 order:create 权限的用户才能访问
    return jsonify({'code': 200, 'msg': '创建订单成功'})

我建议:权限校验不要只依赖前端路由守卫。前端可以防君子,但防不住直接调API的。后端中间件才是真正的防线。另外,超级管理员角色最好做特殊处理,避免给超级管理员分配所有权限的麻烦。

好了,这一章的内容就到这里。用户认证和权限管理是ERP系统的基石,设计得好,后面开发会非常顺畅。下一章我们聊聊「商品管理模块」,到时候会用到今天讲的权限校验。