登录鉴权就是判断用户是否有权利登录该网站
其过程👇👇👇:
Cookie🍪
1.1 Cookie 介绍
由于 HTTP 是无状态的协议,不能保存每一次请求的状态,所以需要给客户端增加 Cookie 来保存客户端的状态。
Cookie 的作用主要用于 用户识别 和 状态管理 。
// 请求首部字段
Cookie: key=value; expires=true, 05 Jul 2016 07:23:31 GMT; path=/; domain=.baidu.com
// 响应首部字段
Set-Cookie:key = value;
- key-value : Cookie 值
- expires=date : Cookie 的有效期(默认的是浏览器关闭之前)
1.2 有效期
Cookie 过期之后,浏览器删除且不再携带 Cookie
- Expires :具体的过期时间
- Max-age :过期的时间范围,浏览器接受 Cookie 开始计时
1.3 作用域
浏览器在发送请求之前,检查 Cookie 的 domain 和 path,如果不一致,则不携带 Cookie。
- path :将服务器的文件目录作为 Cookie 的适用对象
- domain = 域名 : 作为 Cookie 适用对象的域名。如:
naiyou.com
,则www.naiyou.com
、www.naiyou2.com
也可以访问。
1.4 安全性
在 Cookie 传输和管理的时候,要确保 Cookie 的安全性,不被窃取。
- Secure :仅在 HTTPS 安全通信时才会发送 Cookie
- HttpOnly :设置 Cookie 不能被 Javascript 脚本访问(防止跨站脚本攻击 XSS 对 Cookie 信息的窃取)
- SameSite :防止跨站伪造 CSRF 的攻击
- Strict :浏览器完全禁止第三方请求携带 Cookie
- Lax :只能在 get 方法提交表单情况或者 a 标签发送 get 请求的情况下可以携带 Cookie
- None :默认,请求会自动携带上 Cookie
1.5 局限性
- 大小:只有 4KB,只能存储少量信息
- 性能:如果不设置 Cookie 的 Domain 和 path ,Cookie 就会被发送到各个域名下,随着请求数量的增多,性能也会出现很大的问题
- 安全: Cookie 明文传输,被攻击劫持之后,服务器的资源会被窃取。且如果不设置 HttpOnly 攻击者会通过 JS 获取到 Cookie
1.6 JS 操作 cookie
设置
/**
* @param {String} key 属性
* @param {*} value 值
* @param { String } expire 过期时间,单位天
*/
function cookieSet(key, value, expire) {
const d = new Date();
d.setDate(d.getDate() + expire);
document.cookie = `${key}=${value};expires=${d.toUTCString()}`
};
获取
/**
* @param {String} key 属性
* @returns
*/
function cookieGet(key) {
const cookieStr = decodeURIComponent(document.cookie)
const arr = cookieStr.split('; ')
let cookieValue = ''
for (let i = 0; i < arr.length; i++) {
const temp = arr[i].split('=')
if (temp[0] === key) {
cookieValue = temp[1]
break
}
}
return cookieValue
}
删除
/**
* @param {String} key 属性
*/
function cookieRemove(key) {
document.cookie = `${encodeURIComponent(key)}=;expires=${new Date()}`
}
Session🗝️
2.1 什么是 Session
服务器要给每个客户端分配不同的"身份标识",然后客户端每次向服务器发请求的时候,都带上这个”身份标识“,服务器就知道这个请求来自哪里。客户端一般使用 Cookie 来保存这个身份信息。
2.2 如何使用
服务端session + 客户端 sessionId
-
用户向服务器发送用户名和密码
-
服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色, 登陆时间等
-
服务器向用户返回一个
session_id
, 写入用户的cookie
-
用户随后的每一次请求, 都会通过
cookie
, 将session_id
传回服务器 -
服务端收到
session_id
, 找到前期保存的数据, 由此得知用户的身份
OAuth✔️
认证:你怎么证明你是你自己❓❓❓
OAuth 是一种行业标准的授权方式。这其中有三个角色:客户端,服务端,授权服务器
code 认证方式流程
以 github 授权为例
登录你的 github 账号,点击头像下拉列表的 settings --> 左侧边栏的 Developer settings --> 左侧边栏的 OAuth Apps --> 点击 New OAuth App
生成的 Client ID 和 Client secrets,会用在后续的请求中。
// 固定URL
const GITHUB_OAUTH_URL = 'https://github.com/login/oauth/authorize'
// 需要的权限。如果没有赋值,则是最简单的权限,user 则是用户权限
const SCOPE = 'user'
// github 上注册的
const client_id = '7a4**********013866'
module.exports = {
github: {
client_id: '7a4**********013866',
client_secret: 'a9fce********************7620b1f9e717',
request_token_url: 'https://github.com/login/oauth/access_token'
},
GITHUB_OAUTH_URL,
OAUTH_URL: `${GITHUB_OAUTH_URL}?client_id=${client_id}&scope=${SCOPE}`
}
更多资料请访问:https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html
Token🔰
JWT 的 全称是 JSON Web Token ,一个 JWT 由 dot(.) 分隔的三个部分组成 :
- Header(头部)
- Payload(载荷)
- Signature(签名)
xxxxxx.yyyyyyyy.zzzzzzz
const sign = HMACSHA256( base64.encode(header) + "." + base64.encode(payload),secret)
const jwt = base64.encode(header) + "." + base64.encode(payload) + "." + sign
Header (头部)
Header 是一个 JSON 对象
{
"alg": "HS256", // 表示签名的算法,默认是 HMAC SHA256(写成 HS256)
"typ": "JWT" // 表示Token的类型,JWT 令牌统一写为JWT
}
Payload(载荷)
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。包含用户的一些信息,以及 Token 的过期时间
{
// 7个官方字段
"iss": "a.com", // issuer:签发人
"exp": "1d", // expiration time: 过期时间
"sub": "test", // subject: 主题
"aud": "xxx", // audience: 受众
"nbf": "xxx", // Not Before:生效时间
"iat": "xxx", // Issued At: 签发时间
"jti": "1111", // JWT ID:编号
// 可以定义私有字段
"name": "John Doe",
"admin": true
}
Signature (签名)
Signature 是对前两部分的签名,防止数据被篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后使用Header里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
算出签名后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
JWT = Base64(Header) + "." + Base64(Payload) + "." + $Signature
jwt.io网站
过程
- 用户通过用户名和密码发送请求
- 程序验证通过
- 返回一个签名的token给客户端
- 客户端储存token, 并且每次发送请求都会携带 token
- 服务端验证Token通过并返回数据
优缺点
优点
- 易扩展
- 支持移动设备
- 跨应用调用
- 安全
- 承载信息丰富
- 防 CSRF
- 适合移动端应用
- 无状态,编码数据
缺点
- 刷新与过期处理
- Playload不易过大
- 中间人攻击
在框架中的应用
koa + jwt
npm install koa-jwt --save
,npm install jsonwebtoken --save
const jsonwebtoken = require('jsonwebtoken')
/**
* 登录
* @param {Object} ctx
*/
async login(ctx) {
// 校验参数(看具体情况)
ctx.verifyParams({
name: { type: 'string', required: true },
password: { type: 'string', required: true }
})
const user = await User.findOne(ctx.request.body)
if (!user) {
ctx.throw(401, '用户不存在或密码不正确')
}
const { _id, name } = user
// secret =》 秘钥,可以写在一个配置文件中
// expiresIn 过期时间
const token = jsonwebtoken.sign({ _id, name }, secret, {
expiresIn: '12h'
})
// 当用户登录时,校验通过,返回一个 token,在请求之后的接口中,通过校验 token 来确定用户的身份。
ctx.body = { token }
}
config.js
module.exports = {
connectionStr: 'mongodb://localhost/zhihu',
secret: 'zhihu-jwt-secret'
}
校验 token
比如有一个评论接口,那么就可以在做真正的数据操作之前,使用中间件进行 token 判断
const jwt = require('koa-jwt')
const { secret } = require('../config.js')
const auth = jwt({secret})
router.post('/', auth, create)
express + jwt
npm install express-jwt --save
,npm install jsonwebtoken --save
如果上面每一个接口路由都需要加一个判断,那未免有点麻烦。比如对于一个后台管理系统,只有登录才可以进行操作。此时,可以在全局做登录校验。
例如:
登录接口
const jwt = require('jsonwebtoken')
router.post('/login', function (req, res, next) {
const username = req.body.username
const password = req.body.password
Admin.find({ username }, function (err, doc) {
if (err) {
res.json({
status: '1',
msg: '管理员不存在'
})
} else {
const dataPassword = doc[0].password
if (password === dataPassword) {
// PRIVATE_KEY => 秘钥
// JWT_EXPIRED => 过期时间
const token = jwt.sign(
{ username },
PRIVATE_KEY,
{ expiresIn: JWT_EXPIRED }
)
new Result({ token }, '登录成功').success(res)
}
}
})
})
app.js(整个项目的入口主文件)
const router = require('./router')
app.use('/', router)
router / index.js
const jwtAuth = require('./jwt')
// 首先进行登录校验(放在所有 use 的第一个)
router.use(jwtAuth)
// Token验证失败时,使用 use 捕获错误(捕获错误可以放在最后一个)
router.use((err, req, res, next) => {
console.log(err)
if (err.name && err.name === 'UnauthorizedError') {
const { status = 401, message } = err
new Result(null, 'Token验证失败', {
error: status,
errMsg: message
}).jwtError(res.status(status))
}
})
jwt.js
const jwt = require('express-jwt')
const { PRIVATE_KEY } = require('../utils/constant')
module.exports = jwt({
secret: PRIVATE_KEY,
credentialsRequired: true
}).unless({
// 以下接口不需要做登录校验
path: [
'/',
'/user/login'
]
})
egg + jwt
Controller 控制器
npm install egg-jwt --save
// 设置 jwt
async jwtSign({ id }) {
const { ctx, app } = this;
const username = ctx.params('username');
const token = app.jwt.sign({
id,
username
}, app.config.jwt.secret);
// app.config.jwt.secret => 秘钥
return token;
}
// 登录接口
async login() {
const { ctx, app } = this;
const { username, password } = ctx.params();
const user = await ctx.service.user.getUser(username, password);
if (user) {
const token = await this.jwtSign({
id: user.id,
username: user.username
});
this.success({
...this.parseResult(ctx, user),
token
});
} else {
this.error('该用户不存在');
}
}
koa + jwt + 小程序
npm install jsonwebtoken --save
,npm install basic-auth --save
小程序有点区别于平常的 web 开发。在生成的小程序目录中,有一个 app.js 。这是小程序的入口文件。
小程序启动时,会触发 App 中的 onLaunch 函数。
//app.js
App({
// 是否存在 token
onLaunch: function () {
if (!wx.getStorageSync('token')) {
this.onGetToken()
}
},
// 获取 token
onGetToken() {
wx.login({
success: (res) => {
if (res.code) {
wx.request({
url: 'http://localhost:3000/v1/token',
method: 'POST',
data: {
account: res.code,
type: 100
},
success: (res) => {
const code = res.statusCode.toString()
if (code.startsWith('2')) {
wx.setStorageSync('token', res.data.token)
}
}
})
}
}
})
},
// 校验 token
onVerifyToken() {
wx.request({
url: 'http://localhost:3000/v1/token/verify',
method: 'POST',
data: {
token: wx.getStorageSync('token')
},
success: (res) => {
}
})
},
})
对 http 做一些必要的封装
import { config } from '../config.js'
class HTTP {
request(params) {
if (!params.method) {
params.method = 'GET'
}
wx.request({
url: config.api_base_url + params.url,
method: params.method,
data: params.data,
header: {
'Authorization': config.Authorization
},
success:(res) => {
let code = res.statusCode.toString()
if (code.startsWith('2')) {
params.success && params.success(res.data)
} else {
let error_code = res.data.error_code
this._show_error(error_code)
}
},
fail: (res) => {
this._show_error(1)
}
})
}
}
export { HTTP }
对使用的 token 做一些处理
import { Base64 } from 'js-base64'
// 获取存储的 token
let token = wx.getStorageSync('token')
let base64 = Base64.encode(token + ':')
let w_base = "Basic " + base64
const config = {
// 服务器地址
api_base_url: 'http://localhost:3000/v1/',
Authorization: w_base
}
module.exports = {
config
}
使用
getMyFavor(success) {
let params = {
url: 'classic/favor',
success: success
}
this.request(params)
}
后端
获取 token 接口
const { WXManager } = require('../../services/wx')
// 获取 token
router.post('/', async (ctx) => {
// 校验参数(具体看情况)
const v = await new TokenValidator().validate(ctx)
let token = await WXManager.codeToToken(v.get('body.account'))
ctx.body = {
token
}
})
wx.js
const util = require('util')
const axios = require('axios')
const { User } = require('../models/user')
const { generateToken } = require('../../core/util')
const { Auth } = require('../../middlewares/auth')
class WXManager {
static async codeToToken(code) {
// code 小程序生成 微信
// openid 唯一标识
// 显示注册
// 唯一标识
// code
// appid
// appsecret
// url
const url = util.format(global.config.wx.loginUrl,
global.config.wx.appId,
global.config.wx.appSecret,
code)
const result = await axios.get(url)
if (result.status !== 200) {
throw new global.errs.AuthFailed("openid获取失败")
}
const errCode = result.data.errcode
const errMsg = result.data.errmsg
if (errCode) {
throw new global.errs.AuthFailed("openid获取失败: " + errMsg)
}
// opedId
// 建立档案 user uid
// openId
// 判断数据库是否存在微信用户 opendid
let user = await User.getUserByOpenid(result.data.openid)
// 如果不存在,就创建一个微信小程序用户
if (!user) {
user = await User.createUserByOpenid(result.data.openid)
}
return generateToken(user.id, Auth.AUSE)
}
}
module.exports = {
WXManager
}
generateToken.js
// 颁发令牌
const generateToken = function (uid, scope) {
const secretKey = global.config.security.secretKey
const expiresIn = global.config.security.expiresIn
const token = jwt.sign({
uid,
scope
}, secretKey, {
expiresIn
})
return token
}
module.exports = {
generateToken
}
auth.js(校验 token)
const basicAuth = require('basic-auth')
const jwt = require('jsonwebtoken')
class Auth {
constructor() {}
get m() {
// token 检测
// token 开发者 传递令牌
// token body header
// HTTP 规定 身份验证机制 HttpBasicAuth
return async (ctx, next) => {
const tokenToken = basicAuth(ctx.req)
let errMsg = "token不合法"
// 无带token
if (!tokenToken || !tokenToken.name) {
throw new global.errs.Forbidden(errMsg)
}
try {
var decode = jwt.verify(tokenToken.name, global.config.security.secretKey)
} catch (error) {
// token 不合法 过期
if (error.name === 'TokenExpiredError') {
errMsg = "token已过期"
}
throw new global.errs.Forbidden(errMsg)
}
ctx.auth = {
uid: decode.uid,
}
await next()
}
}
// 验证token是否有效
static verifyToken(token) {
try {
jwt.verify(token, global.config.security.secretKey)
return true;
} catch (e) {
return false
}
}
}
module.exports = {
Auth
}
需要使用登录校验的接口
router.get('/latest', new Auth().m, async (ctx, next) => {
// .....
})
koa + session
当登录成功的时候,将用户的信息写入到 session 中。
/**
* 登录
* @param {Object} ctx koa2 ctx
* @param {string} userName 用户名
* @param {string} password 密码
*/
async function login(ctx, userName, password) {
// 获取用户信息
const userInfo = await getUserInfo(userName, doCrypto(password))
if (!userInfo) {
// 登录失败
return new ErrorModel(loginFailInfo)
}
// 登录成功
if (ctx.session.userInfo == null) {
ctx.session.userInfo = userInfo
}
return new SuccessModel()
}
需要做登录校验的时候,可为其封装一个中间件,用作登录检查。
/**
* API 登录验证
* @param {Object} ctx ctx
* @param {function} next next
*/
async function loginCheck(ctx, next) {
if (ctx.session && ctx.session.userInfo) {
// 已登录,则进行下一步
await next()
return
}
// 未登录
ctx.body = new ErrorModel(loginCheckFailInfo)
}
处理路由的时,可以使用中间件进行登录校验
router.post('/create', loginCheck, async (ctx, next) => {
const { content, image } = ctx.request.body
const { id: userId } = ctx.session.userInfo
ctx.body = await create({ userId, content, image })
})
提到了 session,那不妨也了解一下 cookie。二者的设置其实差别不大。
express + cookie
router.post('/login', function (req, res, next) {
var param = {
userName: req.body.userName,
userPwd: req.body.userPwd
}
User.findOne(param, function (err, doc) {
if (err) {
res.json({
status: '1',
msg: err.message
})
} else {
if (doc) {
res.cookie('userid', doc._id, {
path: '/',
maxAge: 1000 * 60 * 60
})
res.json({
status: 0,
msg: '',
result: {
userName: doc.userName
}
})
}
}
})
})
app.js(入口主文件)
要放在路由的前面使用
app.use(function (req, res, next) {
if (req.cookies.userName) {
next()
} else {
// 路由白名单
if (req.originalUrl === '/users/register' || req.originalUrl === '/users/login' || req.originalUrl === '/users/logout' || req.path === '/goods/list' || req.originalUrl === '/goods/getBannerImage') {
next()
} else {
res.json({
status: '100001',
msg: '当前未登录',
result: ''
})
}
}
})