基于Python-Flask的权限管理3:基于Token的登录

一、前言

登录功能是每个系统的基础,本篇实现了基于token的用户登录和请求权限控制。

二、数据库model

models文件夹下新建user.py,创建ORM实体类

# !/usr/bin/python3
# -*- coding: utf-8 -*-
"""
@Author         :  Huguodong
@Version        :  
------------------------------------
@File           :  user.py
@Description    :  用户表
@CreateTime     :  2020/3/7 14:45
------------------------------------
@ModifyTime     :  
"""
import hashlib

from sqlalchemy import func

from db import db
from models.BaseModel import BaseModel


class User(BaseModel):
    """
    用户表
    """
    __tablename__ = "t_user"
    id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment="用户ID")
    nickname = db.Column(db.String(30), comment="用户昵称")
    user_name = db.Column(db.String(30), comment="登录账号")
    user_type = db.Column(db.Boolean, default=1, comment="用户类型(1系统用户")
    email = db.Column(db.String(50), comment="用户邮箱")
    phone = db.Column(db.String(20), comment="手机号")
    phonenumber = db.Column(db.String(11), comment="手机号码")
    sex = db.Column(db.INTEGER, default=1, comment="用户性别(1男 2女 3未知)")
    avatar = db.Column(db.String(100), comment="头像路径")
    password = db.Column(db.String(50), comment="密码")
    salt = db.Column(db.String(20), comment="盐加密")
    status = db.Column(db.INTEGER, default=1, comment="帐号状态(1正常 2禁用")
    dept_id = db.Column(db.INTEGER, comment="部门id")
    del_flag = db.Column(db.INTEGER, default=1, comment="删除标志(1代表存在 2代表删除)")
    login_ip = db.Column(db.String(50), comment="最后登陆IP")
    login_date = db.Column(db.TIMESTAMP, comment="最后登陆时间", nullable=False,
                           onupdate=func.now())

    def check_password(self, passwd):
        '''
        检查密码
        :param passwd:
        :return: 0/1
        '''
        # 创建md5对象
        m = hashlib.md5()
        b = passwd.encode(encoding='utf-8')
        m.update(b)
        str_md5 = m.hexdigest()
        if self.password == str_md5:
            return 1
        else:
            return 0

二、创建蓝图

1.permission文件夹下新建蓝图文件user.py

from permission import *

user = Blueprint('user', __name__)

2.app.py注册蓝图

app.register_blueprint(user.user, url_prefix='/api/user')

 3.写一个测试方法

@user.route('/test', methods=["GET"])
def test():
    return SUCCESS()

4.浏览器输入http://127.0.0.1:5000/api/user/test

二、实现token方法

用token校验身份,是前后端交互的常用方式。
它有以下特性:

    • 会失效
    • 加密
    • 可以根据它拿到用户的信息

关于token的方法写在utils下的common.py下

1.生成token:生成方式( 内部配置的私钥+有效期+用户的id +用户名+用户角色列表) 

def create_token(user_id, user_name, role_list):
    '''
    生成token
    :return: token
    '''
    # 第一个参数是内部的私钥,这里写在共用的配置信息里了,如果只是测试可以写死
    # 第二个参数是有效期(秒)
    s = Serializer(config.SECRET_KEY, expires_in=config.EXPIRES_IN)
    # 接收用户id转换与编码
    token = None
    try:
        token = s.dumps({"id": user_id, "name": user_name, "role": role_list}).decode("ascii")
    except Exception as e:
        app.logger.error("获取token失败:{}".format(e))
    return token

 2.校验token:校验接收到的token,如果成功返回用户信息,否则返回None

def verify_token(token):
    '''
    校验token
    :param token:
    :return: 用户信息 or None
    '''
    # 参数为私有秘钥,跟上面方法的秘钥保持一致
    s = Serializer(config.SECRET_KEY)
    try:
        # 转换为字典
        data = s.loads(token)
        return data
    except Exception as e:
        return None

3.有很多接口是必须登录才能操作的,最好的方式就是在写一个装饰器,添加在需要的api上

def login_required(*role):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            try:
                # 在请求头上拿到token
                token = request.headers["Authorization"]
            except Exception as e:
                # 没接收的到token,给前端抛出错误
                return jsonify(code=Code.NO_PARAMETER.value, msg='缺少参数token')
            s = Serializer(config.SECRET_KEY)
            try:
                user = s.loads(token)
                if role:
                    # 获取token中的权限列表如果在参数列表中则表示有权限,否则就表示没有权限
                    user_role = user['role']
                    result = [x for x in user_role if x in list(role)]
                    if not result:
                        return jsonify(code=Code.ERR_PERMISSOM.value, msg="权限不够")
            except Exception as e:
                return jsonify(code=Code.LOGIN_TIMEOUT.value, msg="登录已过期")
            return func(*args, **kw)
        return wrapper
    return decorator

这样只需在方法上添加装饰器就能限制用户访问

#限制只有admin可以操作
@user.route('/xxx', methods=["POST"])
@login_required(‘admin’)
def xxx()
    return xx

二、用户登录

点击前端页面的登录按钮,会请求登录接口

persmisson文件夹下的user.py新建登录方法

@user.route('/login', methods=["POST"])
def login():
    '''
    用户登录
    :return:token
    '''
    res_dir = request.get_json()
    if res_dir is None:
        return NO_PARAMETER()
    # 获取前端传过来的参数
    username = res_dir.get("username")
    password = res_dir.get("password")
    # 校验参数
    if not all([username, password]):
        return jsonify(code=Code.NOT_NULL.value, msg="用户名和密码不能为空")
    try:
        user = User.query.filter_by(user_name=username).first()
    except Exception as e:
        app.logger.error("login error:{}".format(e))
        return jsonify(code=Code.REQUEST_ERROR.value, msg="获取信息失败")
    if user is None or not user.check_password(password) or user.del_flag == 2:
        return jsonify(code=Code.ERR_PWD.value, msg="用户名或密码错误")

    # 获取用户信息,传入生成token的方法,并接收返回的token
    # 获取用户角色
    user_role = Role.query.join(User_Role, Role.id == User_Role.role_id).join(User,
                                                                              User_Role.user_id == user.id).filter(
        User.id == user.id).all()
    role_list = [i.role_key for i in user_role]
    token = create_token(user.id, user.user_name, role_list)
    data = {'token': token, 'userId': user.id, 'userName': user.user_name, 'nickname': user.nickname}
    # 记录登录ip将token存入rerdis
    try:
        user.login_ip = request.remote_addr
        user.update()
        Redis.write(f"token_{user.user_name}", token)

    except Exception as e:
        return jsonify(code=Code.UPDATE_DB_ERROR.value, msg="登录失败")
    if token:
        # 把token返回给前端
        return jsonify(code=Code.SUCCESS.value, msg="登录成功", data=data)
    else:
        return jsonify(code=Code.REQUEST_ERROR.value, msg="请求失败", data=token)

二、用户注销

persmisson文件夹下的user.py新建注销方法

@user.route('/logout', methods=["POST"])
@login_required()
def logout():
    '''
    注销方法:redis删除token
    :return:
    '''
    try:
        token = request.headers["Authorization"]
        user = verify_token(token)
        if user:
            key = f"token_{user.get('name')}"
            redis_token = Redis.read(key)
            if redis_token:
                Redis.delete(key)
            return SUCCESS()
        else:
            return AUTH_ERR()
    except Exception as e:
        app.logger.error(f"注销失败")
        return REQUEST_ERROR()

三、检查Token

登录成功后,系统会请求一个check_token方法,主要是检查用户token是否合法

persmisson文件夹下的user.py新建check_token方法

@user.route('/check_token', methods=["POST"])
def check_token():
    # 在请求头上拿到token
    token = request.headers["Authorization"]
    user = verify_token(token)
    if user:
        key = f"token_{user.get('name')}"
        redis_token = Redis.read(key)
        if redis_token == token:
            return SUCCESS(data=user.get('id'))
        else:
            return OTHER_LOGIN()
    else:
        return AUTH_ERR()

猜你喜欢

转载自www.cnblogs.com/huguodong/p/12589287.html