前端鉴权(Cookie/Session、Token和OAuth)

  先后端未分离之前,页面都是经过后台来渲染的,能不能访问到页面直接由后台逻辑判断。先后端分离之后,页面的元素由页面自己来控制,因此页面间的路由是由前端来控制了。固然,仅有前端作权限控制是远远不够的,后台还须要对每一个接口作验证。
  为何前端作权限控制是不够的呢?由于前端的路由控制仅仅是视觉上的控制,前端能够隐藏某个页面或者某个按钮,可是发送请求的方式仍是有不少,彻底能够跳过操做页面来发送某个请求。因此就算前端的权限控制作的很是严密,后台依旧须要验证每一个接口。
  前端的权限控制主要有三种:路由控制(路由的跳转)、视图控制(按钮级别)和请求控制(请求拦截器)。这几种方式以后再详谈,前端作完权限控制,后台仍是须要验证每个接口,这就是鉴权。如今先后端配合鉴权的方式主要有如下几种:html

  1. session-cookie
  2. Token 验证(JWT)
  3. OAuth(开放受权)

session-cookie

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) 
复制代码

session

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

session-cookie认证

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

redis

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

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)
复制代码

用户登陆认证

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

//前端代码
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: "登出系统"
  }
})
复制代码

Token

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

Token认证流程

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

Token和session的区别

  1. session-cookie的缺点:(1)认证方式局限于在浏览器中使用,cookie是浏览器端的机制,若是在app端就没法使用cookie。(2)为了知足全局一致性,咱们最好把session存储在redis中作持久化,而在分布式环境下,咱们可能须要在每一个服务器上都备份,占用了大量的存储空间。(3)在不是Https协议下使用cookie,容易受到CSRF跨站点请求伪造攻击。
  2. token的缺点:(1)加密解密消耗使得token认证比session-cookie更消耗性能。(2)token比sessionId大,更占带宽。
  3. 二者对比,它们的区别显而易见:(1)token认证不局限于cookie,这样就使得这种认证方式能够支持多种客户端,而不只是浏览器。且不受同源策略的影响。(2)不使用cookie就能够规避CSRF攻击。(3)token不须要存储,token中已包含了用户信息,服务器端变成无状态,服务器端只须要根据定义的规则校验这个token是否合法就行。这也使得token 的可扩展性更强。

JWT(JSON Web Token)

  JWT 的原理是,服务器认证之后,生成一个 JSON 对象,这个JSON对象确定不能裸传给用户,那谁均可以篡改这个对象发送请求。所以这个JSON对象会被服务器端签名加密后返回给用户,返回的内容就是一张令牌,之后用户每次访问服务器端就带着这张令牌。
  这个JSON对象可能包含的内容就是用户的信息,用户的身份以及令牌的过时时间。面试

JWT的组成部分

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


  它由三部分组成: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 三个部分拼成一个字符串,每一个部分之间用"点"(.)分隔,就能够返回给用户。
JWT的特色
  1. JWT 默认是不加密,但也是能够加密的。生成原始 Token 之后,能够用密钥再加密一次。
  2. JWT 不加密的状况下,不能将秘密数据写入 JWT。
  3. JWT 不只能够用于认证,也能够用于交换信息。有效使用 JWT,能够下降服务器查询数据库的次数。
  4. JWT 的最大缺点是,因为服务器不保存 session 状态,所以没法在使用过程当中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期以前就会始终有效,除非服务器部署额外的逻辑。
  5. JWT 自己包含了认证信息,一旦泄露,任何人均可以得到该令牌的全部权限。为了减小盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  6. 为了减小盗用,JWT 不该该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
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不须要存储,只要服务端能拿着密钥解析出用户信息,就说明该用户是合法的。
//若要更进一步的权限验证,须要判断解析出的用户身份是管理员仍是普通用户。
复制代码

OAuth

  三方登入主要基于OAuth 2.0。OAuth协议为用户资源的受权提供了一个安全的、开放而又简易的标 准。与以往的受权方式不一样之处是OAuth的受权不会使第三方触及到用户的账号信息(如用户名与密码), 即第三方无需使用用户的用户名与密码就能够申请得到该用户资源的受权,所以OAuth是安全的。咱们常见的提供OAuth认证服务的厂商有支付宝、QQ、微信。这样的受权方式使得用户使用门槛低,能够更好的推广本身的应用。
  OAuth相关文章推荐阮一峰老师的一系列文章OAuth 2.0 算法

OAuth认证流程

  OAuth就是一种受权机制。数据的全部者告诉系统,赞成受权第三方应用进入系统,获取这些数据。系统从而产生一个短时间的进入令牌(token),用来代替密码,供第三方应用使用。
  OAuth有四种获取令牌的方式,无论哪种受权方式,第三方应用申请令牌以前,都必须先到系统备案,说明本身的身份,而后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。
  在先后端分离的情境下,咱们常使用受权码方式,指的是第三方应用先申请一个受权码,而后再用该码获取令牌。数据库

GitHub第三方登陆示例

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

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

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


2. 此时在你的第三方网站就能够提供一个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=""/> `

})
复制代码

参考

cookie,session傻傻分不清楚?
把cookie聊清楚
先后端常见的几种鉴权方式
前端面试查漏补缺--(十) 前端鉴权
谈前端权限
OAuth 2.0 的四种方式

相关文章
相关标签/搜索