常见登录认证 DEMO

JS菌
⭐️ 更多前端技术和知识点,搜索订阅号 JS 菌 订阅

20190724002503.png

basic auth

basic auth 是最简单的一种,将用户名和密码通过 form 表单提交的方式在 Http 的 Authorization 字段设置好并发送给后端验证

要点:

  • 不要通过 form 提交表单的默认方式发送请求,转而使用 fetch 或 ajax
  • 客户端注意设置 Authorization 字段的值为 'Basic xxx',通过该 Http 字段传递用户名密码
  • base64 的方法在客户端要注意兼容性 btoa ,建议使用现成的库如 'js-base64' 等,NodeJS 方面使用全局的 Buffer
  • 服务端验证失败后,注意返回 401,但不用返回 'WWW-Authenticate: Basic realm="..."' 避免浏览器出现弹窗
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>AMD</title>
</head>

<body>
  <script defer async="true" src="js/require.js" data-main="js/main"></script>
  <!-- BasicAuth -->
  <div>
    <form id="form" action="">
      <input type="text" name="username" id="username">
      <input type="password" name="password" id="password">
      <button id="login">login</button>
    </form>
  </div>
</body>

</html>
require.config({
  baseUrl: 'js/libs',
  paths: {
    'zepto': 'zepto.min',
  },
  shim: {
    'zepto': 'zepto',
  }
});

define(['zepto'], function ($) {
  let $form = $('#form')
  $form.on('submit', (e) => {
    e.preventDefault()
    $.ajax({
      // ajax 发送验证请求
      type: 'POST',
      url: '/login',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': 'Basic ' + btoa($('#username').val() + ':' + $('#password').val()),
        // 通过 Authorization 传递 base64 编码后的用户名密码
      },
      success: function (data) {
        console.dir(data) // 回调
      }
    })
  })
});

(忽略上述 ajax 加 requirejs 古老的写法 ? )

const Koa = require('koa')
const static = require('koa-static')
const router = require('koa-better-router')().loadMethods()
const koaBody = require('koa-body')

const app = new Koa()
app.use(koaBody())
app.use(router.middleware())
app.use(static('public'))
app.listen(8080)

router.post('/login', (ctx, next) => {
  // 省略从数据库中提取用户密码
  if (ctx.get('Authorization') === 'Basic ' + Buffer('fdsa:fdsa').toString('base64')) {
    // 获取 Authorization 字段 比对 base64 用户名密码
    ctx.body = 'secret'
    ctx.type = 'text/html'
    ctx.status = 200 // 匹配成功
  } else {
    ctx.status = 401 // 匹配失败
  }
  next()
})

cookie auth

这种登录方式实际上就是验证用户信息后,将验证 session 存放在 session cookie 内。一旦过期就需要用户重新登录

要点:

  • session cookie 用户信息容易被截取,需要设置 https
  • session 的会话时间内 cookie 有效,如需要长时生效需要设置过期时间 Max-age, Expires 等
const Koa = require('koa')
const static = require('koa-static')
const router = require('koa-better-router')().loadMethods()
const koaBody = require('koa-body')
const fs = require('fs')

const app = new Koa()
app.listen(8080)
app.use(koaBody())
app.use(router.middleware())
app.use(static('public'))

router.post('/login', (ctx, next) => {
  // 省略从数据库中提取用户密码
  let auth = ctx.request.body
  if (auth.username === 'fdsa', auth.password === 'fdsa') {
    // session cookie验证的用户名和密码属于明文传输,需要 https
    ctx.cookies.set('auth', auth.username) // 没有设置过期时间,属于Session Cookie
    // Koa 服务端默认设置的 cookie 是 session cookie
    ctx.status = 200
    ctx.type = 'application/json'
    ctx.body = { data: 1 }
    next()
  } else {
    ctx.status = 401
    next()
  }
})

router.get('/admin', (ctx, next) => {
  if (ctx.cookies.get('auth')) {
    ctx.body = 'secret'
    ctx.status = 200
    next()
  }
})

SessionSigned Cookie Auth

目前常用的方法,针对 cookie Auth 的改进

要点:

  • 经过签名的 Cookie 安全性提高,要注意加强对签名的密钥的保护
  • 可通过每次访问受权限限制的页面刷新 SessionCookie
  • Koa 建议使用 koa-session 库
const Koa = require('koa')
const static = require('koa-static')
const router = require('koa-better-router')().loadMethods()
const koaBody = require('koa-body')
const session = require('koa-session'); // session

const app = new Koa()
app.listen(8080)
app.use(koaBody())
app.use(router.middleware())
app.use(static('public'))
app.keys = ['session key'] // 签名
app.use(session({
  key: '_session',
  signed: true, // 签名,经过签名的 cookie 安全性比普通 cookie 高
  maxAge: 'session' // 设置过期时间 session 表示当前会话有效
}, app))

router.post('/login', (ctx, next) => {
  // 省略从数据库中提取用户密码
  let auth = ctx.request.body
  if (auth.username === 'fdsa', auth.password === 'fdsa') {
    // 登陆成功,username 结合签名放入到 session cookie 中用于将来鉴别身份
    ctx.session.user = auth.username
    ctx.status = 200
    ctx.type = 'application/json'
    ctx.body = { data: 1 }
    next()
  } else {
    ctx.status = 401
    next()
  }
})

router.get('/admin', (ctx, next) => {
  if (ctx.session.user === 'fdsa') {
    let count = ctx.session.count || 0
    // 每次都将刷新 session cookie 存在客户端的 session cookie 会随着刷新动作而变化
    ctx.session.count = ++count
    ctx.body = 'visit count: ' + count
    ctx.status = 200
    next()
  } else {
    ctx.status = 401
    next()
  }
})

JWT token auth

此种令牌登录方式比较主流,用户输入登录信息,发送给服务器验证,通过后返回 token,token 可以存储在前端任何地方。随后用户请求需要验证的资源,发送 http 请求的同时将 token 放置在请求头中,后端解析 JWT 并判断令牌是否新鲜并有效

要点:

  • 用户输入其登录信息
  • 服务器验证信息是否正确,并返回已签名的token
  • token储在客户端,常见的是存储在local storage中,但也可以存储在session或cookie中
  • 之后的HTTP请求都将token添加到请求头里
  • 服务器解码JWT,并且如果令牌有效,则接受请求
  • 一旦用户注销,令牌将在客户端被销毁,不需要与服务器进行交互一个关键是,令牌是无状态的。后端服务器不需要保存令牌或当前session的记录。

1. 基本介绍

认证流程 https://jothy1023.github.io/2016/11/04/server-authentication-using-jwt/

首先,拥有某网站账号的某 client 使用自己的账号密码发送 post 请求 login,由于这是首次接触,server 会校验账号与密码是否合法,如果一致,则根据密钥生成一个 token 并返回,client 收到这个 token 并保存在本地的 localStorage。在这之后,需要访问一个受保护的路由或资源时,而只要附加上你保存在本地的 token(通常使用 Bearer 属性放在 Header 的 Authorization 属性中),server 会检查这个 token 是否仍有效,以及其中的校验信息是否正确,再做出相应的响应。

优点是自包含不需要服务端储存、无状态客户端销毁即可实现用户注销,以及跨域、易于实现CDN,比cookie更支持原生移动端应用

JWT 的三个部分:header头, payload载荷, signature签名,即:xxx.yyy.zzz

header部分(base64之前):

{
    "alg": "SHA256", // algorithm 哈希算法主要有 HMAC、SHA256、RSA等等
    "typ": "JWT" // type 令牌类型,应当设置为 JWT
}

payload部分(base64之前):

三种payload声明类型:registered, public, private,其中,registered 还包括 iss(issuer),sub(subject),aud(audience),exp(expiration time),nbf(not before),iat(issued at),jti(JWT ID)

{
    "sub": "subject id",
    "exp": "1300819380",
    "role": "admin"
}

signature部分

如果使用 HMACSHA256 方式:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

这三个部分之间加入.即完成了JWT的构造

需要注意,header部分和payload部分只是经过了base64的编码,并未加密,不能在载荷部分保存涉及安全的东西

JWT 令牌通常通过 HTTP 的 Authorization: Bearer <token> 来传输,并存储在 session cookie, localStorage 等地方

2. 例子

<!-- JWT Token SessionCookie Auth -->
  <div>
    <form id="form" action="">
      <input type="text" name="username" id="username">
      <input type="password" name="password" id="password">
      <button id="login">login</button>
    </form>
  </div>
  <!-- JWT Token LocalStorage Auth -->
  <div>
    <pre id="pre"></pre>
    <button id="getData">getData</button>
  </div>

server:

const Koa = require('koa')
const static = require('koa-static')
const router = require('koa-better-router')().loadMethods()
const koaBody = require('koa-body')
const jwt = require('jsonwebtoken')
const fs = require('fs')

const app = new Koa()
app.listen(8080)
app.use(koaBody())
app.use(router.middleware())
app.use(static('public'))
app.keys = ['private key']

router.post('/login', (ctx, next) => {
  // 省略从数据库中提取用户密码
  if (ctx.request.body) {
    if (ctx.request.body.username === 'fdsa', ctx.request.body.password === 'fdsa') {
      // 生成 jwt token
      let token = jwt.sign({ username: 'fdsa', role: 'admin' }, app.keys[0], { algorithm: 'HS256' })
      ctx.cookies.set('koa:token', token)
      ctx.body = { data: 1, token }
      ctx.status = 200
    } else {
      ctx.body = { data: 0, err: 'error' }
      ctx.status = 401
    }
  } else {
    ctx.status = 401
  }
  next()
})

// 通过 session cookie 验证令牌
router.get('/admin', (ctx, next) => {
  let token = ctx.cookies.get('koa:token')
  if (token) {
    // 验证 jwt 令牌
    jwt.verify(token, app.keys[0], function (err, decoded) {
      if (err) {
        ctx.status = 401
        console.log(err)
      } else {
        ctx.body = `welcome ${decoded.role}, ${decoded.username}`
        ctx.type = 'text/html'
        ctx.status = 200
      }
    });
  } else {
    ctx.status = 401
  }
})

// 通过 Authorization 验证令牌
router.get('/secret.json', (ctx, next) => {
  let token = ctx.get('Authorization').split(' ')[1]
  if (token) {
    jwt.verify(token, app.keys[0], function (err, decoded) {
      if (err) {
        ctx.status = 401
        console.log(err)
      } else {
        if (decoded.role === 'admin') {
          let msg = fs.readFileSync('./secret.json', 'utf-8')
          ctx.body = { data: 1, msg }
          ctx.status = 200
        } else {
          ctx.status = 401
        }
      }
    })
  } else {
    ctx.status = 401
  }
})

client:

require.config({
  baseUrl: 'js/libs',
  paths: {
    'zepto': 'zepto.min',
  },
  shim: {
    'zepto': 'zepto',
  }
});

(在此忽略此前写的古老的 requireJS ?)

define(['zepto'], function ($) {
  let $form = $('#form')
  $form.on('submit', (e) => {
    e.preventDefault()
    $.ajax({
      // ajax 发送验证请求
      type: 'POST',
      url: '/login',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      data: {
        username: $('#username').val(),
        password: $('#password').val()
      },
      success: function (data) {
        if (data.data === 1) {
          // 返回的token用于发起请求受限资源
          window.localStorage.setItem('koa:token', data.token)
          location.replace('./admin')
        }
      }
    })
  })

  $('#getData').on('click', (e) => {
    e.preventDefault()
    $.ajax({
      type: 'GET',
      url: '/secret.json',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': 'Bearer ' + window.localStorage.getItem('koa:token')
        // 客户端设置 Authorization Token 令牌
      },
      success: function (data) {
        if (data.data === 1) {
          // 令牌认证后的操作
          $('#pre').text(JSON.parse(data.msg).key)
        }
      }
    })
  })
});

OAuth

OAuth 是目前用的最多的登录认证方式,用户首先确认授权登录,通过一连串方法获取 access token,最后通过 token 请求各种受限的资源

阮一峰老哥的文章清除讲解了这种方法的工作方式:

原理:理解OAuth 2.0 http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html

要点:

  • 用户首先确认授权
  • 再获取 code 临时凭证
  • 通过 code 临时凭证,换取 access token
  • 最后由 token 再获取受限的资源

下面封装了一个基于微博的 OAuth 认证:

let axios = require('axios');

const Koa = require('koa')
const static = require('koa-static')
const router = require('koa-better-router')().loadMethods()
const koaBody = require('koa-body')
const jwt = require('jsonwebtoken')
const fs = require('fs')

const app = new Koa()
app.listen(8080)
app.use(koaBody())
app.use(router.middleware())
app.use(static('public'))

app.keys = ['appid', 'secretid']

class WeiboApi {
  // 获取 code 临时兑换券
  constructor(query) {
    this.code = query.code
  }
  // 根据 code 获取 token
  getToken() {
    return new Promise((resolve, reject) => {
      axios({
        method: 'POST',
        url: `https://api.weibo.com/oauth2/access_token?client_id=${app.keys[0]}&client_secret=${app.keys[1]}&grant_type=authorization_code&redirect_uri=http://127.0.0.1:8080/auth&code=${this.code}`
      }).then(d => { resolve(d) }).catch(e => { reject(e) })
    })
  }
  // 根据 token 获取 相关的用户信息
  getUserInfo(token) {
    return new Promise((resolve, reject) => {
      axios({
        method: 'GET',
        url: `https://api.weibo.com/2/users/show.json?access_token=${token.data.access_token}&uid=${token.data.uid}`
      }).then(d => { resolve(d) }).catch(e => { reject(e) })
    })
  }
  // 根据 token 获取 用户的关注人列表
  getUserFriends(token) {
    return new Promise((resolve, reject) => {
      axios({
        method: 'GET',
        url: `https://api.weibo.com/2/friendships/friends.json?access_token=${token.data.access_token}&uid=${token.data.uid}`
      }).then(d => { resolve(d) }).catch(e => { reject(e) })
    })
  }
}

router.get('/auth', async (ctx, next) => {
  if (ctx.query.code) {
    let weiboApi = new WeiboApi(ctx.request.query)
    let token = await weiboApi.getToken()
    let userInfo = await weiboApi.getUserInfo(token)
    let userFriends = await weiboApi.getUserFriends(token)
    // 根据用户信息,查询数据库,登录逻辑
    ctx.body = { userInfo: userInfo.data, userFriends: userFriends.data }
  } else {
    ctx.status = 401
  }
})
<!-- OAuth2.0 Weibo -->
  <a href="https://api.weibo.com/oauth2/authorize?client_id=HEREISYOURAPPID&response_type=code&redirect_uri=http://127.0.0.1:8080/auth">微博登录</a>

JS 菌公众账号

请关注我的订阅号,不定期推送有关 JS 的技术文章,只谈技术不谈八卦 ?

阅读 2.8k

前端和Node学习笔记
前端和Node学习笔记
6.4k 声望
2k 粉丝
0 条评论
6.4k 声望
2k 粉丝
文章目录
宣传栏