先后端常见的几种鉴权方式

1、四种鉴权方式

目前咱们经常使用的鉴权有四种:javascript

  • HTTP Basic Authentication
  • session-cookie
  • Token 验证
  • OAuth(开放受权)

2、HTTP Basic Authentication

这种受权方式是浏览器遵照http协议实现的基本受权方式,HTTP协议进行通讯的过程当中,HTTP协议定义了基本认证容许HTTP服务器对客户端进行用户身份证的方法。html

认证过程:前端

一、 客户端向服务器请求数据,请求的内容多是一个网页或者是一个ajax异步请求,此时,假设客户端还没有被验证,则客户端提供以下请求至服务器:java

Get /index.html HTTP/1.0 
  Host:www.google.com
复制代码

二、 服务器向客户端发送验证请求代码401,(WWW-Authenticate: Basic realm=”google.com”这句话是关键,若是没有客户端不会弹出用户名和密码输入界面)服务器返回的数据大抵以下:node

HTTP/1.0 401 Unauthorised 
  Server: SokEvo/1.0 
  WWW-Authenticate: Basic realm=”google.com” 
  Content-Type: text/html 
  Content-Length: xxx
复制代码

三、 当符合http1.0或1.1规范的客户端(如IE,FIREFOX)收到401返回值时,将自动弹出一个登陆窗口,要求用户输入用户名和密码。jquery

四、 用户输入用户名和密码后,将用户名及密码以BASE64加密方式加密,并将密文放入前一条请求信息中,则客户端发送的第一条请求信息则变成以下内容:ios

Get /index.html HTTP/1.0 
  Host:www.google.com 
  Authorization: Basic d2FuZzp3YW5n
复制代码

注:d2FuZzp3YW5n表示加密后的用户名及密码(用户名:密码 而后经过base64加密,加密过程是浏览器默认的行为,不须要咱们人为加密,咱们只须要输入用户名密码便可)git

五、 服务器收到上述请求信息后,将 Authorization 字段后的用户信息取出、解密,将解密后的用户名及密码与用户数据库进行比较验证,如用户名及密码正确,服务器则根据请求,将所请求资源发送给客户端程序员

效果: 客户端未未认证的时候,会弹出用户名密码输入框,这个时候请求时属于 pending 状态,当用户输入用户名密码的时候客户端会再次发送带 Authentication 头的请求。github

认证成功:

server.js

let express = require("express");
let app = express();
 
  app.use(express.static(__dirname+'/public'));
 
  app.get("/Authentication_base",function(req,res){
    console.log('req.headers.authorization:',req.headers)
    if(!req.headers.authorization){
      res.set({
        'WWW-Authenticate':'Basic realm="wang"'
      });
      res.status(401).end();
    }else{
      let base64 = req.headers.authorization.split(" ")[1];
      let userPass = new Buffer(base64, 'base64').toString().split(":");
      let user = userPass[0];
      let pass = userPass[1];
      if(user=="wang"&&pass="wang"){
        res.end("OK");
      }else{
        res.status(401).end();
      }
 
    }
 
  })
 
  app.listen(9090)
复制代码

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>HTTP Basic Authentication</title>
  </head>
  <body>
    <div></div>
    <script src="js/jquery-3.2.1.js"></script>
    <script> $(function(){ send('./Authentication_base'); }) var send = function(url){ $.ajax({ url : url, method : 'GET', }); } </script>
  </body>
</html>
复制代码

优势:   基本认证的一个优势是基本上全部流行的网页浏览器都支持基本认证。基本认证不多在可公开访问的互联网网站上使用,有时候会在小的私有系统中使用(如路由器网页管理接口)。后来的机制HTTP摘要认证是为替代基本认证而开发的,容许密钥以相对安全的方式在不安全的通道上传输。   程序员和系统管理员有时会在可信网络环境中使用基本认证,使用Telnet或其余明文网络协议工具手动地测试Web服务器。这是一个麻烦的过程,可是网络上传输的内容是人可读的,以便进行诊断。

缺点:   虽然基本认证很是容易实现,但该方案建立在如下的假设的基础上,即:客户端和服务器主机之间的链接是安全可信的。特别是,若是没有使用SSL/TLS这样的传输层安全的协议,那么以明文传输的密钥和口令很容易被拦截。该方案也一样没有对服务器返回的信息提供保护。   现存的浏览器保存认证信息直到标签页或浏览器被关闭,或者用户清除历史记录。HTTP没有为服务器提供一种方法指示客户端丢弃这些被缓存的密钥。这意味着服务器端在用户不关闭浏览器的状况下,并无一种有效的方法来让用户注销。

3、session-cookie

3.1 cookie

Http协议是一个无状态的协议,服务器不会知道究竟是哪一台浏览器访问了它,所以须要一个标识用来让服务器区分不一样的浏览器。cookie 就是这个管理服务器与客户端之间状态的标识。

cookie 的原理是,浏览器第一次向服务器发送请求时,服务器在 response 头部设置 Set-Cookie 字段,浏览器收到响应就会设置 cookie 并存储,在下一次该浏览器向服务器发送请求时,就会在 request 头部自动带上 Cookie 字段,服务器端收到该 cookie 用以区分不一样的浏览器。固然,这个 cookie 与某个用户的对应关系应该在第一次访问时就存在服务器端,这时就须要 session 了。

const http = require('http')
http.createServer((req, res) => {
  if (req.url === '/favicon.ico') {
    return
  } else {
    res.setHeader('Set-Cookie', 'name=zhunny')
    res.end('Hello Cookie')
  }
}).listen(3000) 
复制代码

3.2 session

session 是会话的意思,浏览器第一次访问服务端,服务端就会建立一次会话,在会话中保存标识该浏览器的信息。它与 cookie 的区别就是 session 是缓存在服务端的,cookie 则是缓存在客户端,他们都由服务端生成,为了弥补 Http 协议无状态的缺陷。

3.3 session-cookie认证

  1. 服务器在接受客户端首次访问时在服务器端建立seesion,而后保存seesion(咱们能够将seesion保存在 内存中,也能够保存在redis中,推荐使用后者),而后给这个session生成一个惟一的标识字符串,而后在 响应头中种下这个惟一标识字符串。
  2. 签名。这一步经过秘钥对sid进行签名处理,避免客户端修改sid。(非必需步骤)
  3. 浏览器中收到请求响应的时候会解析响应头,而后将sid保存在本地cookie中,浏览器在下次http请求的请求头中会带上该域名下的cookie信息。
  4. 服务器在接受客户端请求时会去解析请求头cookie中的sid,而后根据这个sid去找服务器端保存的该客户端的session,而后判断该请求是否合法。

const http = require('http')
//此时session存在内存中
const session = {}
http.createServer((req, res) => {
  const sessionKey = 'sid'
  if (req.url === '/favicon.ico') {
    return
  } else {
    const cookie = req.headers.cookie
    //再次访问,对sid请求进行认证
    if (cookie && cookie.indexOf(sessionKey) > -1) {
      res.end('Come Back')
    }
    //首次访问,生成sid,保存在服务器端
    else {
      const sid = (Math.random() * 9999999).toFixed()
      res.setHeader('Set-Cookie', `${sessionKey}=${sid}`)
      session[sid] = { name: 'zhunny' }
      res.end('Hello Cookie')
    }
  }
}).listen(3000)
复制代码

3.4 redis

redis是一个键值服务器,能够专门放session的键值对。如何在koa中使用session:

const koa = require('koa')
const app = new koa()
const session = require('koa-session')

const redisStore = require('koa-redis')
const redis = require('redis')
const redisClient = redis.createClient(6379, 'localhost')

const wrapper = require('co-redis')
const client = wrapper(redisClient)

//加密sessionid
app.keys = ['session secret']

const SESS_CONFIG = {
  key: 'kbb:sess',
  //此时让session存储在redis中
  store: redisStore({ client })
}

app.use(session(SESS_CONFIG, app))

app.use(ctx => {
  //查看redis中的内容
  redisClient.keys('*', (errr, keys) => {
    console.log('keys:', keys)
    keys.forEach(key => {
      redisClient.get(key, (err, val) => {
        console.log(val)
      })
    })
  })
  if (ctx.path === '/favicon.ico') return
  let n = ctx.session.count || 0
  ctx.session.count = ++n
  ctx.body = `第${n}次访问`
})

app.listen(3000)
复制代码

3.5 用户登陆认证

使用session-cookie作登陆认证时,登陆时存储session,退出登陆时删除session,而其余的须要登陆后才能操做的接口须要提早验证是否存在session,存在才能跳转页面,不存在则回到登陆页面。

在koa中作一个验证的中间件,在须要验证的接口中使用该中间件。

//前端代码
async login() {
    await axios.post('/login', {
        username: this.username,
        password: this.password
    })
},
async logout() {
    await axios.post('/logout')
},
async getUser() {
    await axios.get('/getUser')
}
复制代码
//中间件 auth.js
module.exports = async (ctx, next) => {
  if (!ctx.session.userinfo) {
    ctx.body = {
      ok: 0,
      message: "用户未登陆" };
  } else {
    await next();
} };
//须要验证的接口
router.get('/getUser', require('auth'), async (ctx) => {
  ctx.body = {
    message: "获取数据成功",
    userinfo: ctx.session.userinfo
  }
})
//登陆
router.post('/login', async (ctx) => {
  const {
    body
  } = ctx.request
  console.log('body', body)
  //设置session
  ctx.session.userinfo = body.username;
  ctx.body = {
    message: "登陆成功"
  }
})
//登出
router.post('/logout', async (ctx) => {
  //设置session
  delete ctx.session.userinfo
  ctx.body = {
    message: "登出系统"
  }
})
复制代码

4、Token

token 是一个令牌,浏览器第一次访问服务端时会签发一张令牌,以后浏览器每次携带这张令牌访问服务端就会认证该令牌是否有效,只要服务端能够解密该令牌,就说明请求是合法的,令牌中包含的用户信息还能够区分不一样身份的用户。通常 token 由用户信息、时间戳和由 hash 算法加密的签名构成。

4.1 Token认证流程

  1. 客户端使用用户名跟密码请求登陆
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
  4. 客户端收到 Token 之后能够把它存储起来,好比放在 Cookie 里或者Local Storage
  5. 客户端每次向服务端请求资源的时候须要带着服务端签发的 Token
  6. 服务端收到请求,而后去验证客户端请求里面带着的 Token(request头部添加Authorization),若是验证成功,就向客户端返回请求的数据 ,若是不成功返回401错误码,鉴权失败。

4.2 Token和session的区别

session-cookie的缺点: (1)认证方式局限于在浏览器中使用,cookie 是浏览器端的机制,若是在app端就没法使用 cookie。 (2)为了知足全局一致性,咱们最好把 session 存储在 redis 中作持久化,而在分布式环境下,咱们可能须要在每一个服务器上都备份,占用了大量的存储空间。 (3)在不是 Https 协议下使用 cookie ,容易受到 CSRF 跨站点请求伪造攻击。

token的缺点: (1)加密解密消耗使得 token 认证比 session-cookie 更消耗性能。 (2)tokensessionId 大,更占带宽。

二者对比,它们的区别显而易见: (1)token 认证不局限于 cookie ,这样就使得这种认证方式能够支持多种客户端,而不只是浏览器。且不受同源策略的影响。 (2)不使用 cookie 就能够规避CSRF攻击。 (3)token 不须要存储,token 中已包含了用户信息,服务器端变成无状态,服务器端只须要根据定义的规则校验这个 token 是否合法就行。这也使得 token 的可扩展性更强。

4.3 JWT(JSON Web Token)

基于 token 的解决方案有许多,经常使用的是JWTJWT 的原理是,服务器认证之后,生成一个 JSON 对象,这个 JSON 对象确定不能裸传给用户,那谁均可以篡改这个对象发送请求。所以这个 JSON 对象会被服务器端签名加密后返回给用户,返回的内容就是一张令牌,之后用户每次访问服务器端就带着这张令牌。

这个 JSON 对象可能包含的内容就是用户的信息,用户的身份以及令牌的过时时间。

4.3.1 JWT的组成部分

在该网站JWT,能够解码或编码一个JWT。一个JWT形如:

它由三部分组成:Header(头部)、Payload(负载)、Signature(签名)。

  1. Header部分是一个JSON对象,描述JWT的元数据。通常描述信息为该Token的加密算法以及Token的类型。{"alg": "HS256","typ": "JWT"}的意思就是,该token使用HS256加密,token类型是JWT。这个部分基本至关于明文,它将这个JSON对象作了一个Base64转码,变成一个字符串。Base64编码解码是有算法的,解码过程是可逆的。头部信息默认携带着两个字段。
  2. Payload 部分也是一个 JSON 对象,用来存放实际须要传递的数据。有7个官方字段,还能够在这个部分定义私有字段。通常存放用户名、用户身份以及一些JWT的描述字段。它也只是作了一个Base64编码,所以确定不能在其中存放秘密信息,好比说登陆密码之类的。
  3. Signature是对前面两个部分的签名,防止数据篡改,若是前面两段信息被人修改了发送给服务器端,此时服务器端是可利用签名来验证信息的正确性的。签名须要密钥,密钥是服务器端保存的,用户不知道。算出签名之后,把 Header、Payload、Signature 三个部分拼成一个字符串,每一个部分之间用"点"(.)分隔,就能够返回给用户。
4.3.2 JWT的特色
  1. JWT 默认是不加密,但也是能够加密的。生成原始 Token 之后,能够用密钥再加密一次。
  2. JWT 不加密的状况下,不能将秘密数据写入 JWT。
  3. JWT 不只能够用于认证,也能够用于交换信息。有效使用 JWT,能够下降服务器查询数据库的次数。
  4. JWT 的最大缺点是,因为服务器不保存 session 状态,所以没法在使用过程当中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期以前就会始终有效,除非服务器部署额外的逻辑。
  5. JWT 自己包含了认证信息,一旦泄露,任何人均可以得到该令牌的全部权限。为了减小盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  6. 为了减小盗用,JWT 不该该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
4.3.3 JWT验证用户登陆
//前端代码
//axios的请求拦截器,在每一个request请求头上加JWT认证信息
axios.interceptors.request.use(
    config => {
        const token = window.localStorage.getItem("token");
        if (token) {
        // 判断是否存在token,若是存在的话,则每一个http header都加上token
        // Bearer是JWT的认证头部信息
            config.headers.common["Authorization"] = "Bearer " + token;
        }
        return config;
    },
    err => {
        return Promise.reject(err);
    }
);
//登陆方法:在将后端返回的JWT存入localStorage
async login() {
    const res = await axios.post("/login-token", {
        username: this.username,
        password: this.password
    });
    localStorage.setItem("token", res.data.token);
},
//登出方法:删除JWT
async logout() {
    localStorage.removeItem("token");
},
async getUser() {
    await axios.get("/getUser-token");
}
复制代码
//后端代码
const jwt = require("jsonwebtoken");
const jwtAuth = require("koa-jwt");
//用来签名的密钥
const secret = "it's a secret";

router.post("/login-token", async ctx => {
  const { body } = ctx.request;
  //登陆逻辑,略,即查找数据库,若该用户和密码合法,即将其信息生成一个JWT令牌传给用户
  const userinfo = body.username;
  ctx.body = {
    message: "登陆成功",
    user: userinfo,
    // 生成 token 返回给客户端
    token: jwt.sign(
      {
        data: userinfo,
        // 设置 token 过时时间,一小时后,秒为单位
        exp: Math.floor(Date.now() / 1000) + 60 * 60
      },
      secret
    )
  };
});

//jwtAuth这个中间件会拿着密钥解析JWT是否合法。
//而且把JWT中的payload的信息解析后放到state中,ctx.state用于中间件的传值。
router.get(
  "/getUser-token",
  jwtAuth({
    secret
  }),
  async ctx => {
    // 验证经过,state.user
    console.log(ctx.state.user);
    ctx.body = {
      message: "获取数据成功",
      userinfo: ctx.state.user.data 
    };
  }
)
//这种密码学的方式使得token不须要存储,只要服务端能拿着密钥解析出用户信息,就说明该用户是合法的。
//若要更进一步的权限验证,须要判断解析出的用户身份是管理员仍是普通用户。
复制代码

5、OAuth(开放受权)

OAuth(Open Authorization)是一个开放标准,容许用户受权第三方网站访问他们存储在另外的服务提供者上的信息,而不须要将用户名和密码提供给第三方网站或分享他们数据的全部内容,为了保护用户数据的安全和隐私,第三方网站访问用户数据前都须要显式的向用户征求受权。咱们常见的提供OAuth认证服务的厂商有支付宝,QQ,微信。

OAuth协议又有1.0和2.0两个版本。相比较1.0版,2.0版整个受权验证流程更简单更安全,也是目前最主要的用户身份验证和受权方式。

关于OAuth相关文章,能够查看 OAuth 2.0 的一个简单解释理解OAuth 2.0OAuth 2.0 的四种方式

5.1 OAuth认证流程

OAuth就是一种受权机制。数据的全部者告诉系统,赞成受权第三方应用进入系统,获取这些数据。系统从而产生一个短时间的进入令牌(token),用来代替密码,供第三方应用使用。

OAuth有四种获取令牌的方式,无论哪种受权方式,第三方应用申请令牌以前,都必须先到系统备案,说明本身的身份,而后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。

在先后端分离的情境下,咱们常使用受权码方式,指的是第三方应用先申请一个受权码,而后再用该码获取令牌。

5.2 GitHub第三方登陆示例

咱们用例子来理清受权码方式的流程。

  1. 在GitHub中备案第三方应用,拿到属于它的客户端ID和客户端密钥。

github-settings-developer settings中建立一个OAuth App。并填写相关内容。填写完成后Github会给你一个客户端ID和客户端密钥。

  1. 此时在你的第三方网站就能够提供一个Github登陆连接,用户点击该连接后会跳转到Github。这一步拿着客户端ID向Github请求受权码code。
const config = {
  client_id: '28926186082164bbea8f',
  client_secret: '07c4fdae1d5ca458dae3345b6d77a0add5a785ca'
}

router.get('/github/login', async (ctx) => {
  var dataStr = (new Date()).valueOf();
  //重定向到认证接口,并配置参数
  var path = "https://github.com/login/oauth/authorize";
  path += '?client_id=' + config.client_id;

  //转发到受权服务器
  ctx.redirect(path);
})
复制代码
  1. 用户跳转到Github,输入Github的用户名密码,表示用户赞成使用Github身份登陆第三方网站。此时就会带着受权码code跳回第三方网站。跳回的地址在建立该OAuth时已经设置好了。http://localhost:3000/github/callback
  2. 第三方网站收到受权码,就能够拿着受权码、客户端ID和客户端密钥去向Github请求access_token令牌。
  3. Github收到请求,向第三方网站颁发令牌。
  4. 第三方网站收到令牌,就能够暂时拥有Github一些请求的权限,好比说拿到用户信息,拿到这个用户信息以后就能够构建本身第三方网站的token,作相关的鉴权操做。
router.get('/github/callback', async (ctx) => {
  console.log('callback..')
  const code = ctx.query.code;
  const params = {
    client_id: config.client_id,
    client_secret: config.client_secret,
    code: code
  }
  let res = await axios.post('https://github.com/login/oauth/access_token', params)
  const access_token = querystring.parse(res.data).access_token
  res = await axios.get('https://api.github.com/user?access_token=' + access_token)
  console.log('userAccess:', res.data)
  ctx.body = ` <h1>Hello ${res.data.login}</h1> <img src="${res.data.avatar_url}" alt=""/> `

})
复制代码

OAuth受权的登录流程图:

6、原文

www.lishuaishuai.com/nodejs/1167…