Для внешнего интерфейса вход в систему означает отправку информации о пользователе, и интерфейсному интерфейсу не нужно беспокоиться об этом позже. Но я сделал проект для входа в 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-дерево...