最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 登录系统实现

    正文概述 掘金(B_Cornelius)   2021-03-11   470

    对于前端来说,登录就是把用户信息提交上去,后续就不用前端去担心了。但是做过一个登陆sdk的项目,发现这里边的逻辑不是那么简单。下面是我对登陆的一些理解分享给大家

    session & JWT

    http协议是无状态的,它不能以状态来区分和管理请求和响应。也就是说,如果用户通过账号和密码来进行用户认证后,在下次请求时,用户还需要在再次进行用户认证。因为根据http协议,服务端并不知道是哪个用户发起的请求。为了识别当前的用户,服务端与客户端需要约定某个标识表示当前的用户

    session

    为了识别是哪个用户发出的请求,需要在服务端存储一份用户登录的信息,这份登录信息会在响应传递给客户端进行存储,当下次请求的时候客户端会携带登录信息请求服务端,服务端就能够区分请求是哪个用户发起的 下面是示意图: 登录系统实现session方案中,请求服务端时会携带session_id,服务端会通过当前的session_id,去查询数据库当前session是否有效,如果有效后续请求就能够标识当前用户。

    如果当前的session是无效的或者是不存在的,客户端需要重定向到登录页面,或者提示没有登录 下面是对应的代码:

    const express = require('express');
    const session = require('express-session')
    const redis = require('redis')
    const connect = require('connect-redis')
    const bodyParser = require('body-parser')
    
    const app = express();
    app.use(bodyParser.json());
    
    app.use(bodyParser.urlencoded({ extended: true }))
    
    const RedisStore = connect(session);
    
    const client = redis.createClient({
      host: '127.0.0.1',
      port: 6397
    })
    
    app.use(session({
      store: new RedisStore({
        client,
      }),
      secret: 'sec_id',
      resave: false,
      saveUninitialized: false,
      cookie: {
        secure: true,
        httpOnly: true,
        maxAge: 1000 * 60 * 10
      }
    }))
    
    
    app.get('/', (req, res) => {
      sec = req.session;
      if (sec.user) {
        res.json({
          user: sec.user
        })
      } else {
        res.redirect('/login')
      }
    })
    
    
    app.post('/login', (req, res) => {
      const {pwd, name } = req.body;
      // 这里为了简便,就写简单点
      if (pwd === name) {
        req.session.user = req.body.name;
        res.json({
          message: 'success'
        })
      }
    })
    

    当请求/接口的时候,会判断当前session是否存在。如果存在,就返回对应的信息;如果不存在,则会重定向到/login页面。这个页面登录成功以后,就会设置session

    上面代码中只考虑了单个服务的场景,但是业务中往往是多个服务,服务域名不一样,由于cookie不能跨域,所以session的共享会存在一定问题 登录系统实现 例如有上面场景中,用户首先请求服务Auth Server,然后生成session。当用户再次请求服务feedback Server时,由于session不共享,就导致服务B拿不到登陆态,就需要重新登录。

    session的缺点

    session用于解决鉴权,存在一些缺点:

    1. 多集群支持: 当网站采用集群部署的时候,会遇到多台web服务器之间如何做session共享的问题。因为session是由单个服务创建,处理请求的服务器可能不是创建session的服务器,那么该服务器就无法拿到之前放入到session中的登录凭证之类的信息
    2. 性能差: 当流量高峰期时,由于每个请求的用户信息都需要存储在数据库中,对资源会是一种负担
    3. 低扩展性:当扩容服务端的时候,session store也需要扩容。这会占用额外的资源和增加复杂性

    JWT

    session服务中,服务器需要维护用户的session对象,要么前置一个服务,要么每个服务都从存储层中获取session信息,请求量大的时候IO压力大。

    相比于session服务,把用户信息存放在客户端,每次请求的时候随cookiehttp头部渠道发送到服务器上,就可以让服务器变成无状态的存在,从而减轻服务器的压力。 登录系统实现
    相比于浏览器,Native App设置cookie没有那么容易,所以服务端需要采用另外一种认证方式。在登录后,服务端会根据登录信息生成一个token值,后续的请求客户端请求会携带token值进行登录校验。

    jwt主要由三部分构成: 头部信息(header)、消息体(payload)和签名(signature) 头信息指定了JWT的签名算法

    header = {
      alg: "HS256",
      type: "JWT"
    }
    

    HS256表示使用了 HMAC-SHA256 来生成签名 消息体包含了JWT的意图:

    payload = {
      "loggedInAs": "admin",
      "iat": 1422779638
    }
    

    未签名的令牌由base64url编码的头信息和消息体拼接而成,签名则通过私有的key计算而成:

    key = 'your_key'
    unsignedToken = encodeBase64(header) + "." + encodeBase64(payload)
    signature = HAMC-SHA256(key, unsignedToken)
    

    最后在未签名的令牌尾部拼接上base64url编码的签名就是JWT了:

    token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)
    

    具体实现

    首先创建app.js,用于获取请求参数,还有监听端口等等

    // app.js
    require('dotenv').config();
    const express = require('express');
    const bodyParser = require('body-parser')
    const cookieParser = require('cookie-parser');
    const router = require('./router');
    const app = express();
    
    app.use(bodyParser.json())
    app.use(cookieParser);
    
    app.use(bodyParser.urlencoded({ extended: true }))
    
    router(app);
    
    
    app.listen(3001, () => {
      console.log('server start')
    })
    

    dotenv主要用于配置环境变量,创建.env文件,下面是本示例的配置:

    ACCESS_TOKEN_SECRET=swsh23hjddnns
    ACCESS_TOKEN_LIFE=1200000
    

    然后注册login接口,这个接口提交用户信息到server,后端会用这些信息生成对应的token,可以直接返回给客户端或者设置cookie

    // user.js
    const jwt = require('jsonwebtoken')
    
    function login(req, res) {
      const username = req.body.username;
    
      const payload = {
        username,
      }
      
      const accessToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, {
        algorithm: "HS256",
        expiresIn: process.env.ACCESS_TOKEN_LIFE
      })
    
      res.cookie('jwt', accessToken, {
        secure: true,
        httpOnly: true,
      })
      res.send();
    }
    

    当登录成功以后直接设置客户端的cookie

    下次请求的时候,服务端直接获取用户的jwt cookie,判断当前token是否是有效的:

    //middleware.js
    const jwt = require('jsonwebtoken');
    
    exports.verify = function(req, res, next) {
      const accessToken = req.cookies.jwt;
    
      try {
        jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET);
        next();
      } catch (error) {
        console.log(error);
        return res.status(401).send();
      }
    }
    

    相对于session的方式,jwt具有以下优势:

    1. 扩展性好:在分布式部署场景下,session需要数据共享,而jwt不需要
    2. 无状态: 不需要在服务端存储任何状态

    jwt也存在一些缺点:

    1. 无法废弃: 在签发后,在到期之前会始终有效,无法中途废弃。
    2. 性能差: session方案中,cookie需要携带的sessionId是一个很短的字符串。但是由于jwt是无状态的,需要携带一些必要的信息,体积会比较大。
    3. 安全性:jwt中的payload是base64编码的,没有加密,因此不能存储敏感数据
    4. 续签: 传统的cookie续签方案都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。如果要改变jwt的有效时间,就需要签发新的jwt。一种方案是每次请求都更新jwt,这样性能太差了;第二种方案为每个jwt设置过期时间,每次访问刷新jwt的过期时间,就失去了jwt无状态的优势了。

    session和jwt的适用场景

    适合适用jwt的场景:

    • 有效期短
    • 只希望被使用一次

    例如在请求服务A的时候,服务A会颁发一个很短过期时间的JWT给浏览器,浏览器可以当前的jwt去请求服务B,服务B则可以通过校验JWT来判断当前用户是否有权操作。 由于jwt具有无法废弃的特性,单点登录和会话管理非常不适合用jwt。

    单点登录(SSO)

    sso通常处理的是一个公司的不同应用间的访问登录问题。如企业应用有很多业务子系统,只需要登录一个系统,就可以实现不同子系统间的跳转,而避免了登录操作。 这里举个例子进行说明: 子系统A统一到passport域名登录,并且在passport域名下种上cookie,然后把token加入到url中,重定向到子系统A 回到子系统A后,使用token再次去passport验证,如果验证通过返回必要的信息生成系统A的session 当系统A下次请求的时候会当前服务已有session,不会再去passport去权限校验 当访问系统B的时候,由于系统B不存在session,所以会重定向到passport域名,passport域名下面已经有cookie了,所以不需要登录,直接把token加入到url中,重定向到子系统B,后续流程和A一样

    实现原理

    以腾讯为例,腾讯旗下有多个域名,例如: cd.qq.com、tencent.com、jd.cm、music.qq.com 在cd.qq.commusic.qq.com,我们可以设置cookiedomianqq.com实现cookie的共享。 但是如cd.qq.comtencent.com二级域名不一致,让所有的域名都能共享一个cookie。所以希望有一个通用的服务去承载这个登录服务。例如在腾讯有这样一个域名: passport.tencent.com用于专门登录服务的承载。这个时候cd.qq.comtencent.com的登录登出都由sso(passport.baidu.com)来实现

    具体实现

    成功登录SSO会生成token跳转到源页面,此时SSO已经有登录状态,但是子系统仍然没有登录态。子系统需要通过token设置当前子系统的登录态,并通过当前的token请求passport服务获取用户的基本信息。 下面主要讲三个部分 passport: 登录服务,域名为passport.com system: 子系统,监听端口3001为系统A,监听端口3002为系统B,域名分别为a.comb.com

    passport服务

    passport主要有以下几个功能:

    1. 统一登录服务
    2. 获取用户信息
    3. 校验当前的token是否是有效的

    首先实现登录页面的一些逻辑:

    // passport.js
    import express from 'express';
    import session from 'express-session';
    import bodyParser from 'body-parser';
    import cookieParser from 'cookie-parser';
    import connect from 'connect-redis';
    import redis from '../redis';
    
    const app = express();
    app.use(bodyParser.json());
    
    app.use(bodyParser.urlencoded({ extended: true }));
    app.use(cookieParser());
    
    app.set('view engine', 'ejs');
    app.set('views', `${__dirname}/views`);
    
    const RedisStore = connect(session);
    
    
    app.use(
      session({
        store: new RedisStore({
          client: redis,
        }),
        secret: 'token',
        resave: false,
        saveUninitialized: false,
        cookie: {
          secure: true,
          httpOnly: true,
          maxAge: 1000 * 60 * 10,
        },
      })
    );
    
    app.get('/', (req, res) => {
      const { token } = req.cookies;
      if (token) {
        const { from } = req.query;
        const has_access = await redis.get(token);
        if (has_access && from) {
          return res.redirect(`https://${from}?token=${token}`);
        }
        // 如果不存在便引导至登录页重新登录
        return res.render('index', {
          query: req.query,
        });
      }
      return res.render('index', {
        query: req.query,
      });
    })
    app.port('/login', (req, res) => {
      const { name, pwd, from } = req.body;
    
      if (name === pwd) {
        const token = `${new Date().getTime()}_${ name}`;
        redis.set(token, name);
        res.cookie('token', token);
        if (from) {
          return res.redirect(`https://${from}?token=${token}`);
        }
      } else {
        console.log('登录失败');
      }
    })
    

    /接口首先判断passport是否已经有登录成功的token,如果存在就在去存储中查找当前token是否是有效的。如果有效并且参数中携带from参数,那么就跳转到原页面并且把生成的token值带回到原页面。

    下面是passport页面的样式: 登录系统实现 登录接口需要做的就是登录成功后设置passport域名的token,然后重定向到之前的页面

    子系统实现

    import express from 'express';
    import axios from 'axios';
    import session from 'express-session';
    import bodyParser from 'body-parser';
    import connect from 'connect-redis';
    import cookieParser from 'cookie-parser';
    import redisClient from "../redis";
    import { argv } from 'yargs';
    const app = express();
    
    const RedisStore = connect(session);
    app.use(bodyParser.json());
    
    app.use(bodyParser.urlencoded({ extended: true }));
    app.use(cookieParser('system'));
    
    app.use(session({
      store: new RedisStore({
        client: redisClient,
      }),
      secret: 'system',
      resave: false,
      name: 'system_id',
      saveUninitialized: false,
      cookie: {
        httpOnly: true,
        maxAge: 1000 * 60 * 10
      }
    }))
    
    
    app.get('/', async (req, res) => {
      const { token } = req.query;
      const { host } = req.headers;
      // 如果本站已经存在凭证,便不需要去passport鉴权
      if (req.session.user) {
        return res.send('user success')
      }
    
      // 如果没有本站信息,有没有token,便去passport登录鉴权
      if (!token) {
        return res.redirect(`http://passport.com?from=${host}`)
      }
    
      const {data} = await axios.post('http://127.0.0.1:3000/check',{
        token,
      })
      // 验证成功
      if (data?.code === 0) {
        const user = data?.user;
        req.session.user = user;
      } else {
        // 验证失败
        return res.redirect(`http://passport.com?from=${host}`)
      }
      return res.send('page has token')
    })
    app.listen(argv.port, () => {
      console.log(argv.port);
    })
    

    首先判断当前子系统是否已经登录了,如果当前系统session已经存在,就返回user success。如果没有登录并且url上携带token参数,就需要跳转到passport.com登录。

    如果token存在,并且当前子系统没有登录,就需要使用当前页面的token去请求passport服务,判断这个token是否有效的,如果有效就返回相应的信息,并且设置session

    这里系统A和系统B只是监听的接口不同,所以在启动参数中添加变量获取启动端口

    passport鉴权服务

    app.get('/check', (req, res) => {
      const { token } = req.query;
      if (!token) {
        return res.json({
          code: 1
        })
      }
      const user = await redis.getAsync(token);
      if (user) {
        return res.json({
          code: 0,
          user,
        })
      } else {
        return res.redirect('passport.com')
      }
    })
    

    check接口就是判断请求服务的token是否是有效的,如果有效就返回对应的用户信息,如果无效就重定向到passport.com重新登录

    OAuth

    OAuth协议被广泛应用于第三方授权登录中,借助第三方登录可以让用户规避再次登录的问题。

    github授权为例,讲解OAuth的授权过程:

    1. 访问服务A,服务A没有登录,可以通过github第三方登录
    2. 点击github,跳转到认证服务器。然后询问是否授权
    3. 授权完成后,会重定向到服务A的一个路径,并且携带参数code
    4. 服务A通过code去请求github,获取到token
    5. 通过token值,再去请求github资源服务器获取到你想要的的数据

    首先去github-auth申请一个auth应用,例如以下: 登录系统实现

    执行后会得到对应的client_idclient_secret。下面是具体的授权代码(启动服务就不写,大同小异):

    import { AuthorizationCode } from 'simple-oauth2';
    const config = {
      client: {
        id: 'client_id',
        secret: 'client_secret'
      },
      auth: {
        tokenHost: 'https://github.com',
        tokenPath: '/login/oauth/access_token',
        authorizePath: '/login/oauth/authorize'
      }
    }
    
    const client = new AuthorizationCode(config);
    const authorizationUri = client.authorizeURL({
      redirect_uri: 'http://localhost:3000/callback',
      scope: 'notifications',
      state: '3(#0/!~'
    });
    
    app.set('view engine', 'ejs');
    app.set('views', `${__dirname}/views`);
    
    app.get('/auth', (_, res) => {
      res.redirect(authorizationUri)
    })
    

    上面使用了simple-oauth2用于oauth2的讲解,当访问localhost:3000/auth的时候,服务会自动跳转到github的认证地址下面是具体的地址

    https://github.com/login/oauth/authorize?response_type=code&client_id=86f4138f17d0c3033ca4&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=notifications&state=3(%230%2F!~
    

    当点击授权后会重定向到localhost:3000/callback,并且url上携带参数code。下面是服务端的处理函数

    async function getUserInfo(token) {
      const res = await axios({
        method: 'GET',
        url: 'https://api.github.com/user',
        headers: {
          Authorization: `token ${token}`
        }
      })
      return res.data;
    }
    
    app.get('/callback', async (req, res) => {
      const { code } = req.query;
      console.log(code);
      // 获取token
    
      const options = {
        code,
      }
    
      try {
        const access = await client.getToken(options);
        const resp = await getUserInfo(access.token.access_token);
        return res.status(200).json({
          token: access.token,
          user: resp,
        });
      } catch (error) {
        
      }
    })
    

    根据url上参数code获取到token,然后根据这个token去请求github api服务,获取到用户信息,通常网站会根据当前获取到的用户信息完成注册、加session等一系列操作。上面代码中,把用户请求数据简单返回给返回给前端,下面是最后返回给前端的数据格式: 登录系统实现 最后就实现了第三方的登录授权

    参考文档

    medium.com/@siddhartha…
    livecodestream.dev/post/a-prac…
    medium.com/myplanet-mu…


    起源地下载网 » 登录系统实现

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元