前端如何实现鉴权

488 阅读6分钟

当开发全栈项目时,鉴权功能是必不可少的一部分,那前端如何利用node实现鉴权呢?

  • Session/Cookie
  • Token
  • OAuth
  • SSO

下面主要针对前两种来做介绍。如有描述不对的地方,欢迎指正。

1. Session/Cookie

1.1 cookie

实现方式:

  • 客户端登录页完成首次登录后,调用服务端登录接口。
  • 服务端校验完成后,在响应头设置cookie内容。
  • 客户端接收到请求响应中服务端设置的cookie内容后,会存在浏览器本地。
  • 下次客户端再次请求时,请求头会自动携带浏览器本地的cookie给服务端。

服务端设置cookie字段:

// 设置cookie
res.setHeader('Set-Cookie', 'cookie1=abctest')

cookie主要缺点:

  • 浏览器cookie有长度限制,所存内容有限
  • 安全性不高,内容都是明文传输

1.2 session/cookie

session会话机制是一种服务器端机制,使用类似于哈希表的结构来保存信息。一般cookie会与session结合使用,cookie只保存sessionId,服务端通过 sessionId 来获取具体的值(value信息)。也就是说具体值(value)会保存在服务端,sessionId会随cookie返回并保存在客户端。

概览图:

session鉴权.png

实现思路:

  • 客户端登录页完成首次登录后,调用服务端登录接口。
  • 服务端接收请求后,创建 session 并保存 session, session可保存在内存中(不建议),也可保存在指定的缓存服务器中,比如redis(键值对服务器)。然后根据session 生成唯一标识符 sessionId, 并在响应头的cookie中设置这个唯一sessionId。也可以选择通过密钥对sessionId进行签名,避免sessionId被篡改。
  • 客户端收到请求响应后,请求头中的cookie信息会被缓存在浏览器本地,其中包括cookie中设置的sessionId等信息。
  • 客户端下次发起请求时,http的请求头会携带这个包含sessionId的cookie信息。
  • 服务端接收到请求后,解析请求头中携带的cookie,获取sessionId值,并根据sessionId去数据库或内存中查找对应的session值,然后判断值是否合法。

一般可以考虑使用redis(键值对服务器)作为缓存服务器,来保存session的值。

代码示例:

const Koa = require('koa')
const router = require('koa-router')()
const session = require('koa-session')
const redisStore = require('koa-redis')({
    port: 6379, // Redis port
    host: "127.0.0.1", // Redis host
    password: "123456",
});
const cors = require('koa2-cors')
const bodyParser = require('koa-bodyparser')
const static = require('koa-static')
const app = new Koa();
app.keys = ['secret']; // 密钥

app.use(cors({
    credentials: true
}))

app.use(static(__dirname + '/'));

app.use(bodyParser())

app.use(session({
    key: 'sessionId',
    store: redisStore
}, app));

app.use((ctx, next) => {
    if (ctx.url.indexOf('users') > -1) {
        // 如果是用户信息相关接口
        next()
    } else if (ctx.session.userinfo){
        // 如果已经有了用户信息
        next()
    } else {
        ctx.body = {
            message: "暂无登录信息"
        }
    }
})

// 登录
router.post('/users/login', async ctx => {
    const { body } = ctx.request
    // body中读取用户名和密码信息
    // 用户名和密码与数据库值做比对
    // ...
    // ...
    
    // 比对成功后设置session
    ctx.session.userinfo = body.username;

    ctx.body = {
        message: "账号登录成功"
    }
})

// 登出
router.post('/users/logout', async ctx => {
    // 删除session
    delete ctx.session.userinfo

    ctx.body = {
        message: "账号登出成功"
    }
})

// 获取用户信息
router.get('/users/getUser', async ctx => {
    if(ctx.session.userinfo){
        ctx.body = {
            message: "用户信息获取成功",
            userinfo: ctx.session.userinfo
        }
    }else{
        ctx.body = {
            message: "用户信息获取失败"
        }
    }
})

app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);

访问以上请求,能观察到请求cookie中携带的sessionId, sessionId.sig字段信息。

方案缺点

  • 客户端需要支持session, cookie,平台受限
  • session需要额外维护

2. token

使用token令牌验证应该是项目中比较常用的鉴权方式。利用jsonwebtoken插件生成JWT令牌。

概览图:

token鉴权.png

实现思路:

  • 首次访问时,客户端携带用户名等信息调用登录接口。
  • 服务端接收到请求后,请求体中取出用户信息,与数据库中信息做校验比对。
  • 比对成功后,服务端使用JWT插件,利用密钥生成JWT令牌。
  • 服务端将JWT令牌信息设置到响应头中。
  • 客户端获取响应后,响应头中取出JWT并存储,比如localStorage里。
  • 客户端再次请求时,在请求头中设置存储过的JWT令牌信息。
  • 服务端获取请求后,在请求头中取出JWT,检查JWT签名是否合法,利用密钥解密获取用户信息。
  • 服务端将用户信息返回。

代码示例:

前台页面登录完成后,需要将返回的token设置到请求头中,主要代码如下:

// Bearer是JWT的认证头部信息 
config.headers.common["Authorization"] = `Bearer ${token}`;

后端核心代码如下:

const koa = require('koa')
const router = require('koa-router')()
const jwt = require('jsonwebtoken')
const jwtAuth = require('koa-jwt')
const bodyParser = require('koa-bodyparser')
const secret = 'secret' // 鉴权的密钥

const app = new koa()
app.keys = ['secret']

app.use(bodyParser())

// 登录
router.post('/user/login', async ctx => {
    const { body: { username, password, role } } = ctx.request
    // body中读取用户名和密码信息
    // 用户名和密码与数据库值做比对
    // ...
    // ...
    
    // 比对成功后生成token令牌
    const userinfo = {
        username,
        password,
        role
    }
    const token = jwt.sign({
        data: userinfo,
        exp: Math.floor(Date.now()/1000) + 60 *  // 设置token有效期
    }, secret)

    // 返回响应
    ctx.body = {
        message: '登录成功',
        userinfo,
        token
    }
})

// 获取token
router.get('/user/token', jwtAuth({
        secret
    }), async ctx => {
        ctx.body = {
            message: '获取成功',
            userinfo: ctx.state.user.data  // ctx.state返回值见下面
        }
    }
)

app.use(router.routes())
app.use(router.allowedMethods())
app.listen(3000)
// ctx.state返回值
{
    "user": {
        "data": {
            "username": "admin",
            "password": "123456",
            "role": "12"
        },
        "exp": 1639998728,
        "iat": 1639995128
    }
}

示例token如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsInJvbGUiOiIxMiJ9LCJleHAiOjE2Mzk5OTkzMzIsImlhdCI6MTYzOTk5NTczMn0.60C30yGV-eAae6dHzU8zRrRfwtowfmwzkKjd0rndGaQ

3. JWT(JsonWebToken)

上面提到的token令牌是使用了JWT对内容编码生成的。下面简单介绍下:

3.1 token格式

Bearer token包含三部分:令牌头,payload,哈希,以"."分隔。

token示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsInJvbGUiOiIxMiJ9LCJleHAiOjE2Mzk5OTkzMzIsImlhdCI6MTYzOTk5NTczMn0.60C30yGV-eAae6dHzU8zRrRfwtowfmwzkKjd0rndGaQ

令牌头:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
payload: eyJkYXRhIjp7InVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsInJvbGUiOiIxMiJ9LCJleHAiOjE2Mzk5OTkzMzIsImlhdCI6MTYzOTk5NTczMn0
哈希:60C30yGV-eAae6dHzU8zRrRfwtowfmwzkKjd0rndGaQ

令牌头

使用base64编码,解密后格式:

// atob(string) 解码使用 base64 编码的字符串。
// btoa(string) 使用base64编码。
JSON.parse(window.atob('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'))

// 解密后数据
{
    alg: 'HS256',   // 使用HS256算法加密生成hash
    typ: 'JWT'  // 指定类型为JWT
}

payload

默认使用base64编码,解密后格式:

JSON.parse(window.atob('eyJkYXRhIjp7InVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsInJvbGUiOiIxMiJ9LCJleHAiOjE2Mzk5OTkzMzIsImlhdCI6MTYzOTk5NTczMn0'))

// 解密后数据
{
    data: {
        username: "admin",
        password: "123456",
        role: "12"
    },
    exp: 1639999332,
    iat: 1639995732
}

哈希

签名:使用HS256算法对令牌头,payload和密钥进行综合签名生成哈希。内容非明文,未知。

3.2 token内容

token签名

调用sign方法可以生成签名,代码如下:

// 生成签名
const jwt = require('jsonwebtoken')
const secret = 'lalalala' // 密钥

let token = jwt.sign({ 
    data: userinfo,
    exp: Math.floor(Date.now()/1000) + 60 *60 // 设置token的有效期
}, secret)

token解码

调用verify或者decode方法可以解密token,代码如下:

// 签名解码
const jwt = require('jsonwebtoken')
const secret = 'lalalala' // 密钥

let result = jwt.verify(token, secret, {
    secret: 'jwt_secret', // 指定jwt加密方式
})

token失效

token签名中可以设置过期时间字段,比如exp。token解密后获取过期时间,然后判断当前时间是否大于过期时间,如果大于说明token已经失效了,这时候走token失效逻辑。

以上内容来自网络课程总结。