这是我参加"第四届青训营"笔记创作活动的第10天
身份认证
身份认证就是通过一定的手段,完成对用户身份的确认。例如:手机验证码登录、邮箱密码登录、二维码登录等。
不同开发模式下的身份认证
1.服务器渲染推荐使用session认证机制
2.前后端分离推荐使用jwt认证机制
session认证机制
1.http协议的无状态性
http协议的无状态性指的是客户端的每次http请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次http请求的状态。
2.如何突破http无状态的限制
通过Cookie
3.什么是Cookie
Cookie是存储在用户浏览器中的一段不超过4KB的字符串。它由一个名称、一个值和其他几个用于控制cookie有效期、安全性、使用范围的可选属性组成。
不同域名下的cookie各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的cookie一同发送到服务器。
\
1.cookie的几大特性
1.自动发送
2.域名独立
3.过期时限
4.4KB限制
2.cookie在身份认证中的作用
HTTP协议本身是无状态的。什么是无状态呢,即服务器无法判断用户身份。Cookie实际上是一小段的文本信息(key-value格式)。客户端向服务器发起请求,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。
cookie很容易被伪造,不具有安全性,因此不建议服务器将重要的隐私数据,通过cookie的形式发送给浏览器。
3.安装配置express-session中间件
安装 npm i express-session
//配置session中间件
const session = require('express-session')
app.use(
session({
secret:'itheima',
resave:false,
saveUninitialized:true,
})
)
当express-session中间件配置成功后,即可通过req.session来访问和使用session对象,从而存储用户的关键信息。
app.post('/api/login',(req,res)=>{
if(req.body.username !=='admin' || req.body.password != '000000'){
return res.send({status:1,msg:'登录失败'})
}
req.session.user = req.body //用户的信息
req.session.islogin = true //用户的登录状态
res.send({status:0,msg:'登录成功'})
})
4.从session中取数据
app.post('/api/username',(req,res)=>{
//判断用户是否登录
if(!req.session.islogin){
return res.send({status:1,msg:'fail'})
}
res.send({status:0,msg:'success',username:req.session.user.username})
})
5.清空session
调用req.session.destroy(),只会清空当前用户对应的session
app.post('/api/login',(req,res)=>{
//清空当前客户端对应的session信息
req.session.destroy()
res.send({
status:0,
msg:'退出登录成功'
})
})
JWT认证机制
session认证机制的局限性:session认证机制需要配合cookie才能实现。由于cookie默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域session认证。
注意:当前端请求后端接口不存在跨域问题,推荐使用session身份认证机制,当前端请求后端接口存在跨域问题,推荐使用jwt认证机制。
1.什么是JWT
JWT (Json web token), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。是目前最流行的跨域认证解决方案。
2.JWT的工作原理
流程上是这样的:
- 用户使用用户名密码来请求服务器
- 服务器进行验证用户的信息
- 服务器通过验证发送给用户一个token
- 客户端存储token,并在每次请求时附送上这个token值
- 服务端验证token值,并返回数据
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *。
3.JWT的组成部分
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).三者之间使用英文的‘.’分隔。格式如下:
Header.Payload.Signature
1.header(保证token的安全)
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
完整的头部就像下面这样的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
2.playload(这部分才是真正的用户信息)
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明 (建议但不强制使用) :
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后将其进行base64加密,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
3.signature(保证token的安全)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret秘钥是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
4.JWT的使用方式
客户端收到服务器返回的JWT之后,通常会将它存储在localStorage或sessionStorage中。
此后,客户端每次与服务器通信,都要带上这个JWT的字符串,从而进行身份认证。推荐的做法是把JWT放在http请求头的Authorization字段中,格式如下:
Authorization: Bearer <token>
1.安装相关的包
npm i jsonwebtoken express-jwt
jsonwebtoken 用于生成JWT字符串。
express-jwt用于将JWT字符串解析还原成JSON对象。
// 安装并导入jsonwebtoken和express-jwt
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
// 定义secret密钥。建议将密钥命名为secretKey
const secretKey = 'itheima ^_^'
// 注册将JWT字符串解析还原成JSON对象的中间件
app.use(expressJWT({ secret:secretKey}).unless({path:[/^/api//]}))
// 在登录成功之后,调用jwt.sign()方法生成JWT字符串。并通过token属性发送给客户端
// 参数1:用户的信息对象
// 参数2:加密的密钥
// 参数3:配置对象,可以配置当前token的有效期
const tokenStr = jwt.sign({username:userinfo.username},secretKey,{expiresIn:'30s'})
//使用req.user获取用户信息
//导入express
const express = require('express')
//创建服务器实例
const app = express()
// 安装并导入jsonwebtoken和express-jwt
const jwt = require('jsonwebtoken')
const {expressjwt:expressJWT} = require('express-jwt')
// 解析post表单数据的中间件
const bodyParse = require('body-parser')
app.use(bodyParse.urlencoded({extended:false}))
// 定义secret密钥。建议将密钥命名为secretKey
const secretKey = 'itheima ^_^'
// 注册将JWT字符串解析还原成JSON对象的中间件
// 注意:只要配置成功了express-jwt这个中间件就可以把解析出来的用户信息,挂载到req.user属性上
app.use(expressJWT({ secret:secretKey,algorithms:['HS256']}).unless({path:[/^/api//]}))
app.post('/api/login',(req,res)=>{
// 将req.body请求体中的数据,转存为userinfo常量
const userinfo = req.body
// 登录失败
if(userinfo.username !=='admin' || userinfo.password != '000000'){
return res.send({
status:400,
message:'登录失败!',
})
}
app.get('/admin/getinfo',(req,res)=>{
console.log(req.auth)
res.send({
status:200,
message:'获取用户信息成功!',
data:req.auth
})
})
// 登录成功
// 在登录成功之后,调用jwt.sign()方法生成JWT字符串。并通过token属性发送给客户端
// 参数1:用户的信息对象
// 参数2:加密的密钥
// 参数3:配置对象,可以配置当前token的有效期
// 记住:千万不要把密码加密到token字符中
const tokenStr = jwt.sign({username:userinfo.username},secretKey,{expiresIn:'30s'})
res.send({
status:200,
message:'登录成功',
token: tokenStr, //要发送给客户端的token字符串
})
})
// 使用全局错误处理中间件,捕获解析JWT失败后产生的错误
app.use((err,req,res,next)=>{
// 这次错误是由token解析失败导致的
if(err.name === 'UnauthorizedError'){
return res.send({
status:401,
message:'无效的token'
})
}
res.send({
status:500,
message:'未知的错误'
})
})
//启动服务器
app.listen(80, () => {
console.log('http://127.0.0.1')
})