• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

浅出 Node + React 的微服务项目9. 身份认证

武飞扬头像
嗨Sirius
帮助1

身份认证

身份认证

学新通

  • 在 microservice 中 User auth 是一个很难的问题
  • 有很多方法可以解决,没有一种方法是“绝对正确的”
  • 下面是一些解决方案

⬆ back to top

身份验证策略存在的问题

  • 根据需求,我们需要每次服务的操作都进行身份验证
  • 首先看是否登录再看是否能进行该服务
    学新通
  1. Option #1 单个服务依赖于 Auth 服务进行验证
  • 每次都请求一遍 Auth
    学新通

1.1 单个服务 通过 “网关 gateway” 形式依赖Auth服务

  • 但其实和 Option #1 差不多
    学新通
  1. 单个服务同样和 Auth 一样有「如何验证用户」的逻辑
  • 这样就符合微服务的架构
  • 因为 即使 Auth 挂了,我们也能进行 单个服务的 认证功能
    学新通

⬆ back to top

选择哪个 Option?

Option #1

  • 单个服务依赖 auth 服务
  • 对身份验证状态的更改会立即反映
  • Auth 服务宕机?整个应用程序将破坏

Option #2

  • 单个服务同样和 Auth 一样有「如何验证用户」的逻辑
  • Auth 服务宕机?NBCS!
  • 有的用户被封号了?可是我 5 分钟前刚把认证过后的 Key 给了他们…

⬆ back to top

解决 Option #2 存在的问题

  • 用户创建的流程:认证成功了,就发认证 JWT Cookie 等,以后所有请求都用它
  • 注意:其他服务都是「认证」功能,只需要按照代码给定的固定的认证方式认证即可,只有 Auth 是发认证

学新通

  • 场景:管理员想 ban 掉某一个用户

学新通

  • 但这个时候,虽然 Auth 里面 ban 了,其他 Service 并没有 ban
    学新通
  • 被 ban 的用户还是可以用 JWT/Cookie 对非 Auth 的服务进行认证,这样等于没被 ban,所以是个大问题

⬆ back to top

回顾 Cookies 和 JWT’s 区别

学新通

  • cookie

学新通

  • JWT
    学新通
Cookies JWT’s
传输机制 Authentication/Authorization 认证/授权 机制
在浏览器和服务器之间移动任何类型的数据 存储我们想要的任何数据
由浏览器自动管理 必须手动服务端上管理

⬆ back to top

在 微服务 中 Auth 认证的一些细节

  • 普通用户发起购买请求的时候,需要 Auth 认证
    学新通
  • Admin 用户有创建免费优惠券的请求时,需要 Auth 的用户权限认证
    学新通
  • Admin 要 封号 的时候,需要同步给 MongoDB学新通
  • Auth 服务发认证的时候,有一个 expiring 过期机制
    学新通
    学新通

于是我们之前设想的认证机制脱颖而出 -> JWT

  • 必须能告诉我们用户的详细信息
  • 必须能够处理授权信息
  • 必须有一个内置的、防篡改的方式来过期或 - 使自己失效
  • 必须在不同语言之间易于理解
  • 不需要任何后台数据存储

⬆ back to top

JWT 在 SSR 中遇到的问题

  • 普通 React APP 发送认证数据的时机
    学新通
  • SSR 中,我们需要在第一次 request 的时候 就加入 Auth 认证
    学新通
  • 为了解决 SSR 第一次请求必须要客户端带上 Auth 认证相关数据的问题
  • 我们使用 Cookie 存储 JWT 信息,因为 Cookie 是浏览器管理的,能够持续存储,且每次请求的时候浏览器都会主动带上 Cookie
    学新通

⬆ back to top

Cookie 和 加密

  • 下面是 signup 的工作流
    学新通

Auth 必须满足的条件:

  • 必须携带并告诉用户信息

  • 必须能够处理授权信息

  • 必须有一个内置的、防篡改的方式来过期或 - 使自己失效

  • 必须在不同语言之间易于理解

    • 当我们 encrypt 加密 cookie 中的数据时,跨语言处理 cookie 通常是一个问题
    • 不会对 Cookie 本身加密
    • JWT 是防篡改的
    • 但可以加密 Cookie 的 content 内容,如果有必要的话
  • 不需要任何后台数据存储

⬆ back to top

cookie-session 和 express-session 的区别

express-session服务器上的中间件存储会话数据; 它只在 cookie 本身中保存会话 ID,而不是会话数据。默认情况下,它使用内存存储并且不是为生产环境设计的。在生产中,您需要设置一个可扩展的会话存储;查看兼容的会话存储列表。
express-session中间件将会话数据存储在服务器上;它仅将会话标识(而非会话数据)保存在 cookie 中。从1.5.0版本开始, express-session不再依赖cookie-parser,直接通过req/res读取/写入;默认存储位置内存存储(服务器端),

相比之下,cookie-session中间件实现了 cookie 支持的存储:它将整个会话序列化到 cookie,而不仅仅是一个会话密钥。仅当会话数据相对较小且易于编码为原始值(而不是对象)时才使用它。尽管浏览器应该支持每个 cookie 至多 4096 字节,但为确保不超过限制,每个域的大小不要超过 4093 字节。此外,请注意 cookie 数据将对客户端可见,因此如果有任何理由使其安全或隐蔽,那么express-session可能是更好的选择。

  • 下面这段代码用于测试
    • cookie-session 的 Session 信息存在 浏览器的 cookie 中,服务器不会存储且获取不到
    • express-session 的 Session 信息存在 服务器的 req 缓存中,浏览器的 cookie 只有会话的 session connect id
// ./doc/cookie/cookie-session 
// ./doc/cookie/express-session
var express = require('express');
// var session = requile('cookie-session');
var session = require('express-session');
var app = express();// Use the session middleware 
app.use(session({ 
这里的name值得是cookie的name,默认cookie的name是:connect.sid
  //name: 'hhw',
  secret: 'keyboard cat', 
  cookie: ('name', 'value', { path: '/', httpOnly: true,secure: false, maxAge:  60000 }),  //重新保存:强制会话保存即使是未修改的。默认为true但是得写上
  resave: true, 
  //强制“未初始化”的会话保存到存储。 
  saveUninitialized: true,  
  
}))
// 只需要用express app的use方法将session挂载在‘/’路径即可,这样所有的路由都可以访问到session。//可以给要挂载的session传递不同的option参数,来控制session的不同特性 
app.get('/', function(req, res, next) {  
var sess = req.session//用这个属性获取session中保存的数据,而且返回的JSON数据
  if (sess.views) {
    sess.views  
    res.setHeader('Content-Type', 'text/html')
    res.write('<p>欢迎第 '   sess.views   '次访问       '   'expires in:'   (sess.cookie.maxAge / 1000)   's</p>')
    res.end();
  } else {
    sess.views = 1
    res.end('welcome to the session demo. refresh!')
  }
 console.log(sess.cookie)
});

app.listen(3001);
学新通

测试 express-session

  • 服务端中,能通过 req 缓存获取 session.cookie 中的 session 信息
    学新通
  • 客户端中,cookie 只保存了 session 的 connect id
    学新通

测试cookie-session

  • 服务端中,不能获取到 cookie
    学新通
  • 客户端,Session 信息存在 浏览器的 cookie 中
    学新通

添加 Cookie-Session

cookie-session
显然,我们在 SSR 中,并不想让 服务端保存 会话信息,特备是 Auth 信息,这是客户端才要保存的,所以要用 cookie-session

// index.ts
app.set('trust proxy', true);
app.use(json());
app.use(
  cookieSession({
    signed: false, // 默认登录 tag,服务端保存
    secure: true // 仅通过 https 发送
  })
);

⬆ back to top

生成 JWT

jsonwebtoken

// signup.ts
// Generate JWT
const userJwt = jwt.sign(
  {
    id: user.id,
    email: user.email
  },
  'asdf'
);

// Store it on session object
req.session = {
  jwt: userJwt
};

⬆ back to top

JWT Signing Keys

BASE64 Decode
JWT
学新通
学新通

⬆ back to top

使用 Kubernetes 安全地存储 secret

学新通
学新通

⬆ back to top

创建和访问 Secrets

  • 看情况加 -n [xxx namespace]
kubectl create secret generic jwt-secret --from-literal=JWT_KEY=asdf
kubectl get secrets
kubectl describe secret jwt-secret

⬆ back to top

访问 Pod 中的环境变量

if(!process.env.JWT_KEY) {
  throw new Error('JWT_KEY must be defined');
}

⬆ back to top

通用的 Response 属性

不同DB的属性加到Response是不一样的,如图,本来是 id,但MongoDB是_id,而且还有个_v
学新通
学新通

⬆ back to top

格式化 JSON 属性

const person = { name: 'alex' };
JSON.stringify(person)
// {"name": "alex"}

const personTwo = { 
  name: 'alex', 
  toJSON() { return 1; } 
};
JSON.stringify(personTwo)
// {1}
const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true
  },
  password: {
    type: String,
    required: true
  }
}, {
  toJSON: {
    transform(doc, ret) {
      ret.id = ret._id;
      delete ret._id;
      delete ret.password;
      delete ret.__v;
    }
  }
});
学新通

⬆ back to top

用户登录的工作流

学新通

// signin.ts
import express, { Request, Response } from 'express';
import { body, validationResult } from 'express-validator';

import { RequestValidationError } from '../errors/request-validation-error';

const router = express.Router();

router.post(
  '/api/users/signin',
  [
    body('email')
      .isEmail()
      .withMessage('Email must be valid'),
    body('password')
      .trim()
      .notEmpty()
      .withMessage('You must supply a password')
  ],
  (req: Request, res: Response) => {
    const errors = validationResult(req);

    if (!errors.isEmpty()) {
      throw new RequestValidationError(errors.array());
    }
  }
);

export { router as signinRouter };
学新通

⬆ back to top

通用的请求验证中间件

  • 在 Step 7中,我们进行了错误响应一致化,然后每次处理请求的时候,就 express-validator 进行 body 的校验
  • 然后 validationResult 捕获错误
  • 判断错误是否为空,是就抛出自定义的 Error
  • 中间件拆分原因:signup signin 都要进行以上 请求验证 的错误捕获操作,所以还不如直接把「错误捕获的功能」拆分成一个中间件

下面是原代码

// signup.ts
router.post(
  '/api/users/signup',
  [
    body('email')
      .isEmail()
      .withMessage('Email must be valid'),
    body('password')
      .trim()
      .isLength({ min: 4, max: 20 })
      .withMessage('Password must be between 4 and 20 characters')
  ],
  async (req: Request, res: Response) => {
  	// !!!
    const errors = validationResult(req);
	// !!!
    if (!errors.isEmpty()) {
      throw new RequestValidationError(errors.array());
    }

    const { email, password } = req.body;

    const existingUser = await User.findOne({ email });

    if (existingUser) {
      throw new BadRequestError('Email in use');
    }

    const user = User.build({ email, password });
    await user.save();

    res.status(201).send(user);
  }
);
学新通
// validate-request.ts
import { Request, Response, NextFunction } from 'express';
import { validationResult } from 'express-validator';
import { RequestValidationError } from '../errors/request-validation-error';

export const validateRequest = (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  const errors = validationResult(req);

  if (!errors.isEmpty()) {
    throw new RequestValidationError(errors.array());
  }

  next();
};
学新通
// signin.ts
import express, { Request, Response } from 'express';
import { body } from 'express-validator';

import { validateRequest } from '../middleware/validate-request';

const router = express.Router();

router.post(
  '/api/users/signin',
  [
    body('email')
      .isEmail()
      .withMessage('Email must be valid'),
    body('password')
      .trim()
      .notEmpty()
      .withMessage('You must supply a password')
  ],
  validateRequest,
  (req: Request, res: Response) => {

  }
);

export { router as signinRouter };
学新通

⬆ back to top

登录的代码逻辑

学新通

import express, { Request, Response } from 'express';
import { body } from 'express-validator';
import jwt from 'jsonwebtoken';

import { Password } from '../services/password';
import { User } from '../models/user';
import { validateRequest } from '../middlewares/validate-request';
import { BadRequestError } from '../errors/bad-request-error';

const router = express.Router();

router.post(
  '/api/users/signin',
  [
    body('email')
      .isEmail()
      .withMessage('Email must be valid'),
    body('password')
      .trim()
      .notEmpty()
      .withMessage('You must supply a password')
  ],
  validateRequest,
  async (req: Request, res: Response) => {
    const { email, password } = req.body;

    const existingUser = await User.findOne({ email });
    if (!existingUser) {
      throw new BadRequestError('Invalid credentials');
    }

    const passwordsMatch = await Password.compare(
      existingUser.password,
      password
    );
    if (!passwordsMatch) {
      throw new BadRequestError('Invalid Credentials');
    }

    // 生成 JWT
    const userJwt = jwt.sign(
      {
        id: existingUser.id,
        email: existingUser.email
      },
      process.env.JWT_KEY!
    );

    // Store it on session object
    req.session = {
      jwt: userJwt
    };

    res.status(200).send(existingUser);
  }
);

export { router as signinRouter };
学新通

⬆ back to top

处理当前用户

  • 为什么要进行当前用户的处理
    • 因为在每一次进行 ReactAPP 的时候,header 上面我们需要按照用户的登录状态,展现 signin signup 还是 signout,详见第11章
      学新通

⬆ back to top

返回当前用户

import express from 'express';
import jwt from 'jsonwebtoken';

const router = express.Router();

router.get('/api/users/currentuser', (req, res) => {
  if (!req.session?.jwt) {
    return res.send({ currentUser: null });
  }

  try {
    const payload = jwt.verify(
      req.session.jwt, 
      process.env.JWT_KEY!
    );
    res.send({ currentUser: payload });
  } catch (err) {
    res.send({ currentUser: null });
  }
});

export { router as currentUserRouter };
学新通

⬆ back to top

Signing Out

import express from 'express';

const router = express.Router();

router.post('/api/users/signout', (req, res) => {
  req.session = null;

  res.send({});
});

export { router as signoutRouter };

⬆ back to top

创建处理当前用户的 Middleware

  • 这个功能也可以复用
// current-user.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

export const currentUser = (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  if (!req.session?.jwt) {
    return next();
  }

  try {
    const payload = jwt.verify(req.session.jwt, process.env.JWT_KEY!);
    req.currentUser = payload;
  } catch (err) {}

  next();
};
学新通

⬆ back to top

Augmenting Type 扩充类型的定义

// ./middleware/current-user.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

interface UserPayload {
  id: string;
  email: string;
}

declare global {
  namespace Express {
    interface Request {
      currentUser?: UserPayload;
    }
  }
}

export const currentUser = (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  if (!req.session?.jwt) {
    return next();
  }

  try {
    const payload = jwt.verify(
      req.session.jwt,
      process.env.JWT_KEY! // 强制
    ) as UserPayload;
    req.currentUser = payload;
  } catch (err) {}

  next();
};
学新通
// ./routes/current-user
import express from 'express';
import jwt from 'jsonwebtoken';

import { currentUser } from '../middlewares/current-user';

const router = express.Router();

router.get('/api/users/currentuser', currentUser, (req, res) => {
  res.send({ currentUser: req.currentUser || null });
});

export { router as currentUserRouter };

⬆ back to top

路由访问权限

  • 每一个单独的服务,都需要在请求的时候就 「提取JWT用户信息」「未认证就报错」
  • 所以就有了这两个中间件
    学新通
  • 到目前我们的中间件 以及 作用如下

学新通

  • 新增未认证的错误类型
import { CustomError } from './custom-error';

export class NotAuthorizedError extends CustomError {
  statusCode = 401;

  constructor() {
    super('Not Authorized');

    Object.setPrototypeOf(this, NotAuthorizedError.prototype);
  }

  serializeErrors() {
    return [{ message: 'Not authorized' }];
  }
}
  • 抛出未认证错误的中间件
import { Request, Response, NextFunction } from 'express';
import { NotAuthorizedError } from '../errors/not-authorized-error';

export const requireAuth = (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  if (!req.currentUser) {
    throw new NotAuthorizedError();
  }

  next();
};
// ./routes/current-user.ts
import express from 'express';
import jwt from 'jsonwebtoken';

import { currentUser } from '../middlewares/current-user';
import { requireAuth } from '../middlewares/require-auth';

const router = express.Router();

router.get(
  '/api/users/currentuser', 
  currentUser, 
  requireAuth, 
  (req, res) => {
    res.send({ currentUser: req.currentUser || null });
  });

export { router as currentUserRouter };
学新通

⬆ back to top

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhiacecf
系列文章
更多 icon
同类精品
更多 icon
继续加载