Для внешнего интерфейса вход в систему означает отправку информации о пользователе, и интерфейсному интерфейсу не нужно беспокоиться об этом позже. Но я сделал проект для входа в SDK и обнаружил, что логика здесь не так проста. Ниже приведены некоторые из моих представлений о входе в систему, которыми я хочу поделиться с вами.
session & JWT
http
Протокол не имеет состояния, он не может различать и управлять запросами и ответами с состоянием. То есть, если пользователь выполняет аутентификацию пользователя с помощью учетной записи и пароля, пользователю необходимо снова выполнить аутентификацию пользователя в следующем запросе. потому что согласноhttp
протокола, сервер не знает, какой пользователь инициировал запрос. Чтобы идентифицировать текущего пользователя, сервер и клиент должны согласовать идентификатор для представления текущего пользователя.
session
Чтобы определить, какой пользователь сделал запрос, необходимо сохранить копию регистрационной информации пользователя на сервере.Эта регистрационная информация будет передана клиенту в ответе для хранения.При следующем запросе клиент будет содержать информацию для входа в систему для запроса сервера.Сервер может определить, какой пользователь инициировал запрос
Ниже приведена схема:существует
session
В схеме он будет переноситься при запросе к серверуsession_id
, сервер пропустит текущийsession_id
, чтобы запросить, действителен ли текущий сеанс базы данных, и если он действителен, последующие запросы могут идентифицировать текущего пользователя.
Если текущий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
Будут некоторые проблемы с обменомНапример, в приведенном выше сценарии пользователь сначала запрашивает услугу
Auth Server
, а затем сгенерироватьsession
. Когда пользователь снова запрашивает услугуfeedback Server
когда из-заsession
Если он не является общим, служба B не может получить статус входа в систему и должна снова войти в систему.
Недостатки сеанса
session
Для решения аутентификации есть некоторые недостатки:
- Поддержка нескольких кластеров: когда веб-сайт использует кластерное развертывание, он столкнется с тем, как это сделать между несколькими веб-серверами.
session
общая проблема. потому чтоsession
создается одной службой, сервер, обрабатывающий запрос, может не создаватьсяsession
сервер, то сервер не сможет получить такую информацию, как учетные данные для входа, ранее введенные в сеанс. - Низкая производительность: во время пикового трафика это будет нагрузкой на ресурсы, поскольку информация о пользователе для каждого запроса должна храниться в базе данных.
- Низкая масштабируемость: при расширении сервера
session store
Также требуется расширение. Это потребляет дополнительные ресурсы и добавляет сложности
JWT
существуетsession
В сервисе сервер должен поддерживатьsession
Объект, либо добавьте услугу, либо каждая служба будет получена из уровня хранения.session
Информация, давление ввода-вывода высокое, когда объем запроса большой.
в сравнении сsession
сервис, хранить информацию о пользователе на клиенте и следить за каждым запросомcookie
илиhttp
Когда головной канал отправляется на сервер, сервер может стать безстоящим, тем самым уменьшая нагрузку на сервер.
По сравнению с браузерами,Native App
настраиватьcookie
Это не так просто, поэтому сервер должен использовать другой метод аутентификации. После авторизации сервер сгенерируетtoken
значение, последующие запросы клиентские запросы будут нестиtoken
значение для проверки входа.
jwt
Он в основном состоит из трех частей: информация заголовка (header
), тело сообщения (payload
) и подпись (signature
)
информация заголовка указываетJWT
алгоритм подписи
header = {
alg: "HS256",
type: "JWT"
}
HS256
указывает на то, что он был использованHMAC-SHA256
для создания подписи
Тело сообщения содержитJWT
цель:
payload = {
"loggedInAs": "admin",
"iat": 1422779638
}
Неподписанные токены предоставляютсяbase64url
Кодированная информация заголовка и соращивание тела сообщения, через частную подписьkey
Рассчитано из:
key = 'your_key'
unsignedToken = encodeBase64(header) + "." + encodeBase64(payload)
signature = HAMC-SHA256(key, unsignedToken)
Наконец, он присоединяется к хвосту неподписанного токена.base64url
Закодированная подпись — это 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();
}
}
По сравнению с сеансовым методом jwt имеет следующие преимущества:
- Хорошая масштабируемость: в сценариях распределенного развертывания сеансы требуют совместного использования данных, а jwt — нет.
- Без сохранения состояния: нет необходимости хранить какое-либо состояние на сервере
JWT также имеет некоторые недостатки:
- Невозможно аннулировать: после выдачи он остается действительным до истечения срока его действия и не может быть аннулирован на полпути.
- Низкая производительность: в схеме сеанса sessionId, который должен содержать файл cookie, представляет собой очень короткую строку. Однако, поскольку jwt не имеет состояния, ему нужно нести некоторую необходимую информацию, и объем будет относительно большим.
- Безопасность: полезная нагрузка в jwt закодирована в base64 и не зашифрована, поэтому она не может хранить конфиденциальные данные.
- Продление: традиционная схема обновления файлов cookie поставляется вместе с фреймворком.Сессия действительна в течение 30 минут.Если в течение 30 минут происходит посещение, срок действия обновляется до 30 минут. Если вы хотите изменить действительное время jwt, вам нужно выпустить новый jwt. Одно решение состоит в том, чтобы обновлять jwt каждый раз, когда делается запрос, поэтому производительность слишком низкая; второе решение — установить время истечения срока действия для каждого jwt и обновлять время истечения срока действия jwt каждый раз, когда делается доступ. преимущество jwt без гражданства теряется.
Применимые сценарии сеанса и jwt
Подходит для сценариев, где применим jwt:
- Короткий срок действия
- хочу использовать только один раз
Например, при запросе службы A служба A выдаст браузеру JWT с коротким сроком действия, браузер может запросить службу B с текущим JWT, а служба B может проверить JWT, чтобы определить, имеет ли текущий пользователь право работать . Из-за того, что jwt не может отказаться от функций, единый вход и управление сеансами совершенно не подходят для jwt.
Единый вход (SSO)
sso
Обычно это касается доступа и входа в систему между различными приложениями компании. Например, если корпоративное приложение имеет много бизнес-подсистем, ему нужно войти только в одну систему, чтобы реализовать переход между разными подсистемами и избежать операции входа в систему.
Вот пример для иллюстрации:
подсистемаA
объединены вpassport
логин доменного имени иpassport
Установите файл cookie под доменным именем, затем добавьте токен к URL-адресу и перенаправьте на подсистему A.
Вернувшись в подсистему A, используйте токен, чтобы вернуться снова.passport
Проверка, если проверка создает сеанс системы А, возвращая необходимую информацию.
Когда система A запросит в следующий раз, текущая служба уже будет существовать.session
, больше не пойдуpassport
перейти к проверке разрешений
При доступе к системе B, поскольку системы B не существуетsession
, поэтому перенаправляет наpassport
доменное имя,passport
Под доменным именем уже есть файл cookie, поэтому нет необходимости входить в систему, напрямую добавлять токен к URL-адресу и перенаправлять на подсистему B. Последующий процесс такой же, как и для A.
Принцип реализации
Возьмите Tencent в качестве примера, Tencent владеет несколькими доменными именами, такими как:
cd.qq.com, tencent.com, jd.cm, music.qq.com
существуетcd.qq.com
а такжеmusic.qq.com
, мы можем установитьcookie
изdomian
дляqq.com
выполнитьcookie
的共享。
Ноcd.qq.com
,tencent.com
Доменные имена второго уровня несовместимы, так что все доменные имена могут иметь один и тот же домен.cookie
. Так что я надеюсь, что есть общая служба для выполнения этой службы входа в систему. Например, в Tencent есть такое доменное имя:passport.tencent.com
Носитель для выделенных служб входа в систему. В настоящее времяcd.qq.com
а такжеtencent.com
Вход и выход изsso
(passport.baidu.com
)реализовать
Реализация
успешно авторизовалсяSSO
будет генерироватьtoken
Перейти на исходную страницу, на этот разSSO
Состояние входа в систему уже есть, но у подсистемы все еще нет состояния входа. Подсистемы должны пройтиtoken
Установите состояние входа в текущую подсистему и передайте текущийtoken
проситьpassport
Сервис получает основную информацию о пользователе.
Следующие три основные частиpassport
: Служба входа в систему, имя доменаpassport.com
system
: подсистема, порт прослушивания3001
для системыA
, прослушивающий порт3002
для системыB
, доменные именаa.com
,b.com
паспортная служба
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
доменное имя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
Просто интерфейс прослушивания отличается, поэтому добавьте переменную в параметр запуска, чтобы получить порт запуска
служба проверки подлинности паспорта
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 для повторного входа в систему.
OAuth
OAuth
Протокол широко используется для входа в систему с авторизацией третьей стороны. С помощью входа в систему третьей стороны пользователи могут избежать проблемы повторного входа в систему.
кgithub
Авторизация в качестве примера для объясненияOAuth
Процесс авторизации:
- служба доступа
A
,СлужитьA
Нет входа, вы можете пройтиgithub
Войти в систему с - нажмите
github
для перехода на сервер аутентификации. Затем запросите авторизацию - После завершения авторизации он будет перенаправлен на путь сервиса А с параметрами
code
- Служить
A
пройти черезcode
запроситьgithub
Чтобы добраться доtoken
стоимость - пройти через
token
значение, а затем запроситьgithub
Сервер ресурсов получает нужные вам данные
Иди первымgithub-authподать заявку на один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
Служба получения информации о пользователе, обычно веб-сайт выполняет ряд операций, таких как регистрация, добавление сеанса и т. Д., В соответствии с полученной в настоящее время информацией о пользователе. В приведенном выше коде данные запроса пользователя просто возвращаются на внешний интерфейс.В следующем формате данные возвращаются на внешний интерфейс:Наконец, реализована авторизация стороннего входа.
Справочная документация
medium.com/@Сиддхартха…
живой поток кода Dev/post/Ah-PR AC…
medium.com/my planet-дерево...