For the front end, the login is to submit the user information, then the front end does not have to worry about the rest. However, I have done a login SDK project and found that the logic here is not so simple. Here are some of my understanding of landing to share with you
session & JWT
The HTTP protocol is stateless and cannot distinguish and manage requests and responses in terms of state. In other words, if a user is authenticated using the account and password, the user needs to be authenticated again on the next request. Because, according to the HTTP protocol, the server does not know which user made the request. To identify the current user, the server and client need to agree on an identity to represent the current user
session
Which is in order to identify the user’s request, need on the server information of a user login, the login information is stored in response to the client, the next time a request when the client will carry the login information request to the server, the server will be able to distinguish between the request which is sponsored by the user Below is the schematic diagram: 在session
In the scenario, the requesting server will carrysession_id
, the server will pass the currentsession_id
If the current session is valid, subsequent requests can identify the current user.
If the current session is invalid or does not exist, the client needs to redirect to the login page or prompt that the session is not logged in.
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;
// Let's write it simple for simplicity
if (pwd === name) {
req.session.user = req.body.name;
res.json({
message: 'success'})}})Copy the code
When the request/interface is made, it determines whether the current session exists. If so, the corresponding information is returned; If it does not exist, it is redirected to the /login page. Once this page is logged in successfully, the session will be set
The above code only considers the scenario of a single service, but there are often multiple services in the business with different service domain namescookie
You can’t cross domains, sosession
There are problems with sharingFor example, in the above scenario, the user first requests the serviceAuth Server
And then generatesession
. When the user requests the service againfeedback Server
When, as a result ofsession
If not, service B cannot get the login status and needs to log in again.
The disadvantage of the session
Session is used to solve authentication problems. It has the following disadvantages:
- Multi-cluster support: When a website is deployed in a cluster, what can I do if multiple Web servers are deployed
session
Sharing problems. becausesession
Is created by a single service, and the server that handles the request may not be createdsession
The server will not be able to retrieve information such as login credentials that were previously put into the session - Poor performance: During peak traffic periods, it is a burden on resources because user information for each request needs to be stored in the database
- Low scalability: When expanding the server,
session store
It also needs to be expanded. This takes up additional resources and adds complexity
JWT
In the session service, the server needs to maintain the user’s session object. Either there is a service in front of the server, or each service obtains session information from the storage tier. When the number of requests is large, I/O pressure is heavy.
Compared to thesession
Service, which stores user information on the client every time it is requestedcookie
orhttp
The header channel is sent to the server, making the server stateless and thus relieving the strain on the server.
Compared to the browser,Native App
Set up thecookie
It’s not that easy, so the server needs to adopt a different authentication mode. After login, the server generates one based on the login informationtoken
Value that will be carried in subsequent requests by the clienttoken
Value for login verification.
The JWT consists of three parts: header, payload, and signature. The header specifies the signature algorithm of the JWT
header = {
alg: "HS256".type: "JWT"
}
Copy the code
HS256 indicates that hMAC-SHA256 is used to generate the intent that the body of the signature message contains JWT:
payload = {
"loggedInAs": "admin"."iat": 1422779638
}
Copy the code
The unsigned token is composed of a base64URL-encoded header and a message body, and the signature is computed using a private key:
key = 'your_key'
unsignedToken = encodeBase64(header) + "." + encodeBase64(payload)
signature = HAMC-SHA256(key, unsignedToken)
Copy the code
Finally, the base64URL-encoded signature concatenated to the end of the unsigned token is JWT:
token = encodeBase64(header) + '. ' + encodeBase64(payload) + '. ' + encodeBase64(signature)
Copy the code
The specific implementation
First create app.js to get the request parameters, the listening port, and so on
// 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')})Copy the code
Dotenv is used to configure environment variables and create. Env files.
ACCESS_TOKEN_SECRET=swsh23hjddnns
ACCESS_TOKEN_LIFE=1200000
Copy the code
Then the login interface is registered. This interface submits the user information to the server. The back-end will use the information to generate the corresponding token, which can be directly returned to the client or set cookies
// 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();
}
Copy the code
After a successful login, you can directly set the client’s cookie
In the next request, the server directly obtains the user’s JWT cookie and determines whether the current token is valid:
//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(); }}Copy the code
Compared to session, JWT has the following advantages:
- Good scalability: In distributed deployment scenarios, session requires data sharing, but JWT does not
- Stateless: There is no need to store any state on the server
JWT also has some disadvantages:
- Indelible: After it is issued, it remains valid until its expiration and cannot be revoked.
- Poor performance: In session schemes, the sessionId that cookies need to carry is a very short string. However, since JWT is stateless and needs to carry some necessary information, its volume will be relatively large.
- Security: Payload in JWT is base64 encoded and not encrypted, so sensitive data cannot be stored
- Renewal: The traditional cookie renewal scheme is built-in to the framework. The session validity period is 30 minutes. If there is access within 30 minutes, the validity period is refreshed to 30 minutes. If you want to change the duration of the JWT, you need to issue a new JWT. One option is to update JWT on every request, which is poor performance; The second option sets an expiration time for each JWT and refreshes the JWT expiration time for each access, thus losing the stateless benefits of JWT.
Application scenarios of session and JWT
Suitable for scenarios where JWT applies:
- The validity of short
- You only want to be used once
For example, when requesting service A, service A will issue A JWT with A short expiration time to the browser. The browser can request service B with the current JWT, and service B can verify the JWT to determine whether the current user has the right to operate. Single sign-on and session management are very unsuitable for JWT because of its inescapable nature.
Single Sign-on (SSO)
Sso typically deals with access and logins between different applications within a company. If an enterprise application has multiple service subsystems, you can switch between subsystems by logging in to one system without logging in to the system. Here’s an example: Subsystem A unified login to the Passport domain name, and planted A cookie under the Passport domain name, and then added the token to the URL, redirected to subsystem A, back to Subsystem A, and used the token to authenticate the Passport again. If the authentication returns the necessary information to generate the session of system A when the next request is made by system A, the current service already has A session, so it will not go to passport to verify the permission. When accessing system B, because system B does not have A session, it will redirect to the passport domain name, There are cookies under the passport domain name, so there is no need to log in. The token is directly added to the URL and redirected to subsystem B. The subsequent process is the same as that of SUBSYSTEM A
Realize the principle of
Take Tencent as an example. Tencent has a number of domain names, such as: In cd.qq.com and music.qq.com, we can set the cookie domian for qq.com to achieve cookie sharing. However, such as cd.QQ.com, tencent.com secondary domain names are inconsistent, so that all domain names can share a cookie. So you want a generic service to host this login service. For example, Tencent has such a domain name: passport.tencent.com for hosting specialized login services. At this time, cd.qq.com and tencent.com login and logout are implemented by SSO (passport.baidu.com)
The specific implementation
After SSO is successfully logged in, a token is generated and the login page is displayed. In this case, SSO is logged in, but the subsystem is not logged in. The subsystem needs to use the token to set the login state of the current subsystem and use the token to request the Passport service to obtain basic user information. The name of the passport is passport.com. The name of the passport is 3001. The name of the passport is 3002
Passport services
The passport has the following functions:
- Unified Login Service
- Obtaining user Information
- Verify current
token
Is it valid
First implement some logic for the login page:
// 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://The ${from}? token=${token}`);
}
// If not, boot to the login page and log in again
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 = `The ${new Date().getTime()}_${ name}`;
redis.set(token, name);
res.cookie('token', token);
if (from) {
return res.redirect(`https://The ${from}? token=${token}`); }}else {
console.log('Login failed'); }})Copy the code
The/interface first checks whether the passport already has a login token. If so, it checks whether the current token is valid in the storage. If it is valid and carries the from parameter, it jumps to the original page and brings the generated token value back to the original page.
The following ispassport
Page style:The login interface needs to be set after successful loginpassport
The domain nametoken
, and redirect to the previous page
Subsystem implementation
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;
// If this site already has credentials, you don't need to go to passport
if (req.session.user) {
return res.send('user success')}// If you don't have the login information and have the token, you can go to the Passport for login authentication
if(! token) {return res.redirect(`http://passport.com?from=${host}`)}const {data} = await axios.post('http://127.0.0.1:3000/check',{
token,
})
// Validation succeeded
if(data? .code ===0) {
constuser = data? .user; req.session.user = user; }else {
// Validation failed
return res.redirect(`http://passport.com?from=${host}`)}return res.send('page has token')
})
app.listen(argv.port, () = > {
console.log(argv.port);
})
Copy the code
If the current system session already exists, return user Success. If you are not logged in and the URL carries the token parameter, you need to jump to passport.com login.
If the token exists and the current subsystem is not logged in, you need to use the token on the current page to request the Passport service to determine whether the token is valid. If it is valid, you will return the corresponding information and set the session.
In this case, system A and system B are only listening on different interfaces, so add A variable to the boot parameter to get the boot port
Passport authentication service
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')}})Copy the code
The check interface determines whether the token is valid. If the token is valid, the user information will be returned. If not, the user will be redirected to passport.com to log in again
OAuth
OAuth protocol is widely used in third-party login, which allows users to avoid the problem of login again.
Take Github authorization as an example to explain the OAuth authorization process:
- Access the service
A
, the serviceA
No login, you can go throughgithub
Third party login - Click on the
github
To switch to the authentication server. Then ask for authorization - Once authorization is complete, it is redirected to A path for service A with parameters
code
- service
A
throughcode
To requestgithub
To obtain thetoken
value - through
token
Value, and then requestgithub
The resource server gets the data you want
First of all togithub-authTo apply for aauth
Applications such as the following:
The corresponding client_id and client_secret are obtained. The following is the specific authorization code (not to start the service) :
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)
})
Copy the code
When localhost:3000/auth is accessed, the service is automatically redirected to github where the authentication address is located
https://github.com/login/oauth/authorize?response_type=code&client_id=86f4138f17d0c3033ca4&redirect_uri=http%3A%2F%2Floc alhost%3A3000%2Fcallback&scope=notifications&state=3(%230%2F! ~Copy the code
When authorization is clicked, it is redirected to localhost:3000/callback with the parameter code on the URL. The following are the handlers on the server side
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);
/ / access 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) {
}
})
Copy the code
According to theurl
On the parameterscode
Access to thetoken
And then based on thistoken
To requestgithub api
Service to obtain user information, usually the website will complete a series of operations such as registration, adding session and so on according to the currently obtained user information. In the above code, the user request data is simply returned to the front end. Here is the format of the data returned to the front end:Finally, the third party login authorization is implemented
Reference documentation
medium.com/@siddhartha… Livecodestream. Dev/post/a – prac… Medium.com/myplanet-mu…