搭建Tornado Https服务器之Tornado Jwt权限验证(7)

       经过前面的学习通过,我们已经初步掌握对数据库的增删改查,并且基本实现了Tornado api服务器的编写。我们知道,写api服务器是给前端使用的,api服务器所用的场景是前后端完全分离的项目,api服务器给前端提供不同的接口,前端通过访问接口对数据库进行读写,那么问题来了,既然我把接口开放出去,万一遇到坏人怎么办呢?比如说,我们对用户操作的api,在项目中的拿出一个路由说说: /user/update 在原先的项目中,只要你传入用户id和要更改的信息,只要用户存在就能更改,写了这个路由,不加保护措施,任何人都能够访问并通过此路由改写数据库信息。

      因此为了防止别人恶意的利用接口发出请求,我们需要对发往服务器的请求,拦截审核。如果你有web基础,你或许会听过session cookies token这些词汇,它们都属于会话验证的机制,在这里我们关注 jwt一种是目前最流行的跨域身份验证解决方案。

一、初识JWT

       JWT 全称Json Web Token,它是允许我们使用JWT在用户和服务器之间传递安全可靠的信息的一种规范,一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。关于理论论述请参考 前后端分离之JWT用户认证  百度JWT 我不加赘述,我们关注如何使用它,当然在使用它的前提是简单的了解。

二、在本项目中我们的使用流程

       结合项目来说,我们计划在用户登录的时候,如果登录成功,我们将他的用户id等基本信息(不涉及密码,防止泄露)通过jwt加密成一个字符串,而他之后的发送的每一个请求都需要在头部加上    Authorization:jwt加密的字符串    ,具体的操作在代码中体现。对于其他的路由,也就是api请求我们选择性的进行核实请求者的身份,比如说获取用户信息,我们设一道卡。模拟一下流程:

 三、写代码

我们之前的代码处理一下,把不需要的清楚,并且创建一些新的文件。

1、目录结构

2、auth.py文件

auth.py文件是我们存放生成jwt和解密jwt方法的文件,接下来上代码,同时别忘记安装jwt(pip install pyjwt)

import jwt
import datetime
"""
进行权限验证,encode_auth_token 生成token
decode_auth_token 根据 token 解析出数据
import base64
import uuid
print(base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes))
#hhLgDUloTO2hKpawAGathnZEwNDbDEAOrNZQLj1DAzk=
"""
# 自定义密钥,密钥的生成在上面注释的代码中
secret = "hhLgDUloTO2hKpawAGathnZEwNDbDEAOrNZQLj1DAzk="
# 对用户信息加密生成jwt
def encode_auth_token(user_id, username, class_id,):
    try:
        # 这个就是jwt三个部分中的载荷部分
        # # 过期时间最后是和 UTC 作比较,所以设置的时候使用 datetime.datetime.utcnow()
        pyload={
            'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7),  # 过期时间,我设7天过期
            'iat': datetime.datetime.utcnow(),  #  开始时间
            'iss': 'wang', # 签名
            'data': {
                'id': user_id,
                'username': username,
                'class_id': class_id
            } # 内容,一般存放用户id
        }
        # 开始进行加密,返回字符串,可以看到传入例如密钥,指定了加密算法
        # encode 返回的是 bytes,需要 decode() 得到 str,不转换的话,在封装json的时候报错
        auth_token = jwt.encode(pyload, secret, algorithm='HS256').decode()
    except Exception as e:
        auth_token = e
    return auth_token

# 从jwt中提取用户信息,进行验证
def decode_auth_token(auth_token):
    # 如果需要关闭过期时间的验证,可以在 options 中使用 verify_exp
    # jwt.decode(auth_token, secret, issuer='wang', algorithms=['HS256'], options={'verify_exp': False})
    # 传入了密钥和算法,和加密是对应的,因此密钥一定不要泄露
    s = jwt.decode(auth_token, secret, issuer='wang', algorithms=['HS256'])  # 解密,校验签名
    return s

3、我们更改basehandler.py文件,在此文件中我们需要写一个方法去拦截请求

import tornado.web # 导入tornado模块
from auth import decode_auth_token
import jwt
# 这是一个父类,此父类允许跨域,并对预请求做了处理
class BaseHandler(tornado.web.RequestHandler):

    # 配置请求头,允许跨域,否者在浏览器调用的时候报错误,同时还得加上允许Authorization字段过来
    def set_default_headers(self):
        self.set_header("Access-Control-Allow-Origin", "*")
        self.set_header("Access-Control-Allow-Headers", "Content-Type,Authorization")
        self.set_header("Access-Control-Allow-Methods", "POST,GET,OPTIONS")

    # 这个函数是必要的,有些浏览器或者测试工具在访问之前都会预先访问,你不写的话会导致出错的
    # 例如vue一般需要访问options方法
    def options(self):
        self.finish()

    """
    通过分析请求头部的Authorization中的数据 判断是否过期 
    并解析出用户的class_id username id
    """
    def getHeaderData(self):
        try:
            auth_token = self.request.headers.get("Authorization")
            if not auth_token:
                # 权限拒绝,返回401
                self.set_status(401) #给请求者提示 权限拒绝
                self.write({"error":"权限拒绝!"})
                return
            # 传过来的是字符串 我们需要转换成字节,才能解密
            auth_token = bytes(auth_token, encoding="utf8") # 字符转字节
            # 对jwt解密
            auth_token = decode_auth_token(auth_token)
        # 权限过期会发生异常,我们捕获它
        except jwt.exceptions.ExpiredSignatureError as e:
            # 权限过期了
            self.set_status(403) #给请求者提示 资源不可用
            self.write({"error":"权限过期了!"})
            return
        return auth_token.get("data")

4、更改main.py文件,清理之前的残留

import sys
# 将handler目录下的文件放到和main.py文件同级目录下,便于我们引用
sys.path.append("./handler")
# 添加必要的tornado的模块
import tornado.ioloop
import tornado.web
# 从classhandler.py文件中导出写好的 类
from classhandler import NewClassHandler
from userhandler import NewUserHandler,GetUserHandler,ListUserHandler,UpdateUserHandler,DeleteUserHandler,LoginUserHandler

# 写程序入口函数 main函数
def main():
    # 定义请求的路径和响应的请求类,此类会根据你发出的请求区分get 还是post而给予不同的处理
    application = tornado.web.Application([
        (r"/class/new", NewClassHandler),
        (r"/user/new", NewUserHandler),
        (r"/user/info", GetUserHandler),
        (r"/user/list", ListUserHandler),
        (r"/user/update", UpdateUserHandler),
        (r"/user/delete", DeleteUserHandler),
        (r"/user/login", LoginUserHandler),
        ])
    # 绑定端口,单进程启动
    application.listen(8000)
    tornado.ioloop.IOLoop.instance().start()

if __name__ == "__main__":
    main()

5、为了保证项目的完整性,model.py文件也拿出来,当然了host我没有放出来,你们自己更改吧。

from peewee import *

database = MySQLDatabase('test', **{'charset': 'utf8', 'sql_mode': 'PIPES_AS_CONCAT', 'use_unicode': True, 'host': 'x.x.x.x', 'port': 3306, 'user': 'root', 'password': 'root'})

class UnknownField(object):
    def __init__(self, *_, **__): pass

class BaseModel(Model):
    class Meta:
        database = database

class Classes(BaseModel):
    classname = CharField()
    createtime = DateTimeField(constraints=[SQL("DEFAULT CURRENT_TIMESTAMP")])
    id = BigAutoField()

    class Meta:
        table_name = 'classes'

class Users(BaseModel):
    class_ = ForeignKeyField(column_name='class_id', field='id', model=Classes)
    createtime = DateTimeField(constraints=[SQL("DEFAULT CURRENT_TIMESTAMP")])
    id = BigAutoField()
    password = CharField(constraints=[SQL("DEFAULT '11111111'")])
    username = CharField()

    class Meta:
        table_name = 'users'

 6、classhandler.py文件本次用不到,我没有加上权限验证,但是代码也放出来

from basehandler import BaseHandler
from model import Classes,database

# 这是一个处理新建班级的请求类,继承tornado.web.RequestHandler
class NewClassHandler(BaseHandler):
    # 重写post请求函数,处理post请求
    def post(self):
        # 为了防止出错导致程序崩溃,我们需要捕获异常函数,并处理 
        try:
            # 从请求中获取参数,如果获取不到,我们赋值 None
            classname = self.get_argument('classname', None)
            # 对传入的参数进行判断,如果为空我们回复 error,并终止执行下去
            if not classname:
                # self.write()函数的参数为标准的json类型
                self.write({"error":"班级名称为空!"})
                return
            database.connect()
            classes = Classes.get(Classes.classname==classname)
            self.write({"error":"班级已存在!"})
        except Classes.DoesNotExist:
            classes_id=Classes.insert({
                'classname':classname
            }).execute()
            if classes_id:
                self.write({"success":"新建班级成功!"})
            else:
                self.write({"error":"新建班级失败!"})
        except Exception as e:
            print('classes new',e)
            self.write({"error":"服务器出错!"})
        finally:
            database.close()

7、接下来是重头戏,userhandler.py文件,对于这个文件我们只改两个部分,第一获取用户信息的路由加个验证;第二增加登录路由

from basehandler import BaseHandler
# 从model中导出Users类 就是users表 以及数据库连接对象
from model import Users,database
# 这个模块能够将 model类型与字典互换 例如:users的一条记录 转换成 数据和字段一一对应的字典
# 这里我们只用到了 model_to_dict 字面的意思 model转换成字典
from playhouse.shortcuts import model_to_dict
from auth import encode_auth_token

# 这是一个处理新建用户的请求类,继承BaseHandler
class NewUserHandler(BaseHandler):
    # 重写post请求函数,处理post请求
    def post(self):
        # 为了防止出错导致程序崩溃,我们需要捕获异常函数,并处理 
        try:
            # 从请求中获取参数,如果获取不到,我们赋值 None
            username = self.get_argument('username', None)
            password = self.get_argument('password', None)
            class_id = self.get_argument('class_id', None)
            # 对传入的参数进行判断,如果为空我们回复 error,并终止执行下去
            if not username:
                # self.write()函数的参数为标准的json类型
                self.write({"error":"用户账号为空!"})
                return
            if not password:
                # self.write()函数的参数为标准的json类型
                self.write({"error":"用户密码为空!"})
                return
            if not class_id:
                # self.write()函数的参数为标准的json类型
                self.write({"error":"班级没有选择!"})
                return
            database.connect() # 连接数据库
            # 下面这句话的意思是根据 username获取用户信息 我们作做这一部是为了防止用户已存在
            # 如果用户不存在会抛出异常DoesNotExist 我们要捕获异常,否则程序报错
            user = Users.get(Users.username==username)
            self.write({"error":"用户已存在!"})
        except Users.DoesNotExist:
            # 下面的代码就是插入的操作,当然peewee不止一种插入方式,请学习peewee文档
            # 执行成功后会返回新插入的id
            user_id=Users.insert({
                'username':username,
                'password':password,
                'class_id':class_id
            }).execute()
            self.write({"success":user_id})
        except Exception as e:
            # 如果出现异常我们打印出来
            print(e)
            self.write({"error":"服务器出错!"})
        finally:
            # 不论是否出现异常,关闭数据库
            database.close()

# 这是一个处理获取用户信息的请求类,继承BaseHandler
class GetUserHandler(BaseHandler):
    # 重写get请求函数,处理get请求
    def get(self):
        # 为了防止出错导致程序崩溃,我们需要捕获异常函数,并处理 
        try:
            # 判断权限,获取请求头中的Authorization信息,如果没有,给401提示
            auth_token = self.getHeaderData()
            if not auth_token:
                return
            # 如果权限通过 那么 我们从 jwt中提取用户id,不接收传来的id防止传入非法id
            user_id = auth_token.get('id')
            database.connect() # 连接数据库
            user=Users.get_by_id(user_id) # 通过id得到用户信息
            user_dir=model_to_dict(user) # model转换成字典
            # 转换时间格式
            user_dir['createtime']=user.createtime.strftime('%Y-%m-%d %H:%M:%S')
            # 对于外键处理 它会自己把外键的相关信息封装一起,我们需要什么拿什么,在查询的时候无需联合查询
            user_dir['classname']=user.class_.classname
            del user_dir['class_'] # 将冗余的删除,只剩下一层json
            del user_dir['password'] 
            self.write({"success":user_dir})
        except Users.DoesNotExist:
            self.write({"error":"查无此人!"})
        except Exception as e:
            # 如果出现异常我们打印出来
            print(e)
            self.write({"error":"服务器出错!"})
        finally:
            database.close()

# 这是一个处理获取用户列表的请求类,继承BaseHandler
class ListUserHandler(BaseHandler):
    # 重写get请求函数,处理get请求
    def get(self):
        # 为了防止出错导致程序崩溃,我们需要捕获异常函数,并处理 
        try:
            database.connect()
            # select查询会返回多条记录,并且支持指定字段和条件查询以及排序
            users = Users.select().order_by(Users.createtime.asc())
            result_list=[]
            # 这个是返回查询条数的
            effect_row=users.count()
            # 这个是支持分页的,我们给的是一页10条数据,现在取首页
            users=users.paginate(1,10)
            for user in users:
                user_dir=model_to_dict(user)
                # 转换时间格式
                user_dir['createtime']=user.createtime.strftime('%Y-%m-%d %H:%M:%S')
                user_dir['classname']=user.class_.classname
                del user_dir['class_'] # 将冗余的删除,只剩下一层json
                del user_dir['password'] 
                result_list.append(user_dir)
            self.write({"success":result_list,"length":effect_row,"page":1,"limit":10})
        except Exception as e:
            print('user list post',e)
            self.write({"error":"服务器出错!"})
        finally:
            database.close()

# 这是一个处理更改用户信息的请求类,继承BaseHandler
# 这里我们只更改密码,如果你有想法的可以自己增加
class UpdateUserHandler(BaseHandler):
    # 重写get请求函数,处理get请求
    def post(self):
        # 为了防止出错导致程序崩溃,我们需要捕获异常函数,并处理 
        try:
            # 从请求中获取参数,如果获取不到,我们赋值 None
            user_id = self.get_argument('id', None)
            password = self.get_argument('password', None)
            # 对传入的参数进行判断,如果为空我们回复 error,并终止执行下去
            if not user_id:
                # self.write()函数的参数为标准的json类型
                self.write({"error":"用户账号为空!"})
                return
            if not password:
                # self.write()函数的参数为标准的json类型
                self.write({"error":"用户密码为空!"})
                return
            database.connect()
            # 下面代码是更新用户信息
            user=Users.get(Users.id==user_id) # 首先获取到用户
            # 其次更改用户字段
            user.password=password
            # 然后保存提交
            user.save()
            self.write({"success":"更新密码成功!"})
        except Users.DoesNotExist:
            self.write({"error":"用户密码错误!"})
        except Exception as e:
            # 如果出现异常我们打印出来
            print(e)
            self.write({"error":"服务器出错!"})
        finally:
            database.close()

# 这是一个处理删除用户信息的请求类,继承BaseHandler
class DeleteUserHandler(BaseHandler):
    # 重写get请求函数,处理get请求
    def get(self):
        # 为了防止出错导致程序崩溃,我们需要捕获异常函数,并处理 
        try:
            # 从请求中获取参数,如果获取不到,我们赋值 None
            user_id = self.get_argument('id', None)
            # 对传入的参数进行判断,如果为空我们回复 error,并终止执行下去
            if not user_id:
                # self.write()函数的参数为标准的json类型
                self.write({"error":"用户账号为空!"})
                return
            database.connect()
            # 下面代码是删除用户
            Users.delete().where(Users.id == user_id).execute()
            self.write({"success":"删除用户成功!!"})
        except Exception as e:
            print('user delete',e)
            self.write({"error":"服务器出错!"})
        finally:
            database.close()

# 登录的请求函数
class LoginUserHandler(BaseHandler):
    def post(self):
        try:
            # 从请求中获取传入的参数
            username = self.get_argument('username', 0)
            password = self.get_argument('password', 0)
            database.connect() #连接数据库
            user=Users.get((Users.username==username)&(Users.password==password))
            user_dir={} #数据封装一下
            user_dir['id']=user.id
            user_dir['class_id']=user.class_.id
            user_dir['username']=user.username
            # 我们将一些个人信息通过jwt生成加密字符串,这将是我们的token也就是访问的令牌,身份的象征,唯一的
            user_dir['jwt']=encode_auth_token(user_dir['id'],user_dir['username'],user_dir['class_id'])
            # 返回给请求者
            self.write({"success":user_dir})
        except Users.DoesNotExist:
            self.write({"error":"用户名或者密码错误!"})
        except Exception as e:
            print('login login',e)
            self.write({"error":"服务器出错!"})
        finally:
            database.close()

4、验证

1、我们先建一个用户,因为新建用户没有设置权限验证,因此可以访问

2、我们来根据用户id访问此用户信息,并没有传入jwt

 3、接下来我们登录获取jwt

4、我们将jwt放入请求头,然后访问个人信息

我们什么参数都没有传入,但是服务器根据jwt提取你的id,并且给你返回了信息 

 5、现在我们来测试jwt过期咋办,在这里我们需要将,jwt有效期填小,过期时间设置1分钟。

保存,重新启动程序,我们登录然后等待1分钟过去再次访问信息查询,发现权限过期了

 

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
Tornado可以通过使用JWT(JSON Web Token)来实现同时对App和Web进行Token鉴权。 JWT是一种基于JSON的开放标准(RFC 7519),用于在网络上安全地传输信息。它由三部分组成:Header、Payload和Signature。其中,Header和Payload是Base64编码的JSON字符串,Signature是使用密钥对Header和Payload进行加密后的字符串。 在Tornado中,可以使用第三方库PyJWT来生成和验证JWT。具体步骤如下: 1. 在服务器端生成JWT ```python import jwt # 生成JWT def generate_jwt(user_id): payload = {'user_id': user_id} token = jwt.encode(payload, 'secret_key', algorithm='HS256') return token.decode('utf-8') ``` 2. 在客户端发送请求时,将JWT作为Authorization头部的Bearer Token发送 ```python import requests # 发送请求 def send_request(url, token): headers = {'Authorization': 'Bearer ' + token} response = requests.get(url, headers=headers) return response.json() ``` 3. 在服务器验证JWT ```python import jwt # 验证JWT def verify_jwt(token): try: payload = jwt.decode(token, 'secret_key', algorithms=['HS256']) user_id = payload['user_id'] return user_id except jwt.ExpiredSignatureError: # Token过期 return None except jwt.InvalidTokenError: # Token无效 return None ``` 通过以上步骤,可以实现同时对App和Web进行Token鉴权。在客户端发送请求时,只需要将JWT作为Authorization头部的Bearer Token发送即可。在服务器端,可以使用PyJWT库来验证JWT的有效性,并获取其中的用户信息。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江湖人称王某人的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值