在不少应用中,咱们都须要向服务端提供本身的身份凭证来得到访问一些非公开资源的受权。好比在一个博客平台,咱们要修改本身的博客,那么服务端要求咱们可以证实 “我是我” ,才会容许咱们修改本身的博客。
为用户提供受权以容许用户操做非公开资源,有不少种方式。好比使用 token、session、cookie,还有容许第三方登陆受权的 OAuth 2.0.前端
为了理解这些技术的机制和它们之间的关系,本文就来一一使用这些方案实现一个前端经过后端验证受权来访问后端服务的应用。ios
咱们将用 express 搭建一个简单的后端,为了保存用户信息,咱们使用 mongoDB。前端是一个注册页面和一个登陆页面,此外还有一个修改用户密码的页面,在这个页面上修改密码的操做只有在用户登陆以后才被容许,也就是被服务端受权以后才能修改密码,不然返回 401 未受权。git
下面就是咱们这个简单 demo 的文件结构:github
服务端结构:web
前端页面结构:redis
如上图,咱们在服务端写了4个路由分别用于用户注册、登陆、修改密码、和登出。其中在登陆路由中,用户登陆以后将会生成一个用户凭证,在后续修改密码的路由中将会利用这个凭证来受权用户修改密码。具体的代码根据不一样的受权方案而有所不一样。前端相应地分为注册、登陆、修改密码 3 个页面:算法
注册页面:数据库
登陆页面:express
修改密码页面:npm
咱们最终实现的效果就是:
(GIF图过大,能够转到GitHub项目地址查看:地址)
搭建起一个先后端分离的应用框架以后,咱们下面依次使用 token、OAuth 2.0、express-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。
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。
重点中的重点,巨坑中的巨坑:使用 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 有效时间 } }))
首先来看看 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 的方式有如下两种:
在 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。
在这种状况下,咱们须要使用 httpOnly 来使客户端脚本没法访问到 cookie,才能保证 token 安全。这样就避免了 CSRF 攻击。
jsonwebtoken 主要 API:
若是有 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
若是有 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)
只是解码 token 中的 payload,不会验证 token。
options 参数有:
1)json: 强制在 payload 用JSON.parse 序列化,即便头部没有声明 "typ":"JWT" 2)complete: true 则返回解码后的包含 payload 和 header 的对象.
在验证 token 的过程当中可能或抛出错误,jwt.verify() 的回调的第一个参数就是 err,err 对象有一下几种类型:
err = { name: 'TokenExpiredError', message: 'jwt expired', expiredAt: 1408621000 }
err = { name: 'JsonWebTokenError', message: 'jwt malformed' /* message 有如下几个可能的值: 'jwt malformed' 'jwt signature is required' 'invalid signature' 'jwt audience invalid. expected: [OPTIONS AUDIENCE]' 'jwt issuer invalid. expected: [OPTIONS ISSUER]' 'jwt id invalid. expected: [OPTIONS JWT ID]' 'jwt subject invalid. expected: [OPTIONS SUBJECT]' */ }
err = { name: 'NotBeforeError', message: 'jwt not active', date: 2018-10-04T16:10:44.000Z }
HS25六、HS38四、HS5十二、RS256 等。
后端登陆接口如今须要改用 JWT 来签发 token,把原来使用 express-session 的代码去掉:
if(olduser.password === password){// 密码正确 /* // 受权方法 1. session req.session.username = olduser.username req.session.userID = olduser._id console.log('登陆时的会话 ID:',req.sessionID) req.session.cookie.maxAge = 60*60*1000 req.session.save() res.send({code:0})// 登陆成功 */ // 受权方法 2. JWT let token = JWT.sign( {username:olduser.username, exp:Date.now() + 1000 * 60}, // payload secret, // 签名密钥 {algorithm} // 签名算法 ) res.send({ code:0, token }) }else{ res.send({code:2}) // 密码错误 }
后端给前端发回了 token,前端须要存储 token 以便于后续请求受权,能够存储在 localStorage ,在修改密码页面再取出 localStorage 中 的 token,并再 axios 发送请求以前拦截请求,在请求头的 Authorization 中带上 token:
前端存储 token:
// src/pages/login.js: alert('登陆成功,请修改密码') localStorage.setItem('token',res.data.token)
前端拦截 axios 请求,从 localStorage 中取出保存好的 token,在请求头带上 token:
// src/axios.config.js: // 支持 JWT 的 axios 配置 export function axios_JWT(){ axios.interceptors.request.use(config => { // 在 localStorage 获取 token let token = localStorage.getItem("token"); console.log('axios配置:token',token) // 若是存在则设置请求头 if (token) { config.headers['Authorization'] = token; console.log(config) } return config; }); return axios }
前端修改密码页面调用能够拦截请求的 aios 来发送修改密码的请求:
// src/pages/ModifyUserInfo.js: // 支持 JWT 的 axios 调用 let res = await axios_JWT().post('http://localhost:3002/modify',{newPassword:input.current.value})
后端修改密码接口调用 JWT 的用户认证中间件:
认证中间件:
const JWT = require('jsonwebtoken') const secret = require('../server.config').JWT_config.secret const algorithm = require('../server.config').JWT_config.algorithm function JWT_auth(req,res,next){ let authorization = req.headers["authorization"] console.log('authorization',authorization) if(authorization) try{ let token = authorization; JWT.verify(token,secret,{algorithm:'HS256'},(err,decoded)=>{ // 用户认证 if(err){ console.log(err) next(err) }else{ console.log(decoded) req.username = decoded.username // 在 req 上添加 username,以便于传到下一个中间件取出 username 来查询数据库 next() } }) }catch(err){ res.status(401).send("未受权"); } else res.status(401).send("未受权"); } module.exports = JWT_auth
有的应用会提供第三方应用登陆,好比掘金 web 客户端提供了微信、QQ帐号登陆,咱们能够不用注册掘金帐号,而能够用已有的微信帐号登陆掘金。看看用微信登陆掘金的过程:
step1: 打开掘金,未登陆状态,点击登陆,掘金给咱们弹出一个登陆框,上面有微信、QQ登陆选项,咱们选择微信登陆;<br/>
step2: 以后掘金会将咱们重定向到微信的登陆页面,这个页面给出一个二维码供咱们扫描,扫描以后;<br/>
step3: 咱们打开微信,扫描微信给的二维码以后,微信询问咱们是否赞成掘金使用咱们的微信帐号信息,咱们点击赞成;<br/>
step4: 掘金刚才重定向到微信的二维码页面,如今咱们赞成掘金使用咱们的微信帐号信息以后,又重定向回掘金的页面,同时咱们能够看到如今掘金的页面上显示咱们已经处于登陆状态,因此咱们已经完成了用微信登陆掘金的过程。<br/>
这个过程比咱们注册掘金后才能登陆要快捷多了。这归功于 OAuth2.0 ,它容许客户端应用(掘金)能够访问咱们的资源服务器(微信),咱们就是资源的拥有者,这须要咱们容许客户端(掘金)可以经过认证服务器(在这里指微信,认证服务器和资源服务器能够分开也能够是部署在同一个服务上)的认证。很明显,OAuth 2.0 提供了4种角色,资源服务器、资源的拥有者、客户端应用 和 认证服务器,它们之间的交流实现了 OAuth 2.0 整个认证受权的过程。
OAuth 2.0 登陆的原理,根据4中不一样的模式有所不一样。本文使用受权码模式,因此只讲受权码模式下 OAuth2.0 的登陆过程,其余模式能够自行搜索学习。
能够参考GitHub 官网。
下面咱们改用 OAuth2.0 来使用 GitHub 帐号来受权咱们上面的应用,从而修改咱们应用的密码。
步骤:
填写咱们的应用名称、应用首页和受权须要的回调 URL:
这个登陆入口其实就是一个指向 GitHub 登陆页面的链接
<a href='https://github.com/login/oauth/authorize?client_id=211383cc22d28d9dac52'> 使用 GitHub 帐号登陆 </a>
咱们能够在 http://localhost:3002/login/callback 这个路由获取 code 受权码,并结合咱们以前得到的 client-id、client_secret,向https://github.com/login/oaut...,token 获取以后,咱们能够用这个 token向 https://api.github.com/user?a... 请求到用户的GitHub帐号信息好比GitHub用户名、头像等等。
// server/routes/login.js: // 使用 OAuth2.0 时的登陆接口, router.get('/callback',async (req,res,next)=>{//这是一个受权回调,用于获取受权码 code var code = req.query.code; // GitHub 回调传回 code 受权码 console.log(code) // 带着 受权码code、client_id、client_secret 向 GitHub 认证服务器请求 token let res_token = await axios.post('https://github.com/login/oauth/access_token', { client_id:Auth_github.client_id, client_secret:Auth_github.client_secret, code:code }) console.log(res_token.data) let token = res_token.data.split('=')[1].replace('&scope','') // 带着 token 从 GitHub 获取用户信息 let github_API_userInfo = await axios.get(`https://api.github.com/user?access_token=${token}`) console.log('github 用户 API:',github_API_userInfo.data) let userInfo = github_API_userInfo.data // 用户使用 GitHub 登陆后,在数据库中存储 GitHub 用户名 users.findOne({username:userInfo.name},(err,oldusers)=>{ // 看看用户以前有没有登陆过,没有登陆就会在数据库中新增 GitHub 用户 if(oldusers) { res.cookie('auth_token',res_token.data) res.cookie('userAvatar',userInfo.avatar_url) res.cookie('username',userInfo.name) res.redirect(301,'http://localhost:8082') // 从GitHub的登陆跳转回咱们的客户端页面 return }else new users({ username:userInfo.name, password:'123', // 为使用第三方登陆的可以用户初始化一个密码,后面用户能够本身去修改 }).save((err,savedUser)=>{ if(savedUser){ res.cookie('auth_token',res_token.data) res.cookie('userAvatar',userInfo.avatar_url) res.cookie('username',userInfo.name) res.redirect(301,'http://localhost:8082') // 从GitHub的登陆跳转回咱们的客户端页面 } }) }) }, ) module.exports = router
在请求到用户的GitHub信息以后,咱们能够将用户头像和用户名存在cookie、里,便于发送给前端在页面上显示出来,告诉用户他已经用GitHub帐号登陆了咱们的客户端。
同时,咱们把GitHub用户名存到咱们本身的数据库里,并给一个‘123’简单的初始化密码,后面用户能够在得到权限后修改密码。
咱们使用和 JWT 同样的发送token的方式,前面咱们从GitHub得到用户的token以后有已经用cookie的方式将其发送给前端,咱们在前端能够读取cookie里的token,而后将其经过 Authorization 头方式给后端验证:
前端读取 token,并加到 Authorization 里:
// OAuth2.0 axios.interceptors.request.use(config => { // 在 localStorage 获取 token let token = localStorage.getItem("token"); console.log('axios配置:token',token) // 若是存在则设置请求头 if(document.cookie){ let OAtuh_token = unescape(document.cookie.split(';').filter(e=>/auth_token/.test(e))[0].replace(/auth_token=/,'')) config.headers['Authorization'] = OAtuh_token; console.log(config) } return config; });
后端验证中间件 :
const axios = require('axios') const OAuth=async (req,res,next)=>{ let OAuth_token = req.headers["authorization"] console.log('authorization',OAuth_token) console.log('OAuth 中间件拿到cookie中的token:',OAuth_token) if(OAuth_token) { let token = OAuth_token.split('=')[1].replace('&scope','') let github_API_userInfo = await axios.get(`https://api.github.com/user?access_token=${token}`) let username = github_API_userInfo.data.name req.username = username next() } else res.status(401) } module.exports = OAuth
session、JWT、OAuth2.0 这三种受权方式每一种里面都会有其余方式的影子,主要是体如今用户凭证的存储和发送上,好比一般所说的基于服务端的 session,它能够把用户凭证,也就是 session ID 存储在服务端(内存或者数据库redis等),可是也是能够发给前端经过cookie保存的。JWT 能够把做为用户凭证的 token 在服务端签发后发给用户保存,能够在 localStorage 保存,一样也能够保存在 cookie 。OAuth2.0是比较复杂的一种受权方式,可是它后面得到 token 后也能够像 JWT 同样处理 token 的保存和验证来受权用户。
不论是哪一种方式,都会有一些要注意的安全问题,还有性能上须要兼顾的地方。这里有关这方面再也不赘述。
最后,本项目的地址:https://github.com/qumuchegi/auth-demo