Реализация системы входа

внешний интерфейс

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

  1. Поддержка нескольких кластеров: когда веб-сайт использует кластерное развертывание, он столкнется с тем, как это сделать между несколькими веб-серверами.sessionобщая проблема. потому чтоsessionсоздается одной службой, сервер, обрабатывающий запрос, может не создаватьсяsessionсервер, то сервер не сможет получить такую ​​информацию, как учетные данные для входа, ранее введенные в сеанс.
  2. Низкая производительность: во время пикового трафика это будет нагрузкой на ресурсы, поскольку информация о пользователе для каждого запроса должна храниться в базе данных.
  3. Низкая масштабируемость: при расширении сервера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 имеет следующие преимущества:

  1. Хорошая масштабируемость: в сценариях распределенного развертывания сеансы требуют совместного использования данных, а jwt — нет.
  2. Без сохранения состояния: нет необходимости хранить какое-либо состояние на сервере

JWT также имеет некоторые недостатки:

  1. Невозможно аннулировать: после выдачи он остается действительным до истечения срока его действия и не может быть аннулирован на полпути.
  2. Низкая производительность: в схеме сеанса sessionId, который должен содержать файл cookie, представляет собой очень короткую строку. Однако, поскольку jwt не имеет состояния, ему нужно нести некоторую необходимую информацию, и объем будет относительно большим.
  3. Безопасность: полезная нагрузка в jwt закодирована в base64 и не зашифрована, поэтому она не может хранить конфиденциальные данные.
  4. Продление: традиционная схема обновления файлов 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В основном имеют следующие функции:

  1. Единая служба входа
  2. Получить информацию о пользователе
  3. проверить текущий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Процесс авторизации:

  1. служба доступаA,СлужитьAНет входа, вы можете пройтиgithubВойти в систему с
  2. нажмитеgithubдля перехода на сервер аутентификации. Затем запросите авторизацию
  3. После завершения авторизации он будет перенаправлен на путь сервиса А с параметрамиcode
  4. СлужитьAпройти черезcodeзапроситьgithubЧтобы добраться доtokenстоимость
  5. пройти через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-дерево...