摘要:为用户提供授权以允许用户操作非公开资源,有很多种方式。具体的代码根据不同的授权方案而有所不同。使用授权原理利用来验证用户,有两种机制实现。使用来实现用户授权主要用于签发如果有将异步的签名。
在很多应用中,我们都需要向服务端提供自己的身份凭证来获得访问一些非公开资源的授权。比如在一个博客平台,我们要修改自己的博客,那么服务端要求我们能够证明 “我是我” ,才会允许我们修改自己的博客。
为用户提供授权以允许用户操作非公开资源,有很多种方式。比如使用 token、session、cookie,还有允许第三方登录授权的 OAuth 2.0.
为了理解这些技术的机制和它们之间的关系,本文就来一一使用这些方案实现一个前端通过后端验证授权来访问后端服务的应用。
我们将用 express 搭建一个简单的后端,为了保存用户信息,我们使用 mongoDB。前端是一个注册页面和一个登录页面,此外还有一个修改用户密码的页面,在这个页面上修改密码的操作只有在用户登录之后才被允许,也就是被服务端授权之后才能修改密码,否则返回 401 未授权。
下面就是我们这个简单 demo 的文件结构:
服务端结构:
前端页面结构:
如上图,我们在服务端写了4个路由分别用于用户注册、登录、修改密码、和登出。其中在登录路由中,用户登录之后将会生成一个用户凭证,在后续修改密码的路由中将会利用这个凭证来授权用户修改密码。具体的代码根据不同的授权方案而有所不同。前端相应地分为注册、登录、修改密码 3 个页面:
注册页面:
登录页面:
修改密码页面:
我们最终实现的效果就是: (GIF图过大,可以转到GitHub项目地址查看:地址)
搭建起一个前后端分离的应用框架之后,我们下面依次使用 token、OAuth 2.0、express-session 来实现用户授权。
1. 使用 session 授权 1.1 session 原理:利用 session 来验证用户,有两种机制实现。
需要服务端在用户登录成功后生成一个 session ID 保存在服务端,这个session ID 标识当前会话的用户,以后用户的每一次请求中都会包含session ID,服务端可以识别这个 session ID 验证用户身份然后才会授权。
把 session ID 和其他数据加密后发给用户,由用户来存储并在以后每次请求中发给服务端来验证。比如可以用 cookie 存储发送,也可以使用其他客户端存储。
本文使用 express-session 来实现。并且使用上述 session 的第一种机制。所以先来看一下 express-session 主要的 API:
session( options ):生成 session 中间件,使用这个中间件会在当前会话中创建 session,session 数据将会被保存在服务端,而 session ID 会保存在 cookie。options 为传入的配置参数,有以下这些参数:
1. cookie: 存储 session ID, 默认值 { path: ‘/‘, httpOnly: true,secure: false, maxAge: null }) 2. genid: 一个函数,返回一个字符串用来作为新的 session ID,传入 req 可以按需在 req 上添加一些值。 3. name: 存储 session ID 的 cookie 的名字,默认是"connect.sid",但是如果有多个使用 express-session 的 app 运行在同一个服务器主机上,需要用不同的名字命名 express-session 的 cookie。 4. proxy : 当设置了secure cookies(通过”x-forwarded-proto” header )时信任反向代理。 5. resave: 强制保存会话,即使会话在请求期间从未被修改过 6. rolling: 强制在每次响应时,都设置保存会话标识符的cookie。cookie 到期时间会被重置为原始时间 maxAge。默认值为`false`。 7. saveUninitialized: 默认 `true`, 强制存储未初始化的 session。 8. secret ( 必需 ): 用来对session ID cookie签名,可以提供一个多带带的字符串作为 secret,也可以提供一个字符串数组,此时只有第一个字符串才被用于签名,但是在 express-session 验证 session ID 的时候会考虑全部字符串。 9. store: 存储 session 的实例。 10. unset: 控制 req.session 是否取消。默认是 `keep`,如果是 `destroy`,那么 session 就会在响应结束后被终止。
req.session:这是 express-session 存放 session 数据的地方,注意,只有 session ID 存储在 cookie,所以 express-session 会自动检查 cookie 中的 session ID ,并用这个 session ID 来映射到对应的 session 数据,所以使用 express-session 时我们只需读取 req.session ,express-session 知道应该读取哪个 session ID 标识的 session 数据。
1. 可以从 req.session 读取 session : req.session.id:每一个 session 都有一个唯一ID来标识,可以读取这个ID,而且只读不可更改,这是 req.sessionID 的别名; req.session.cookie:每一个 session 都有一个唯一 的cookie来存储 session ID,可以通过 req.session.cookie 来设置 cookie 的配置项,比如 req.session.cookie.expires 设置为 false ,设置 req.session.cookie.maxAge 为某个时间。 2. req.session 提供了这些方法来操作 session: req.session.regenerate( callback (err) ): 生成一个新的 session, 然后调用 callback; req.session.destroy( callback (err) ): 销毁 session,然后调用 callback; req.session.reload( callback (err) ): 从 store 重载 session 并填充 req.session ,然后调用 callback; req.session.save( callback (err) ): 将 session 保存到 store,然后调用 callback。这个是在每次响应完之后自动调用的,如果 session 有被修改,那么 store 中将会保存新的 session; req.session.touch(): 用来更新 maxAge。
req.sessionID:和 req.session.id 一样。
store:如果配置这个参数,可以将 session 存储到 redis和mangodb 。一个使用 rtedis 存储 session 的例子。store 提供了一下方法来操作 store:
1. store.all( callback (error, sessions) ) : 返回一个存储store的数组; 2. store.destroy(sid, callback(error)): 用session ID 来销毁 session; 3. store.clear(callback(error)): 删除所有 session 4. store.length(callback(error, len)): 获取 store 中所有的 session 的数目 5. store.get(sid, callbackcallback(error, session)): 根据所给的 ID 获取一个 session 6. store.set(sid, session, callback(error)): 设置一个 session。 7. store.touch(sid, session, callback(error)): 更新一个 session
以上就是 express-session 的全部 API。
1.3 使用 express-session
重点中的重点,巨坑中的巨坑:使用 express-session 是依赖于 cookie 来存储 session ID 的,而 session ID 用来唯一标识一个会话,如果要在一个会话中验证当前会话的用户,那么就要求用户前端能够发送 cookie,而且后端能够接收 cookie。所以前端我们设置 axios 的 withCredentials = true 来设置 axios 可以发送 cookie,后端我们需要设置响应头 Access-Control-Allow-Credentials:true,并且同时设置 Access-Control-Allow-Origin 为前端页面的服务器地址,而不能是*。我们可以用 cors 中间件代替设置:
// 跨域
app.use(cors({
credentials: true,
origin: "http://localhost:8082", // web前端服务器地址,,不能设置为 *
}))
我开始就是因为没有设置这个,所以遇到了问题,就是后端登录接口在session中保存 用户名( req.session.username = req.body.username) 之后,在修改用户密码的接口需要读取 req.session.username 以验证用户的时候读取不到 req.session.username ,很明显两个接口的 req.session 不是同一个 session,果然 console 出来 的 session ID 是不同的。这就让我想到了 cookie,cookie 是生成之后每次请求都会带上并且后端可以访问的,现在存储在 cookie 中的 session ID 没有被读取到而是读取到了新 session ID,所以问题就出在后端不能拿到 cookie,也有可能是因为前端发送不出去 cookie。可是开始的时候搜索关于 session ID 读取不一致的这个问题我找不到解决办法,而且发现很多人存在同样的问题,但是没有人给出答案,现在通过自己的思考想到了解决办法,这是很多人需要避免的巨坑。
现在跨过了最大的一个坑,我们就可以来编写前后端所有的逻辑了。关于注册的逻辑,是一个很简单的用户注册信息填写页面,它发送用户的名字和密码到后端注册接口,后端注册接口保存用户的名字和密码到数据库理。因此我在这里省略掉前端注册页面和后端注册接口,只讲前端登录页面和后端登录接口,前端修改密码页面和后端修改密码接口和登出接口。
前端登录接口:
async function login(){ // 登录
let res = await axios.post("http://localhost:3002/login",{username,password})
if(res.data.code === 0){
setLoginSeccess(true)
alert("登录成功,请修改密码")
}else if(res.data.code === 2){
alert("密码不正确")
return
}else if(res.data.code === 1){
alert("没有该用户")
return
}
}
后端登录接口:
const getModel = require("../db").getModel
const router = require("express").Router()
const users = getModel("users")
router.post("/", (req,res,next)=>{
let {username, password} = req.body
users.findOne({username},(err,olduser)=>{
if(!olduser){
res.send({code:1})// 没有该用户
}else{
if(olduser.password === password){// 登陆成功,生成 session
req.session.username = olduser.username
req.session.userID = olduser._id
console.log("登录时的会话 ID:",req.sessionID)
req.session.save()
res.send({code:0})// 登录成功
}else{
res.send({code:2}) // 密码错误
}
}
})
})
module.exports = router
前端修改密码和登出页面:
// src/axios.config.js:
// 支持 express-session 的 axios 配置
export function axios_session(){
axios.defaults.withCredentials = true
return axios
}
async function modify(){ // 修改密码
if(!input.current.value) return alert("请输入新密码")
try{
// 支持 session 的 axios 调用
let res = await axios_session().post("http://localhost:3002/modify",{newPassword:input.current.value})
if(res.data.code === 0)
alert("密码修改成功")
}catch(err){
alert("没有授权 401")
console.log(err)
}
}
async function logout(){ // 登出
let res = await axios.post("http://localhost:3002/logout")
if(res.data.code === 0){
history.back()
}
}
后端修改密码接口:
const getModel = require("../db").getModel
const router = require("express").Router()
const users = getModel("users")
const sessionAuth = require("../middlewere/sessionAuth")
router.post("/", sessionAuth, (req,res,next)=>{
let {newPassword} = req.body
console.log("修改密码时的会话 ID:",req.session.id)
if(req.session.username){
users.findOne({username: req.session.username},(err,olduser)=>{
olduser.password = newPassword
olduser.save(err=>{
if(!err){
res.send({code:0})// 修改密码成功
}
})
})
}
})
module.exports = router
sessionAuth 验证中间件:
const sessionAuth = (req,res,next)=>{
if(req.session && req.session.username){// 验证用户成功则进入下一个中间件来修改密码
next()
}else{// 验证失败返回 401
res.sendStatus(401)
}
}
module.exports = sessionAuth
后端登出:
const router = require("express").Router()
router.post("/", (req,res,next)=>{
req.session.destroy(()=>console.log("销毁session,已经推出登录"))
res.send({code:0})
})
module.exports = router
我们还需要调用 session 的中间件,配置一些参数,才能在之后的中间件中使用 req.session 来进行存储、读取和销毁 session 的操作:
// server/app.js:
// session
app.use(session({
secret: "123456789",// 必需,用来签名 session
unset:"destroy",// 在每次会话就熟后销毁 session
resave:true,
saveUninitialized:false,
rolling:true,
cookie:{
maxAge:60*60*1000// session ID 有效时间
}
}))
2. 使用 JWT 授权 2.1 JWT 的原理:
首先来看看 JWT 的概念,JWT 的 token 由 头部(head)、数据(payload)、签名(signature) 3个部分组成 具体每个部分的结构组成以及JWT更深的讲解可以看看这个。其中头部(header)和数据(payload)经过 base64 编码后经过秘钥 secret的签名,就生成了第三部分----签名(signature) ,最后将 base64 编码的 header 和 payload 以及 signature 这3个部分用圆点 . 连接起来就生成了最终的 token。
signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) token = base64UrlEncode(header) + "." + base64UrlEncode(payload) + signature
token 生成之后,可以将其发送给客户端,由客户端来存储并在以后每次请求中发送会后端用于验证用户。前端存储和发送 token 的方式有以下两种:
2.1.1 使用 Header.Authorization + localStorage 存储和发送 token
在 localStorage 中存储 token,通过请求头 Header 的 Authorization 字段将 token发送给后端。
这种方法可以避免 CSRF 攻击,因为没有使用 cookie ,在 cookie 中没有 token,而 CSRF 就是基于 cookie 来攻击的。虽然没有 CSRF ,但是这种方法容易被 XSS 攻击,因为 XSS 可以攻击 localStorage ,从中读取到 token,如果 token 中的 head 和 payload 部分没有加密,那么攻击者只要将 head 和 payload 的 base64 形式解码出来就可以看到head 和payload 的明文了。这个时候,如果 payload 保护敏感信息,我们可以加密 payload。
2.1.2 使用 cookie 存储和发送 token:
在这种情况下,我们需要使用 httpOnly 来使客户端脚本无法访问到 cookie,才能保证 token 安全。这样就避免了 CSRF 攻击。
2.2 使用 jsonwebtoken 来实现 JWT 用户授权:
jsonwebtoken 主要 API:
1. jwt.sign(payload, secretOrPrivateKey, [options, callback]) 用于签发 token
如果有 callback 将异步的签名 token。
payload 就是我们要在 token 上装载的数据,比如我们可以在上面添加用户ID,用于数据库查询。payload可以是一个object, buffer或者string,payload 如果是 object,可以在里面设置 exp 过期时间。
secretOrPrivateKey 即包含HMAC算法的密钥或RSA和ECDSA的PEM编码私钥的string或buffer,是我们用于签名 token 的密钥,secretOrPublicKey 应该和下面 的 jwt.verify 的 secretOrPublicKey 一致。
options 的参数有:
1)algorithm (default: HS256) 签名算法,这个算法和下面将要讲的 jwt.verify 所用的算法一个一致 2)expiresIn: 以秒表示或描述时间跨度zeit / ms的字符串。如60,"2 days","10h","7d",含义是:过期时间 3)notBefore: 以秒表示或描述时间跨度zeit / ms的字符串。如:60,"2days","10h","7d" 4)audience:Audience,观众 5)issuer: Issuer,发行者 6)jwtid: JWT ID 7)subject: Subject,主题 8)noTimestamp: 9)header 10)keyid 11)mutatePayload
2. jwt.verify(token, secretOrPublicKey, [options, callback]) 用于验证 token
如果有 callback 将异步的验证 token。
token 便是我们保存在前端的token,我们将它发送给后端,后端调用 jwt.verify 并接受 token 和传入放在后端的 secretOrPublicKey 来验证 token。注意这里的 secretOrPublicKey 与之前用于签发 token 的 secretOrPublicKey 应该是同一个。
options 的参数有:
1)algorithms: 一个包含签名算法的数组,比如 ["HS256", "HS384"]. 2)audience: if you want to check audience (aud), provide a value here. The audience can be checked against a string, a regular expression or a list of strings and/or regular expressions. Eg: "urn:foo", /urn:f[o]{2}/, [/urn:f[o]{2}/, "urn:bar"] 3)complete: return an object with the decoded { payload, header, signature } instead of only the usual content of the payload. 4)issuer (optional): string or array of strings of valid values for the iss field. 5)ignoreExpiration: if true do not validate the expiration of the token. 6)ignoreNotBefore... 7)subject: if you want to check subject (sub), provide a value here 8)clockTolerance: number of seconds to tolerate when checking the nbf and exp claims, to deal with small clock differences among different servers 9)maxAge: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span zeit/ms. Eg: 1000, "2 days", "10h", "7d". A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default ("120" is equal to "120ms"). 10)clockTimestamp: the time in seconds that should be used as the current time for all necessary comparisons. 11)nonce: if you want to check nonce claim, provide a string value here. It is used on Open ID for the ID Tokens. (Open ID implementation notes)
3. jwt.decode(token [, options]) 解码 token
只是解码 token 中的 payload,不会验证 token。 options 参数有:
1)json: 强制在 payload 用JSON.parse 序列化,即使头部没有声明 "typ":"JWT" 2)complete: true 则返回解码后的包含 payload 和 header 的对象.
4. 错误码
在验证 token 的过程中可能或抛出错误,jwt.verify() 的回调的第一个参数就是 err,err 对象有一下几种类型:
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/6742.html
摘要:为用户提供授权以允许用户操作非公开资源,有很多种方式。具体的代码根据不同的授权方案而有所不同。使用授权原理利用来验证用户,有两种机制实现。使用来实现用户授权主要用于签发如果有将异步的签名。注意这里的与之前用于签发的应该是同一个。 在很多应用中,我们都需要向服务端提供自己的身份凭证来获得访问一些非公开资源的授权。比如在一个博客平台,我们要修改自己的博客,那么服务端要求我们能够证明 我是...
摘要:框架具有轻便,开源的优点,所以本译见构建用户管理微服务五使用令牌和来实现身份验证往期译见系列文章在账号分享中持续连载,敬请查看在往期译见系列的文章中,我们已经建立了业务逻辑数据访问层和前端控制器但是忽略了对身份进行验证。 重拾后端之Spring Boot(四):使用JWT和Spring Security保护REST API 重拾后端之Spring Boot(一):REST API的搭建...
摘要:什么是鉴权鉴权是指验证用户是否拥有访问系统的权利。传统的鉴权是通过密码来验证的。这种方式的前提是,每个获得密码的用户都已经被授权。接下来就一一介绍一下这三种鉴权方式。 在系统级项目开发时常常会遇到一个问题就是鉴权,身为一个前端来说可能我们距离鉴权可能比较远,一般来说我们也只是去应用,并没有对权限这一部分进行深入的理解。 什么是鉴权 鉴权:是指验证用户是否拥有访问系统的权利。传统的鉴权是...
摘要:作为目前最主流的微服务框架,发展速度很快,成为了最全面的微服务解决方案。通过认证后,转发给内部相应的服务器。所有远程访问资源服务器相关的必须提供。 Part 1 - 理论相关 作者 freewolf 关键词 微服务、Spring Cloud、OAuth 2.0、JWT、Spring Security、SSO、UAA 写在前面 作为从业了十多年的IT行业和程序的老司机,今天如果你说你不懂...
摘要:作为目前最主流的微服务框架,发展速度很快,成为了最全面的微服务解决方案。通过认证后,转发给内部相应的服务器。所有远程访问资源服务器相关的必须提供。 Part 1 - 理论相关 作者 freewolf 关键词 微服务、Spring Cloud、OAuth 2.0、JWT、Spring Security、SSO、UAA 写在前面 作为从业了十多年的IT行业和程序的老司机,今天如果你说你不懂...
阅读 2269·2019-08-30 15:56
阅读 3107·2019-08-30 13:48
阅读 1122·2019-08-30 10:52
阅读 1489·2019-08-29 17:30
阅读 416·2019-08-29 13:44
阅读 3527·2019-08-29 12:53
阅读 1112·2019-08-29 11:05
阅读 2666·2019-08-26 13:24