鉴权相关

Posted by Qz on May 23, 2017

“Yeah It’s on. ”

https://juejin.cn/post/6844903829285289998

因为Http协议无状态,在需要识别状态的时候,需要借助外力来辨别,所以才会出现cookie这些东西

Cookie是存储在用户本地计算机上,用于保存一些用户操作的历史信息,当用户再次访问我们的服务器的时候,浏览器通过HTTP协议,将他们本地的Cookie内容也发到咱们服务器上,从而完成验证。

  • Cookie 是存储在浏览器客户的一小片数据;
  • Cookie 可以同时被前台与后台操作;
  • Cookie 可以跨页面存取;
  • Cookie 是不可以跨服务器访问的;
  • Cookie 有限制; 每个浏览器存储的个数不能超过300个,每个服务器不能超过20个,数据量不能超过4K;
  • Cookie 是有生命周期的,默认与浏览器相同,如果进程退出,cookie会被销毁

cookie存在跨域问题

Cookie又分为了会话Cookie与持久Cookie,要区分这两种类型,非常的简单,持久Cookie就是我们设置了它的过期时间,而没设置过期时间的,都属于会话Cookie

因为当我们设置了Cookie的过期时间,那么这个Cookie就会存储在用户的硬盘中,而不是在内存中,因为不在内存中,不管用户是关闭浏览器,还是关机重启,只要在有效时间内,这个Cookie都能用

// cookie.js
const http = require("http")
http
    .createServer((req, res) => {
            if(req.url === '/favicon.ico'){
          res.end('')
          return
}
	// 观察cookie存在
  console.log('cookie:', req.headers.cookie) // 设置cookie
  res.setHeader('Set-Cookie', 'cookie1=abc;')
  res.end('hello cookie!!')
})
    .listen(3000)
  • Header Set-Cookie负责设置cookie
  • 请求传递Cookie

服务器端向客户端发送Cookie是通过HTTP响应报文实现的,在Set-Cookie中设置需要向客户端发送的cookie,

cookie格式如下:

Set-Cookie: “name=value;domain=.domain.com;path=/;expires=Sat, 11 Jun 2016 11:29:42 GMT;HttpOnly;secure”

之后对该服务器发起的每一次新请求,浏览器都会将之前保存的Cookie信息通过 Cookie 请求头部再发送给服务器。

https://www.cnblogs.com/diligenceday/p/10885868.html

HttpOnly 属性:

设置HttpOnly=true的cookie不能被js获取到,无法用document.cookie打出cookie的内容

Secure 属性:

如果一个cookie被设置了Secure=true,那么这个cookie只能用https协议发送给服务器,用http协议是不发送的。换句话说,cookie是在https的情况下创建的,而且他的Secure=true,那么之后你一直用https访问其他的页面(比如登录之后点击其他子页面),cookie会被发送到服务器,你无需重新登录就可以跳转到其他页面。但是如果这是你把url改成http协议访问其他页面,你就需要重新登录了,因为这个cookie不能在http协议中发送。

domain:

cookie对于哪个域是有效的。所有向该域发送的请求中都会包含这个cookie信息。这个值可以包含子域(如:yq.aliyun.com),也可以不包含它(如:.aliyun.com,则对于aliyun.com的所有子域都有效).

Cookie跨域

假设服务端的域名是a.com,发送跨域请求的前端的域名是b.com,那么在b.com想a.com发送跨域请求时,是可以携带cookie的,但是这个cookie必须是域名为a.com下的cookie

也就是说,b.com的前端发送的跨域请求携带的cookie,是目标页面所在域的cookie。

所以带cookie跨域的前提是目标页面的cookie在本机存在,跨域要携带的cookie必须是目标页面所在域的cookie


  • b.coma.com发送跨域请求,是不可以把b.com域名下的cookie带上。

  • b.com通过JS在本机生成一个域名a.com的Cookie,或者a.com的服务端在发送响应时setCookie的domain为b.com。这两种做法都是行不通的,因为设置cookie的domain可以设置为父域名和自身,但是不能设置其他域名和子域名,否则cookie设置不会成功。

如何跨域携带cookies

https://juejin.cn/post/6859939491994402824

  1. 服务端需要设置

    Access-Control-Allow-Credentials: true
    Access-Control-Allow-Origin: [特定域名] // 不可以是*
    复制代码
    
  2. 客户端

    XMLHttpRequest发请求需要设置withCredentials=true,fetch 发请求需要设置 credentials = include


  1. 服务端是无法跨域设置cookie的(set-cookie),只能设置自身域名或者父域名的Cookie
  2. 前端是可以带cookie跨域的,前提是cookie是目标服务器所在域的cookie

session

Session也非常简单,这货存储在我们的服务器上,就是在我们的服务器上保存用户的操作信息

当用户访问我们的网站时,我们的服务器会成一个Session ID,然后把Session ID存储起来,再把这个Session ID发给我们的用户,用户再次访问我们的服务器的时候,拿着这个Session ID就能验证了,当这个ID能与我们服务器上存储的ID对应起来时,我们就可以认为是自己人

// cookie.js
const http = require("http")
const session = {}
http
.createServer((req, res) => { // 观察cookie存在
        console.log('cookie:', req.headers.cookie)
        const sessionKey = 'sid'
        const cookie = req.headers.cookie
        if(cookie && cookie.indexOf(sessionKey) > -1 ){
  res.end('Come Back ')
  // 简略写法未必具有通用性
  const pattern = new RegExp(`${sessionKey}=([^;]+);?\s*`) 
  const sid = pattern.exec(cookie)[1] 
  console.log('session:',sid ,session ,session[sid])
} else {
  const sid = (Math.random() * 99999999).toFixed()
  // 设置cookie
  res.setHeader('Set-Cookie', `${sessionKey}=${sid};`)
        session[sid] = {name : 'laowang'}
        res.end('Hello')
    }
    res.end('hello cookie!!')
})
.listen(3000)

实现原理:

  1. 服务器在接受客户端首次访问时在服务器端创建seesion,然后保存seesion(我们可以将seesion保存在 内存中,也可以保存在redis中,推荐使用后者),然后给这个session生成一个唯一的标识字符串,然后在 响应头中种下这个唯一标识字符串。

  2. 签名。这一步通过秘钥对sid进行签名处理,避免客户端修改sid。(非必需步骤)

  3. 浏览器中收到请求响应的时候会解析响应头,然后将sid保存在本地cookie中,浏览器在下次http请求的

请求头中会带上该域名下的cookie信息,

  1. 服务器在接受客户端请求时会去解析请求头cookie中的sid,然后根据这个sid去找服务器端保存的该客

    户端的session,然后判断该请求是否合法。

注意:

  • cookie只是实现session的其中一种方案。虽然是最常用的,但并不是唯一的方法。禁用cookie后还有其他方法存储,比如放在url中
  • 现在大多都是Session + Cookie,但是只用session不用cookie,或是只用cookie,不用session在理论上都可以保持会话状态。可是实际中因为多种原因,一般不会单独使用
  • 用session只需要在客户端保存一个id,实际上大量数据都是保存在服务端。如果全部用cookie,数据量大的时候客户端是没有那么多空间的。
  • 如果只用cookie不用session,那么账户信息全部保存在客户端,一旦被劫持,全部信息都会泄露。并且客户端数据量变大,网络传输的数据量也会变大

Cookie与Session的不同

  • 首先就是存储方式的不同Cookie是存储在用户的计算机上,而Session是存储在我们的服务器上
  • Cookie与Seesion的存储内容大小也是有区别的
  • 从安全性上来说,Session比起Cookie来说,还是要更安全的

koa中的session

npm i koa-session -S

const koa = require('koa')
const app = new koa()
const session = require('koa-session')
// 签名key keys作用 用来对cookie进行签名 app.keys = ['some secret'];
// 配置项
const SESS_CONFIG = {
  key: 'kkb:sess', // cookie键名
  maxAge: 86400000, // 有效期,默认一天 
  httpOnly: true, // 仅服务器修改
  signed: true, // 签名cookie
};
// 注册
app.use(session(SESS_CONFIG, app));
// 测试 app.use(ctx => {
if (ctx.path === '/favicon.ico') return; // 获取
let n = ctx.session.count || 0;
// 设置
ctx.session.count = ++n;
ctx.body = '' + n + '次访问'; });
app.listen(3000)                   

使用redis存储session

// koa-redis
const redisStore = require('koa-redis');
const redis = require('redis')
const redisClient = redis.createClient(6379, "localhost");
var wrapper = require('co-redis');
var client = wrapper(redisClient);

app.use(session({
	key:'kkb:sess',
	store: redisStore({client}) // 此处可以不必指定client
}, app));

app.use(ctx => {
  //...
	//查看redis中存储的数据 
  redisClient.keys('*',(err,keys)=>{
    console.log(keys);
    keys.forEach(key=>{
      redisClient.get(key,(err,val)=>{
        console.log(val);
			})
    })
	}) 
});

token

网页链接

在Web领域基于Token的身份验证随处可见。在大多数使用Web API的互联网公司中,tokens 是多用户下处理认证的最佳方式。

以下几点特性会让你在程序中使用基于Token的身份验证

  1. 无状态、可扩展
  2. 支持移动设备
  3. 跨程序调用
  4. 安全

基于Token的验证原理

基于Token的身份验证是无状态的,我们不将用户信息存在服务器或Session中。(这就是Token的优势)

这种概念解决了在服务端存储信息时的许多问题

  NoSession意味着你的程序可以根据需要去增减机器,而不用去担心用户是否登录。       基于Token的身份验证的过程如下:  

  1. 用户通过用户名和密码发送请求。
  2. 程序验证。
  3. 程序返回一个签名的token 给客户端。
  4. 客户端储存token,并且每次用于每次发送请求。
  5. 服务端验证token并返回数据。  

每一次请求都需要token。token应该在HTTP的头部发送从而保证了Http请求无状态。     

JWT

JSON WEB TOKEN

Bearer Token包含三个组成部分:令牌头、payload、哈希

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoiYWJjIiwicGFzc3dvcmQiOiIxMTExMTEifSwi ZXhwIjoxNTU3MTU1NzMwLCJpYXQiOjE1NTcxNTIxMzB9.pjGaxzX2srG_MEZizzmFEy7JM3t8tjkiu3yULgzFwU k

以 . 分割

  1. 签名:默认使用base64对payload编码,使用hs256算法对令牌头、payload和密钥进行签名生成哈希
  2. 验证:默认使用hs256算法对hs256算法对令牌中数据签名并将结果和令牌中哈希比对
 // jsonwebtoken.js
const jsonwebtoken = require('jsonwebtoken')
const secret = '12345678'
const opt = {
  secret: 'jwt_secret',
	key: 'user' 
}
const user = {
  username: 'abc',
  password: '111111'
}
const token = jsonwebtoken.sign({
data: user,
// 设置 token 过期时间
exp: Math.floor(Date.now() / 1000) + (60 * 60),
}, secret)
console.log('生成token:' + token)
// 生成 token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoiYWJjIiwicGFzc3dv cmQiOiIxMTExMTEifSwiZXhwIjoxNTQ2OTQyMzk1LCJpYXQiOjE1NDY5Mzg3OTV9.VPBCQgLB7XPBq3RdHK9WQM kPp3dw65JzEKm_LZZjP9Y
console.log('解码:', jsonwebtoken.verify(token, secret, opt))
// 解码: { data: { username: 'abc', password: '111111' },
// exp: 1546942395,
// iat: 1546938795 }

JWT 实现踢人

错误的解决:

颁布jwt时,把jwt存到一个中心redis中。每次访问验证jwt时看看redis里是否有这个token,没有这个token就认证失败。踢人封号只用把用户关联的jwt删除掉就ok了 (这么玩完全违背了jwt的初衷,服务器又变了有状态)


比较好的解决方案:

颁布2个token,access token和refresh token

  • access token访问令牌为一个JWT,设置一个较短的过期时间,比如1小时。访问令牌每次调用后端服务都需要携带,往返网络的频率非常高,暴露的可能性就越大,设置较短的过期时间也可以降低安全风险。
  • refresh token刷新令牌,可以不为JWT,设置一个较长的过期时间,比如1个月。刷新令牌主要用来换取新的access token。因为其仅在访问令牌要失效或已经失效时才会被传递给服务端,较长的过期时间并不会有太大的安全风险。颁发token的时候,仅将刷新令牌保存在redis并设置过期时间。当使用刷新令牌换取新的访问令牌时,需要判断redis里是否存在该刷新令牌,如果不存在,则刷新失败,用户就需要重新登录。

客户端要长时间维护登录态,就需要当访问令牌失效后,自动使用刷新令牌获取新的访问令牌。或者在访问令牌失效之前,提前刷新令牌。

现在我们想要踢人,只需要将用户相关的刷新令牌从redis里删除。当前的访问令牌失效后,自然也没有办法再刷新令牌了。从而达到强制用户登出的目的。

缺点:踢人不及时

为什么要有refreshToken

使用refreshToken可以提高安全性

accessToken被盗取了,此时攻击者就可以拿这个accessToke访问权限以内的功能了。如果accessToken设置一个短暂的有效期2小时,攻击者能使用被盗取的accessToken的时间最多也就2个小时,除非再通过refreshToken刷新accessToken才能正常访问。

有了refreshToken可以降低accessToken被盗的风险

无感刷新TOKEN方案

无感刷新 Token

在用户登录应用后,服务器会返回一组数据,其中就包含了accessTokenrefreshToken,每个accessToken都有一个固定的有效期,如果携带一个过期的token向服务器请求时,服务器会返回401的状态码来告诉用户此token过期了,此时就需要用到登录时返回的refreshToken调用刷新Token的接口(Refresh)来更新下新的token再发送请求即可。

OAuth(开放授权)  

https://www.jianshu.com/p/4f5fcddb4106

https://juejin.cn/post/6847009773477429255  

概述:三方登入主要基于OAuth 2.0。OAuth协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码), 即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。

OAuth 简单理解就是一种授权机制,它是在客户端和资源所有者之间的授权层,用来分离两种不同的角色。在资源所有者同意并向客户端颁发令牌后,客户端携带令牌可以访问资源所有者的资源。

OAuth2.0 的授权简单理解其实就是获取令牌(token)的过程,OAuth 协议定义了四种获得令牌的授权方式(authorization grant )如下:

  • 授权码(authorization-code
  • 隐藏式(implicit
  • 密码式(password):
  • 客户端凭证(client credentials

但值得注意的是,不管我们使用哪一种授权方式,在三方应用申请令牌之前,都必须在系统中去申请身份唯一标识:客户端 ID(client ID)和 客户端密钥(client secret)。这样做可以保证 token 不被恶意使用。

SSO单点登陆  

https://yq.aliyun.com/articles/636281

单点登录英文全称Single Sign On,简称就是SSO。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。


不同域下的单点登录:

  1. 用户访问app系统,app系统是需要登录的,但用户现在没有登录。
  2. 跳转到CAS server,即SSO登录系统,以后图中的CAS Server我们统一叫做SSO系统。 SSO系统也没有登录,弹出用户登录页。
  3. 用户填写用户名、密码,SSO系统进行认证后,将登录状态写入SSO的session,浏览器(Browser)中写入SSO域下的Cookie。
  4. SSO系统登录完成后会生成一个ST(Service Ticket),然后跳转到app系统,同时将ST作为参数传递给app系统。
  5. app系统拿到ST后,从后台向SSO发送请求,验证ST是否有效。
  6. 验证通过后,app系统将登录状态写入session并设置app域下的Cookie。

至此,跨域单点登录就完成了。以后我们再访问app系统时,app就是登录的。接下来,我们再看看访问app2系统时的流程。

  1. 用户访问app2系统,app2系统没有登录,跳转到SSO。
  2. 由于SSO已经登录了,不需要重新登录认证。
  3. SSO生成ST,浏览器跳转到app2系统,并将ST作为参数传递给app2。
  4. app2拿到ST,后台访问SSO,验证ST是否有效。
  5. 验证成功后,app2将登录状态写入session,并在app2域下写入Cookie。

SSO系统登录后,跳回原业务系统时,带了个参数ST,业务系统还要拿ST再次访问SSO进行验证,觉得这个步骤有点多余。他想SSO登录认证通过后,通过回调地址将用户信息返回给原业务系统,原业务系统直接设置登录状态,这样流程简单,也完成了登录,不是很好吗?

其实这样问题时很严重的,如果我在SSO没有登录,而是直接在浏览器中敲入回调的地址,并带上伪造的用户信息,是不是业务系统也认为登录了呢?这是很可怕的。


不同浏览器访问同一个网站时,Cookies是不共享的。

每个浏览器都有自己独立的Cookie存储空间,它们不会互相访问或共享数据。

所以,当你在使用一个浏览器登录并进行操作时,另一个浏览器是无法识别你的登录状态或个人设置的。

salt

salt加盐 是对密码进行加密

https://juejin.cn/post/6932290083794583566

在这个文章中 由于加密算法是cpu密集型 所以导致 抗不了并发

那么我们可以将密码加密的操作放到前端

将hash工作交给前端,最后数据传递只传递hash后的密码。有人可能认为,如果由前端进行hash,会导致salt泄漏之类的情况,但是实际上,salt泄漏并不会带来问题。salt只是为了防止相同数据的hash碰撞问题,只要你设定了独特的salt,不会和其他被脱库的数据集采用了一样的salt,就不会由有问题。还有人会考虑,如果泄漏salt有可能导致可以本地生成字典hash进行碰撞。其实这个情况也不用担心,节流阀等东西可以做好限制,而且如果别人铁了心要攻击了,估计会开大量代理来反复撞库,可能密码还没破,网站先挂了。总而言之,这种可以由客户端解决的任务,可以交给客户端来处理,既能降低服务端的负载,同时也不会丢失安全性。