一文看懂各主流登录方式:Session、JWT、单点登录以及 OAuth2

2025-08-20 19:34:09

对于前端来说,登录就是把用户信息提交上去,后续就不用前端去担心了。但是,当我真正完整地做过一个登陆 sdk 项目,就会发现这里边的逻辑不是那么简单。本文总结了目前各种主流系统登录方式,包括:session、jwt、单点登录以及 oauth。

session & JWTHTTP 协议是无状态的,它不能以状态来区分和管理请求和响应(查看HTTP状态码)。也就是说,如果用户通过账号和密码来进行用户认证后,在下次请求时,用户还需要在再次进行用户认证。因为根据 HTTP 协议,服务端并不知道是哪个用户发起的请求。为了识别当前的用户,服务端与客户端需要约定某个标识表示当前的用户。

通过 session 登录为了识别是哪个用户发出的请求,需要在服务端存储一份用户登录的信息,这份登录信息会在响应传递给客户端进行存储,当下次请求的时候客户端会携带登录信息请求服务端,服务端就能够区分请求是哪个用户发起的。

下面是 Session 登录的示意图:

Session 登录流程图在 session 登录方案中,请求服务端时会携带 session_id,服务端会通过当前的 session_id,去查询数据库当前 session 是否有效,如果有效后续请求就能够标识当前用户。

如果当前的 session 是无效的或者是不存在的,客户端需要重定向到登录页面,或者提示没有登录。下面是对应的代码:

const express = require('express');

const session = require('express-session')

const redis = require('redis')

const connect = require('connect-redis')

const bodyParser = require('body-parser')

const app = express();

app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: true }))

const RedisStore = connect(session);

const client = redis.createClient({

host: '127.0.0.1',

port: 6397

})

app.use(session({

store: new RedisStore({

client,

}),

secret: 'sec_id',

resave: false,

saveUninitialized: false,

cookie: {

secure: true,

httpOnly: true,

maxAge: 1000 * 60 * 10

}

}));

app.get('/', (req, res) => {

sec = req.session;

if (sec.user) {

res.json({

user: sec.user

})

} else {

res.redirect('/login')

}

});

app.post('/login', (req, res) => {

const {pwd, name } = req.body;

// 这里为了简便,就写简单点

if (pwd === name) {

req.session.user = req.body.name;

res.json({

message: 'success'

})

}

});当请求 / 接口的时候,会判断当前 session 是否存在。如果存在,就返回对应的信息;如果不存在,则会重定向到 /login 页面。这个页面登录成功以后,就会设置 session。

上面代码中只考虑了单个服务的场景,但是业务中往往是多个服务,服务域名不一样,由于 cookie 不能跨域,所以 session 的共享会存在一定问题:

共享 Session 问题例如在上面场景中,用户首先请求服务 Auth Server,然后生成 session。当用户再次请求服务 feedback Server 时,由于 session 不共享,就导致服务 B 拿不到登陆态,就需要重新登录。

session 的缺点session 用于解决鉴权,存在一些缺点:

多集群支持:当网站采用集群部署的时候,会遇到多台 web 服务器之间如何做 session 共享的问题。因为 session 是由单个服务创建,处理请求的服务器可能不是创建 session 的服务器,那么该服务器就无法拿到之前放入到 session 中的登录凭证之类的信息性能差:当流量高峰期时,由于每个请求的用户信息都需要存储在数据库中,对资源会是一种负担低扩展性:当扩容服务端的时候,session store 也需要扩容。这会占用额外的资源和增加复杂性JWT 登录方案在 session 服务中,服务器需要维护用户的 session 对象,要么前置一个服务,要么每个服务都从存储层中获取 session 信息,请求量大的时候 IO 压力大。相比于 session 服务,把用户信息存放在客户端,每次请求的时候随 cookie 或 HTTP 头部发送到服务器上,就可以让服务器变成无状态的存在,从而减轻服务器的压力。

JWT 登录流程图相比于浏览器,Native App 设置 cookie 没有那么容易,所以服务端需要采用另外一种认证方式。在登录后,服务端会根据登录信息生成一个 token 值,后续的请求客户端请求会携带 token 值进行登录校验。

jwt 主要由三部分构成:头部信息 (header)、消息体 (payload) 和签名 (signature) 头信息指定了 JWT 的签名算法:

header = {

alg: "HS256",

type: "JWT"

}HS256 表示使用了 HMAC-SHA256 来生成签名,消息体包含了 JWT 的意图:

payload = {

"loggedInAs": "admin",

"iat": 1422779638

}未签名的令牌由 base64 url 编码的头信息和消息体拼接而成,签名则通过私有的 key 计算而成:

key = 'your_key'

unsignedToken = encodeBase64(header) + "." + encodeBase64(payload)

signature = HAMC-SHA256(key, unsignedToken)最后,在未签名的令牌尾部拼接上 base64 url 编码的签名就是JWT了:

token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)具体实现

首先创建 app.js,用于获取请求参数,还有监听端口等等:

// app.js

require('dotenv').config();

const express = require('express');

const bodyParser = require('body-parser')

const cookieParser = require('cookie-parser');

const router = require('./router');

const app = express();

app.use(bodyParser.json())

app.use(cookieParser);

app.use(bodyParser.urlencoded({ extended: true }))

router(app);

app.listen(3001, () => {

console.log('server start')

});dotenv 主要用于配置环境变量,创建 .env 文件,下面是本示例的配置:

ACCESS_TOKEN_SECRET = swsh23hjddnns

ACCESS_TOKEN_LIFE = 1200000然后注册 login 接口,这个接口提交用户信息到 server,后端会用这些信息生成对应的 token,可以直接返回给客户端或者设置 cookie:

// user.js

const jwt = require('jsonwebtoken')

function login(req, res) {

const username = req.body.username;

const payload = {

username,

}

const accessToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, {

algorithm: "HS256",

expiresIn: process.env.ACCESS_TOKEN_LIFE

})

res.cookie('jwt', accessToken, {

secure: true,

httpOnly: true,

})

res.send();

}当登录成功以后直接设置客户端的 cookie。

下次请求的时候,服务端直接获取用户的 jwt cookie,判断当前 token 是否是有效的:

// middleware.js

const jwt = require('jsonwebtoken');

exports.verify = function(req, res, next) {

const accessToken = req.cookies.jwt;

try {

jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET);

next();

} catch (error) {

console.log(error);

return res.status(401).send();

}

}相对于 session 的方式,jwt 具有以下优势:

扩展性好:在分布式部署场景下,session 需要数据共享,而 jwt 不需要无状态:不需要在服务端存储任何状态jwt 也存在一些缺点:

无法废弃:在签发后,在到期之前会始终有效,无法中途废弃。性能差: session 方案中,cookie 需要携带的 sessionId 是一个很短的字符串。但是由于 jwt 是无状态的,需要携带一些必要的信息,体积会比较大。安全性:jwt 中的 payload 是 base64 编码的,没有加密,因此不能存储敏感数据。续签:传统的 cookie 续签方案都是框架自带的,session 有效期 30 分钟,30 分钟内如果有访问,有效期被刷新至 30 分钟。如果要改变 jwt 的有效时间,就需要签发新的 jwt。一种方案是每次请求都更新 jwt,这样性能太差了;第二种方案为每个 jwt 设置过期时间,每次访问刷新 jwt 的过期时间,就失去了 jwt 无状态的优势了。session 和 jwt 的适用场景适合适用 jwt 的场景:

有效期短只希望被使用一次例如,在请求服务 A 的时候,服务 A 会颁发一个很短过期时间的 JWT 给浏览器,浏览器可以当前的 jwt 去请求服务 B,服务 B 则可以通过校验 JWT 来判断当前用户是否有权操作。由于 jwt 具有无法废弃的特性,单点登录和会话管理非常不适合用 jwt。

单点登录(SSO)sso 通常处理的是一个公司的不同应用间的访问登录问题。

如企业应用有很多业务子系统,只需要登录一个系统,就可以实现不同子系统间的跳转,而避免了登录操作。

这里举个例子进行说明:子系统 A 统一到 passport 域名登录,并且在 passport 域名下种上 cookie,然后把 token 加入到 url 中,重定向到子系统 A 回到子系统 A 后,使用 token 再次去 passport 验证,如果验证通过返回必要的信息生成系统 A 的 session 当系统 A 下次请求的时候会当前服务已有 session,不会再去 passport 去权限校验 当访问系统 B 的时候,由于系统 B 不存在 session,所以会重定向到 passport 域名,passport 域名下面已经有 cookie 了,所以不需要登录,直接把 token 加入到 url 中,重定向到子系统 B,后续流程和 A 一样

实现原理以腾讯为例,腾讯旗下有多个域名,例如: new.qq.com、tencent.com、music.qq.com 等,在 new.qq.com 和 music.qq.com 等以 qq.com 为主域名的二级域名,我们可以设置 cookie 的域名为 qq.com,来实现 cookie 共享。但是对于 tencent.com 下的二级域名,就无法共享一个 cookie 了。所以,我们希望有一个通用的服务去承载这个登录服务。

例如,在腾讯有这样一个域名: passport.tencent.com 用于专门登录服务的承载。这个时候 new.qq.com 和 xxx.tencent.com 的登录登出都由 passport.tencent.com 这台单点登录服务器来实现。

具体实现成功登录 SSO 会生成 token 跳转到源页面,此时 SSO 已经有登录状态,但是子系统仍然没有登录态。子系统需要通过 token 设置当前子系统的登录态,并通过当前的 token 请求 passport 服务获取用户的基本信息。

下面主要讲三个部分 passport:登录服务,域名为 passport.com;system 子系统,监听端口 3001 为系统 A,监听端口 3002 为系统 B,域名分别为 a.com、b.com

passport 服务

passport 主要有以下几个功能:

统一登录服务获取用户信息校验当前的 token 是否是有效的首先实现登录页面的一些逻辑:

// passport.js

import express from 'express';

import session from 'express-session';

import bodyParser from 'body-parser';

import cookieParser from 'cookie-parser';

import connect from 'connect-redis';

import redis from '../redis';

const app = express();

app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: true }));

app.use(cookieParser());

app.set('view engine', 'ejs');

app.set('views', `${__dirname}/views`);

const RedisStore = connect(session);

app.use(

session({

store: new RedisStore({

client: redis,

}),

secret: 'token',

resave: false,

saveUninitialized: false,

cookie: {

secure: true,

httpOnly: true,

maxAge: 1000 * 60 * 10,

},

})

);

app.get('/', (req, res) => {

const { token } = req.cookies;

if (token) {

const { from } = req.query;

const has_access = await redis.get(token);

if (has_access && from) {

return res.redirect(`https://${from}?token=${token}`);

}

// 如果不存在便引导至登录页重新登录

return res.render('index', {

query: req.query,

});

}

return res.render('index', {

query: req.query,

});

});

app.port('/login', (req, res) => {

const { name, pwd, from } = req.body;

if (name === pwd) {

const token = `${new Date().getTime()}_${ name}`;

redis.set(token, name);

res.cookie('token', token);

if (from) {

return res.redirect(`https://${from}?token=${token}`);

}

} else {

console.log('登录失败');

}

});/ 接口首先判断 passport 是否已经有登录成功的 token,如果存在就在去存储中查找当前 token 是否是有效的。如果有效并且参数中携带 from 参数,那么就跳转到原页面并且把生成的 token 值带回到原页面。

下面是 passport 页面的样式:

Passport 登录页效果登录接口需要做的就是登录成功后设置 passport 域名的 token,然后重定向到之前的页面。

子系统实现

import express from 'express';

import axios from 'axios';

import session from 'express-session';

import bodyParser from 'body-parser';

import connect from 'connect-redis';

import cookieParser from 'cookie-parser';

import redisClient from "../redis";

import { argv } from 'yargs';

const app = express();

const RedisStore = connect(session);

app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: true }));

app.use(cookieParser('system'));

app.use(session({

store: new RedisStore({

client: redisClient,

}),

secret: 'system',

resave: false,

name: 'system_id',

saveUninitialized: false,

cookie: {

httpOnly: true,

maxAge: 1000 * 60 * 10

}

}));

app.get('/', async (req, res) => {

const { token } = req.query;

const { host } = req.headers;

// 如果本站已经存在凭证,便不需要去passport鉴权

if (req.session.user) {

return res.send('user success')

}

// 如果没有本站信息,有没有token,便去passport登录鉴权

if (!token) {

return res.redirect(`http://passport.com?from=${host}`)

}

const {data} = await axios.post('http://127.0.0.1:3000/check',{

token,

})

// 验证成功

if (data?.code === 0) {

const user = data?.user;

req.session.user = user;

} else {

// 验证失败

return res.redirect(`http://passport.com?from=${host}`)

}

return res.send('page has token')

})

app.listen(argv.port, () => {

console.log(argv.port);

});首先,判断当前子系统是否已经登录了,如果当前系统 session 已经存在,就返回 user success。如果没有登录并且 url 上携带 token 参数,就需要跳转到 passport.com 登录。

如果 token 存在,并且当前子系统没有登录,就需要使用当前页面的 token 去请求 passport 服务,判断这个 token 是否有效的,如果有效就返回相应的信息,并且设置 session。

这里系统 A 和系统 B 只是监听的接口不同,所以在启动参数中添加变量获取启动端口。

passport 鉴权服务

app.get('/check', (req, res) => {

const { token } = req.query;

if (!token) {

return res.json({

code: 1

})

}

const user = await redis.getAsync(token);

if (user) {

return res.json({

code: 0,

user,

})

} else {

return res.redirect('passport.com')

}

});check 接口就是判断请求服务的 token 是否是有效的,如果有效就返回对应的用户信息,如果无效就重定向到 passport.com 重新登录。

OAuthOAuth 协议被广泛应用于第三方授权登录中,借助第三方登录可以让用户规避再次登录的问题。

以 github 授权为例,讲解 OAuth 的授权过程:

访问服务 A,服务 A 没有登录,可以通过 github 第三方登录点击 github,跳转到认证服务器。然后询问是否授权授权完成后,会重定向到服务 A 的一个路径,并且携带参数 code服务 A 通过 code 去请求 github,获取到 token 值通过 token 值,再去请求 github 资源服务器获取到你想要的的数据首先去 github-auth 申请一个 auth 应用,如下图所示:

Github Auth 应用授权执行后会得到对应的 client_id 和 client_secret。

下面是具体的授权代码:

import { AuthorizationCode } from 'simple-oauth2';

const config = {

client: {

id: 'client_id',

secret: 'client_secret'

},

auth: {

tokenHost: 'https://github.com',

tokenPath: '/login/oauth/access_token',

authorizePath: '/login/oauth/authorize'

}

}

const client = new AuthorizationCode(config);

const authorizationUri = client.authorizeURL({

redirect_uri: 'http://localhost:3000/callback',

scope: 'notifications',

state: '3(#0/!~'

});

app.set('view engine', 'ejs');

app.set('views', `${__dirname}/views`);

app.get('/auth', (_, res) => {

res.redirect(authorizationUri)

});上面使用了 simple-oauth2 用于 OAuth2 的讲解,当访问 localhost:3000/auth 的时候,服务会自动跳转到 github 的认证地址下面是具体的地址:

https://github.com/login/oauth/authorize?response_type=code&client_id=86f4138f17d0c3033ca4&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=notifications&state=3(%230%2F!~当点击授权后会重定向到 localhost:3000/callback,并且 url 上携带参数 code。下面是服务端的处理函数:

async function getUserInfo(token) {

const res = await axios({

method: 'GET',

url: 'https://api.github.com/user',

headers: {

Authorization: `token ${token}`

}

})

return res.data;

}

app.get('/callback', async (req, res) => {

const { code } = req.query;

console.log(code);

// 获取token

const options = {

code,

}

try {

const access = await client.getToken(options);

const resp = await getUserInfo(access.token.access_token);

return res.status(200).json({

token: access.token,

user: resp,

});

} catch (error) {

}

});根据 url 上参数 code 获取到 token,然后根据这个 token 去请求 github api 服务,获取到用户信息,通常网站会根据当前获取到的用户信息完成注册、加 session 等一系列操作。上面代码中,把用户请求数据简单返回给返回给前端,下面是最后返回给前端的数据格式:

OAuth 响应数据最后就实现了第三方的登录授权。