第四章:用户认证系统

用户认证,说白了就是「你是谁」的问题。

我刚开始做ERP系统时,觉得认证不就是登录登出嘛,简单。结果第一个项目就被安全审计打回来三次……嗯,从那以后我再也不敢小看这块了。

今天咱们就把用户认证系统从头到尾捋一遍。从用户模型设计,到注册登录,再到JWT令牌和权限控制基础。你跟着我走一遍,以后做任何系统都能复用这套思路。

4.1 用户模型设计

用户模型是整个认证系统的地基。地基没打好,后面全是坑。

我个人习惯,用户模型最少包含这几个核心字段:

字段名 类型 说明
id UUID 主键,不暴露自增ID给前端
username string(50) 唯一,用于登录
email string(100) 唯一,用于找回密码
password_hash string(255) 永远不存明文!
role enum admin / manager / staff
status boolean 是否激活
created_at datetime 注册时间
updated_at datetime 最后更新时间
⚠️ 注意: 千万别把密码存成明文。我曾经接手过一个项目,数据库里密码全是base64编码的「123456」……那感觉就像看到有人用铁丝挂锁,一扯就开。

用Django的模型来写,大概是这样的:

from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    ROLE_CHOICES = (
        ('admin', '管理员'),
        ('manager', '经理'),
        ('staff', '员工'),
    )
    role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='staff')
    phone = models.CharField(max_length=20, blank=True)
    status = models.BooleanField(default=True)
    
    class Meta:
        db_table = 'sys_user'
        verbose_name = '用户'

为什么继承AbstractUser?因为Django自带的认证系统已经帮我们处理了密码哈希、会话管理等麻烦事。你想想看,自己手写密码加密算法?那大概率会出问题。

4.2 注册功能实现

注册功能看着简单,但有几个细节要注意。

核心原则: 永远不要信任前端传过来的数据。后端必须做二次校验。

我建议的注册流程:

  1. 前端提交用户名、邮箱、密码、确认密码
  2. 后端校验用户名和邮箱是否唯一
  3. 校验密码强度(至少8位,包含字母和数字)
  4. 密码加密存储(用bcrypt或Django自带的PBKDF2)
  5. 返回成功信息,不返回密码相关字段

代码示例:

from rest_framework import serializers
from django.contrib.auth.hashers import make_password

class RegisterSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, min_length=8)
    confirm_password = serializers.CharField(write_only=True)
    
    class Meta:
        model = User
        fields = ['username', 'email', 'password', 'confirm_password']
    
    def validate(self, data):
        if data['password'] != data['confirm_password']:
            raise serializers.ValidationError('两次密码不一致')
        # 这里可以加密码强度校验
        return data
    
    def create(self, validated_data):
        validated_data.pop('confirm_password')
        validated_data['password'] = make_password(validated_data['password'])
        return super().create(validated_data)
💡 小技巧: 注册成功后,我习惯返回一个简单的欢迎消息,而不是直接返回用户数据。这样既安全又友好。

4.3 登录/登出功能

登录认证,我推荐用JWT(JSON Web Token)。为什么不用Session?

说白了,Session是服务端存储状态,JWT是无状态的。在微服务架构下,JWT的优势很明显——每个服务都能独立验证令牌,不用每次都去查Session数据库。

登录流程:

  1. 用户提交用户名和密码
  2. 后端验证凭据
  3. 验证通过后生成JWT令牌
  4. 返回access_token和refresh_token

代码示例:

from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.views import APIView
from rest_framework.response import Response

class LoginView(APIView):
    def post(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        
        user = authenticate(username=username, password=password)
        if user is None:
            return Response({'error': '用户名或密码错误'}, status=401)
        
        if not user.status:
            return Response({'error': '账号已被禁用'}, status=403)
        
        refresh = RefreshToken.for_user(user)
        return Response({
            'access_token': str(refresh.access_token),
            'refresh_token': str(refresh),
            'user': {
                'id': user.id,
                'username': user.username,
                'role': user.role
            }
        })

登出呢?JWT是无状态的,所以登出其实就是前端把令牌删掉。但为了更安全,我一般会在后端维护一个黑名单,把登出的令牌加进去。

⚠️ 注意: 我曾经犯过一个错——把JWT的有效期设成了7天。结果有个员工离职后,他的令牌还能用……从那以后,access_token有效期我最多设30分钟,配合refresh_token使用。

4.4 JWT令牌认证

JWT的结构很简单:header.payload.signature。你想想看,就像一份带防伪标识的文件,谁都能看内容,但只有持有密钥的人才能验证真伪。

配置JWT:

# settings.py
from datetime import timedelta

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': True,
    'AUTH_HEADER_TYPES': ('Bearer',),
    'ALGORITHM': 'HS256',
}

前端每次请求时,在HTTP头里带上令牌:

Authorization: Bearer <your_access_token>

后端通过中间件验证令牌:

from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework.permissions import IsAuthenticated

class SomeView(APIView):
    authentication_classes = [JWTAuthentication]
    permission_classes = [IsAuthenticated]
    
    def get(self, request):
        # request.user 就是当前登录用户
        return Response({'message': f'你好, {request.user.username}'})
💡 小技巧: 我习惯在JWT的payload里存一些常用信息,比如用户角色、用户ID。这样就不用每次请求都查数据库了。但别存敏感信息,因为payload是base64编码的,不是加密的。

4.5 权限控制基础

权限控制,说白了就是「你能干什么」。

在ERP系统里,权限控制分三个层次:

层次 说明 示例
接口权限 能不能访问某个API 只有admin能删除用户
数据权限 能看到哪些数据 经理能看到本部门数据
字段权限 能看到哪些字段 普通员工看不到薪资字段

最简单的实现方式——基于角色的权限控制(RBAC):

from rest_framework.permissions import BasePermission

class IsAdminUser(BasePermission):
    def has_permission(self, request, view):
        return request.user.role == 'admin'

class IsManagerOrAdmin(BasePermission):
    def has_permission(self, request, view):
        return request.user.role in ['admin', 'manager']

# 使用方式
class DeleteUserView(APIView):
    permission_classes = [IsAuthenticated, IsAdminUser]
    
    def delete(self, request, user_id):
        # 只有admin才能删除用户
        pass
核心思路: 权限控制要「默认拒绝,显式授权」。也就是说,默认谁都不能访问,只有明确授权的才能访问。这样最安全。

我建议在项目初期就设计好权限模型,不然后面改起来很痛苦。我曾经在一个项目里,权限控制是写在视图函数里的if-else……后来加了20多个角色,那代码简直没法看。

4.6 本章小结

好了,咱们把用户认证系统过了一遍:

  • 用户模型设计——地基要打牢
  • 注册功能——后端校验不能省
  • 登录/登出——JWT是主流方案
  • JWT令牌认证——无状态,适合微服务
  • 权限控制基础——默认拒绝,显式授权

下一章咱们要进入ERP的核心模块——商品管理了。到时候你会看到,今天学的认证和权限控制,会在每个功能里用到。

嗯,今天就到这儿。有什么问题,咱们下节课见。


公众号:蓝海资料掘金营,微信deep3321