[Vue+Flask]前后端分离JWT用户认证授权

传统Flask通过Flask-Login的login_user()解决登录问题,通过session进行处理,不适合前后端分离系统,所以使用JWT进行用户认证

Session是对于服务端来说的,客户端是没有Session一说的。Session是服务器在和客户端建立连接时添加客户端连接标志,最终会在服务器软件(Apache、Tomcat、JBoss)转化为一个临时Cookie(SessionId)发送给给客户端,当客户端请求时服务器会检查是否携带了这个SessionId(临时Cookie),如果没有则会要求重新登录。

问题:

  1. 如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户SessionId泄露,而作为后端识别用户的标识,cookie 的泄露意味着用户信息不再安全。在设置 cookie 的时候,其实你还可以设置 httpOnly 以及 secure 项。设置 httpOnly 后 cookie 将不能被 JS 读取,浏览器会自动的把它加在请求的 header 当中,设置 secure 的话,cookie 就只允许通过 HTTPS 传输。

  2. httpOnly 选项使得 JS 不能读取到 cookie,那么 XSS 注入的问题也基本不用担心了。但设置 httpOnly 就带来了另一个问题,就是很容易的被 XSRF或CSRF(跨站请求伪造)。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容。因为 cookie 默认被发了出去。

  3. 由于后端保存了所有用户的Session,后端每次都需要根据SessionId查出用户Session进行匹配,加大了服务器端的压力

JWT:

JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:

  1. 简洁(Compact)

    可以通过URL, 参数或者在 HTTP header 发送,因为数据量小,传输速度快

  2. 自包含(Self-contained)

    负载中包含了所有用户所需要的信息,避免了多次查询数据库


    JWT一共由三部分组成,header(头部)、payload(负载)、signature(签名)通过‘.’进行拼接

    006tNc79gy1fbv54tfilmj31120b2wl9

    header(头部) 转Base64

    • alg 加密算法
    • typ 类型

    payload(负载) 自定义信息内容, 不建议存储敏感信息(如密码) 转Base64

    • iss 签发者
    • sub 面向的用户
    • aud 接收jwt的一方
    • exp 过期时间(必须大于签发时间jat)
    • nbf 定义在什么时间之前,该jwt都是不可用的
    • jat 签发时间
    • jti 唯一身份标识,主要用来作为一次性token,从而回避重放攻击

    signature(签名) 一共三部分。转base64的header和转base64的payload拼接之后,然后使用header中声明的加密方式和secret加盐的方式加密字符串

    • 转Base64的header

    • 转Base64的payload

    • secret(私钥)

      最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。


  • 差异比较

Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态。一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存储。

JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分组等。虽说JWT方式让服务器有一些计算压力(例如加密、编码和解码),但是这些压力相比磁盘存储而言可能就不算什么了。具体是否采用,需要在不同场景下用数据说话

  • 单点登录

Session方式来存储用户id,一开始用户的Session只会存储在一台服务器上。对于有多个子域名的站点,每个子域名至少会对应一台不同的服务器,例如:www.taobao.comnv.taobao.comnz.taobao.comlogin.taobao.com。所以如果要实现在login.taobao.com登录后,在其他的子域名下依然可以取到Session,这要求我们在多台服务器上同步Session。使用JWT的方式则没有这个问题的存在,因为用户的状态已经被传送到了客户端。


JWT认证流程

006tNc79gy1fbv63pzqocj30pj0h8t9m

  1. 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
  2. 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形同hhh.ppp.sss的字符串。
  3. 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStoragesessionStorage上,退出登录时前端删除保存的JWT即可。
  4. 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSSXSRF问题)
  5. 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查JWT是否过期;检查JWT的接收方是否是自己
  6. 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。


Vue前端

Vue-router

成功登录时,将后端返回的jwt存入sessionStorage

使用Vue-router在前端每次界面切换前都判断jwt,不符合要求则跳转至login登录界面

// 路由守护
router.beforeEach((to, from, next)=>{
  const accessToken = window.sessionStorage.getItem('accessToken')
  
  if(accessToken) 
  {
      // 重新登录后,转到之前的页面
      if(Object.keys(from.query).length !== 0)
      {
        let redirect = from.query.redirect
        if(to.path === redirect) // 解决无限循环问题
        {
          next()
        }
        else
        {
          next({path:redirect}) // 重新登录后,转到之前的页面
        }
      }
  }

  if(accessToken && to.path !== '/login')
  {
    // 有token 但不是去 login页面
    next()
  }
  else if(accessToken && to.path === '/login')
  {
    //用户已经登陆,不让访问Login登录界面
    next({path: from.fullPath})
  }
  else if(!accessToken && to.path !== '/login')
  {
    // 未登录
    next('/login')
  }
  else
  {
    next()
  }
})

axios

axios 全局配置拦截器

request拦截器每次向后端请求携带header头Authorization信息

// http request 拦截器
axios.interceptors.request.use(
  config =>{
    if(sessionStorage.getItem("accessToken"))
    {
        config.headers.Authorization = sessionStorage.getItem("accessToken")
    }
    return config;
  },
  err => {
    return Promise.reject(err);
  }
)

response拦截器若接收到403错误,则是未登录,无权访问,则清除sessionStorage信息并跳转至login登录界面

/ http response 拦截器
axios.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    if(error.response){
      console.log('axios:' + error.response.status);
      switch(error.response.status){
        case 403:
          // 返回403 清除token信息并跳转到登录页面
          sessionStorage.clear() 
          router.replace({
            path: '/login',
            query: {redirect: router.currentRoute.fullPath}   // 重新登录后,返回之前的页面
          })
          Message({showClose:true, message:'未登录,返回登陆界面', type:'error', duration:3000})  
   
      }
    }
    return Promise.reject(error);   // 返回接口的错误信息
  }
)


Flask后端

  1. 安装PyJWT pip install PyJWT

  2. 编写JWT生成函数与解密函数(util.py)

    key = "123456" # secret私钥,可通过配置文件导入
    
    def generate_access_token(username: str = "", algorithm: str = 'HS256', exp: float = 2):
        """
        生成access_token
        :param username: 用户名(自定义部分)
        :param algorithm: 加密算法
        :param exp: 过期时间
        :return:token
        """
    
        now = datetime.utcnow()
        exp_datetime = now + timedelta(hours=exp)
        access_payload = {
            'exp': exp_datetime,
            'flag': 0,   #标识是否为一次性token,0是,1不是
            'iat': now,   # 开始时间
            'iss': 'leon',   # 签名
            'username': username   #用户名(自定义部分)
        }
        access_token = jwt.encode(access_payload, key, algorithm=algorithm)
        return access_token
    
    
    def decode_auth_token(token: str):
        """
        解密token
        :param token:token字符串
        :return:
        """
        try:
            payload = jwt.decode(token, key=key, algorithms='HS256')
        except (jwt.ExpiredSignatureError, jwt.InvalidTokenError, jwt.InvalidSignatureError):
            return ""
        else:
            return payload
    
    
    def identify(auth_header: str):
        """
        用户鉴权
        """
    
        if auth_header:
            payload = decode_auth_token(auth_header)
            if not payload:
                return False
            if "username" in payload and "flag" in payload:
                if payload["flag"] == 0:
                    return payload["username"]
                else:
                    return False
        return False
    
  3. 编写登录保护函数(util.py)

    def login_required(f):
        """
        登录保护,验证用户是否登录
        :param f:
        :return:
        """
    
        @wraps(f)
        def wrapper(*args, **kwargs):
            token = request.headers.get("Authorization", default=None)
            if not token:
                return 'not Login','403 Permission Denied'
            username = identify(token)
            if not username:
                return 'not Login','403 Permission Denied'      # return 响应体, 状态码, 响应头
            return f(*args, **kwargs)
        return wrapper
    

tips: Flask的Response常用返回 return 响应体, 状态码, 响应头



总结

相关项目

参考链接

原文地址:https://www.cnblogs.com/leon7/p/14287566.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Jinja2:是Python的Web项目中被广泛应用的模板引擎,是由Python实现的模板语言,Jinja2 的作者也是 Flask 的作者。他的设计思想来源于Django的模板引擎,并扩展了其语法和一系列强大的功能,其是Flask内置的模板语言。
Fullcalendar日历使用,包括视图选择、事件插入、编辑事件、事件状态更改、事件添加和删除、事件拖动调整,自定义头部,加入el-popover显示图片、图片预览、添加附件链接等,支持手机显示。
监听QQ消息并不需要我们写代码,因为市面上已经有很多开源QQ机器人框架,在这里我们使用go-cqhttp官方文档:go-cqhttp如果您感兴趣的话,可以阅读一下官方文档,如果不想看,直接看我的文章即可。
【Flask框架】—— 视图和URL总结
python+web+flask轻量级框架的实战小项目。登录功能,后续功能可自行丰富。
有了这个就可以配置可信IP,关键是不需要企业认证,个人信息就可以做。
本专栏是对Flask官方文档中个人博客搭建进行的归纳总结,与官方文档结合事半功倍。 本人经验,学习一门语言或框架时,请首先阅读官方文档。学习完毕后,再看其他相关文章(如本系列文章),才是正确的学习道路。
本专栏是对Flask官方文档中个人博客搭建进行的归纳总结,与官方文档结合事半功倍。基础薄弱的同学请戳Flask官方文档教程 本人经验,学习一门语言或框架时,请首先阅读官方文档。学习完毕后,再看其他相关文章(如本系列文章),才是正确的学习道路。 如果python都完全不熟悉,一定不要着急学习框架,请首先学习python官方文档,一步一个脚印。要不然从入门到放弃是大概率事件。 Python 官方文档教程
快到年末了 相信大家都在忙着处理年末数据 刚好有一个是对超市的商品库存进行分析的学员案例 真的非常简单~
一个简易的问答系统就这样完成了,当然,这个项目还可以进一步完善,比如 将数据存入Elasticsearch,通过它先进行初步的检索,然后再通过这个系统,当然我们也可以用其他的架构实现。如果你对这系统还有其他的疑问,也可以再下面进行留言!!!
#模版继承和页面之间的调用@app.route("/bl")def bl(): return render_template("file_2.html")主ht
#form表达提交@app.route("/data",methods=['GET','POST']) #methods 让当前路由支持GET 和
#form表达提交@app.route("/data",methods=['GET','POST']) #methods 让当前路由支持GET 和
#session 使用app.secret_key = "dsada12212132dsad1232113"app.config['PERMANENT_SESSION_LI
#文件上传@app.route("/file",methods=['GET','POST'])def file(): if request.meth
#跳转操作:redirect@app.route("/red")def red(): return redirect("/login")
#session 使用app.secret_key = "dsada12212132dsad1232113"app.config['PERMANENT_SESSION_LI
@app.route("/req",methods=['GET','POST'])def req(): print(request.headers)
#模版继承和页面之间的调用@app.route("/bl")def bl(): return render_template("file_2.html")主ht
#文件操作:send_file,支持图片 视频 mp3 文本等@app.route("/img")def img(): return send_file("1.jpg&q