0%

前后端身份认证

不同身份认证方案:

服务端渲染推荐使用Session认证机制

前后端分离推荐使用JWT认证机制

Session认证机制

Cookie是存储在用户浏览器中一段不超过4kb的字符串,它由一个名称(Name),y一个值(Value)和几个用于控制Cookie有效期,安全性,使用范围的可选属性组成

不同域名下的Cookie各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的Cookie一同发送到服务器。

Cookie的几大特性:

  • 自动发送

  • 域名独立

  • 过期时限

  • 4kb限制

浏览器可以设置不接受Cookie,也可以设置不向服务器发送Cookie,window.navigator.cookieEnabled属性返回一个布尔值,表示浏览器是否打开cookie功能

两个域名只要域名相同和端口相同就可以共享Cookie,也就是说http://example.com设置的Cookie可以被https://example.com读取

Cookie在身份认证中的作用:

客户端第一次请求服务器时,服务器通过响应头Set-Cookie的形式,向客户端发送一个身份认证的Cookie,客户端会自动将Cookie保存在浏览器中,随后,当客户端浏览器每次请求服务器时浏览器会自动将身份认证相关的Cookie,通过请求头的形式发送给服务器Cookie:foo=bar,服务器即可验证客户端身份

Cookie的属性

Expires,Max-Age:

Expires属性指定一个具体的到齐时间,到了指定时间后,浏览器就不再保留这个Cookie,它的值时UTC格式,可以使用Date.prototype.toUTCString()进行格式转换

1
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;

如果不设置该属性,或者设置为null,Cookie只在当前会话有效,浏览器窗口一旦关闭,当前Session结束,该Cookie被删除,另外,浏览器根据本地时间,觉得Cookie是否过期,由于本地时间不精确,没有办法保证Cookie一旦会在服务器指定时间过期

Max-Age属性指定从现在开始Cookie存在的描述,即过了这个时间后,浏览器就不再保留这个Cookie,如果同时指定Expires和Max-Age,那么Max-Age的值优先生效

Domain,Path

Domain属性指定浏览器发出Http请求时,哪些域名要附带这个Cookie,如果没有指定该属性,浏览器会默认将其设为当前URL的一级域名,比如www.example.com会设为example.com,而且以后如果访问example.com的任何子域名,HTTP请求也会带上这个Cookie,如果服务器在Set-Cookie字段指定的域名不属于当前域名,浏览器会拒绝这个Cookie

Path属性指定指定浏览器发出HTTP请求时哪些路径要附带这个Cookie,只要浏览器发现,Path属性是HTTP请求路径的开头一部分,就会在头信息里面带上这个Cookie,比如PATH属性时/,那么请求/docs路径也会包含该Cookie,当然,前提是域名必须一致

Secure,HttpOnly

Secure属性指定浏览器在加密协议HTTPS下,才能将这个Cookie发送到服务器,另一方面,如果当前协议是HTTP,浏览器会自动忽略服务器发来的Secure属性,该属性只是一个开关,不需要指定值,如果通信时HTTS协议,开关自动打开

HttpOnly属性指定该Cookie无法通过JavaScript脚本拿到,主要是Document.cookie属性,XMLHttpRequest对象和Request API都拿不到该属性,这样就防止了该Cookie被脚本读到,只有浏览器发出HTTP请求时才会带上该Cookie

Cookie具有不安全性

由于Cookie是存储在浏览器中的,因此浏览器也提供了读写Cookie的API,因此Cookie很容易被伪造,不具有安全性,因此Cookie不能存放重要隐私数据

Session工作原理:

  1. 客户端把用户ID和密码等登陆信息放入报文的实体部分,通常是以POST请求发送给服务器,而这时使用HTTPS通信来进行HTML表单画面的显示和用户输入数据的发送
  2. 服务器会发放用以识别用户的Session ID,通过验证从客户端送过来的登录信息进行身份认证,人后把用户认证状态与Session ID绑定后记录在服务器端,向客户端返回响应时,会在首部字段Set-Cookie内写入Sessin ID,为避免SessionId被盗,可在Cookie中加入httponly属性
  3. 客户端接收到从服务器发来的Session ID后,会将其作为Cookie保存哎本地,下次向服务器发送请求时,浏览器自动发送Cookie,服务器通过验证接收到的Session ID识别用户和其认证状态

Session认证需要配合Cookie实现,由于Cookie默认不支持跨域访问,所以当涉及前端跨域请求后端接口时需要做额外配置。因此当前端请求后端接口不涉及跨域请求时推荐使用Session身份认证机制,否则使用JWT认证

流程:

  • 浏览器登录发送账号密码,服务端查询用户库,校验用户
  • 服务端把用户登录状态存为Session,生成一个sessionId
  • 通过登录把接口返回,把sessionId set到cookie上
  • 此后浏览器再请求业务接口,sessionId随cookie带上
  • 服务端查sessionId检验session
  • 成功后正常做业务处理,返回结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const express=require('express');
const session=require('express-session');
const cors=require('cors');
const bodyParser=require('body-parser')
//var formParser=require('express-formidable')
const app=express();
app.use(cors());
app.use(
session({
secret:'xiaoqi',
resave:false,
saveUninitialized:true
})
)
//托管静态页面
app.use(express.static('./pages'));
//自定义解析Post提交过来的表单数据
app.use(bodyParser.urlencoded({extended:false}))
app.use(bodyParser.json())
//登录API接口
app.post('/api/login',(req,res)=>{
//判断用户提交的登录信息是否正确
console.log(req.body)
if(req.body.username!=="admin"||req.body.password!=="888888"){
return res.send({status:1,msg:'登录失败'})
}
//登录成功则把信息存储在Session中
console.log(req.body);
req.session.user=req.body;//用户登录信息
req.session.islogin=true;//用户登录状态
res.send({status:0,msg:'登录成功'})

})
//获取用户姓名的接口
app.get('/api/username',(req,res)=>{
//从Session中获取用户姓名响应给客户端
if(!req.session.islogin){
return res.send({status:1,msg:'fail'})
}
res.send({
status:0,
msg:'success',
username:req.session.user.username,
})

})
//退出登录的接口
app.post('/api/logout',(req,res)=>{
//清空当前客户端的session信息
req.session.destroy()
res.send({
status:0,
msg:'退出登录成功'
})

})
app.listen(80,()=>{
console.log('运行在http://127.0.0.1')
})


JWT认证机制:

工作原理

jwt

jwt组成部分:

头部.有效荷载.签名

Header.PayLoad.Signature

头部由两部分组成:令牌类型即JWT,以及正在使用的签名算法,例如HMAC SHA256或RSA

例如:

1
2
3
4
5
{
"alg":"HS256",
"typ":"JWT"

}

这个JSON被Base64Url编码形成JWT第一部分

有效载荷:

包含声明,声明式关于实体(通常是用户)和附加数据的陈述,

身份认证中:

当用户使用凭据成功登录后,将返回一个JSON Web Token,由于token是凭据,因此要小心出现安全问题,通常,不应该将令牌保留超过所需的时间,也不应该将敏感数据存储在浏览器存储中,token在Authorization标头中发送,则跨域资源共享不会成为问题,因为它不使用cookie

客户端收到服务器返回的JWT后,通常会将它存储在localStorage或者sessionStorage中,此后客户端每次与服务端通信,都要带上这个JWT的字符串,进行身份认证,推荐把JWT放在Http请求头的Authorization字段中

Authorization:Bearer

为什么使用JWT:

应用程序可以使用访问access token去访问受保护的资源,比如一个接口

在Express中使用JWT,express-jwt会自动把JWT的payload部分赋值于req.user,方便逻辑部分调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
const express=require('express');
const app=express();
const cors=require('cors');
//导入用于生成jwt字符串的包
const jwt=require('jsonwebtoken');
//导入用于将客户端发送过来的JWT字符串解析还原成JSON对象的包
const expressJWT=require("express-jwt");
//秘钥的本质是字符串
const secretkey='xiaochunzuishuai^_^';
//以/api/开头的不用权限,配置成功了express-jwt这个中间件,就可以把解析出来的用户信息挂载到req.user上
app.use(expressJWT({
secret:secretkey,
algorithms: ['HS256'],
}).unless({path:[/^\/api\//]}))
const bodyParser=require('body-parser')
app.use(cors());

//托管静态页面
app.use(express.static('./pages'));
//自定义解析Post提交过来的表单数据
app.use(bodyParser.urlencoded({extended:false}))
app.use(bodyParser.json())
//登录API接口
app.post('/api/login',(req,res)=>{
//判断用户提交的登录信息是否正确
const userInfo=req.body;
if(userInfo.username!=="admin"||userInfo.password!=="888888"){
return res.send({status:1,msg:'登录失败'})
}
//登录成功生成JWT字符串,通过token属性响应给客户端
res.send({
satus:0,
msg:'success',
//不要把密码加密到token字符串中
token:jwt.sign({username:userInfo.username},secretkey,{expiresIn:'100s'}),
data:req.user
})
})
//有权限的接口,配置成功了express-jwt这个中间件,有权限的接口就可以通过req.user获取解析出来的用户信息
app.get('/admin/getInfo',(req,res)=>{
res.send({
status:0,
msg:'success',
data:req.user
})

})
//退出登录的接口
app.post('/api/logout',(req,res)=>{
res.send({
status:0,
msg:'退出登录成功'
})

})
//配置全局错误处理中间件
app.use((err,req,res,next)=>{
//token解析失败导致的错误,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')
})