1. 用户验证方式
1.1. cookie & session
流程是下面这样:
1. 用户向服务器发送用户名和密码。
2. 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
3. 服务器向用户返回一个 session_id,写入用户的 Cookie。
4. 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
5. 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
缺点:
1. 扩展性(scaling)不好,如果是服务器集群,就要求每台服务器都能够读取session,工程量大
2. 对于浏览器外的其他客户端(比如iOS、Android),必须手动的设置cookie和session
3. Cookie的大小限制是4KB,对于复杂的需求来说是不够的
4. Cookie是明文传递的,所以存在安全性的问题
解决方案:所有数据都保存在客户端,每次请求都发回服务器,JWT 就是这种方案的一个代表。
1.2. JWT
在目前的前后端分离的开发过程中,使用token来进行身份验证的是最多的情况,我们用JWT来生成token来验证。步骤是
1. 当用户第一次登陆时,用户名名密码都正常后,后端根据用户名等通过JWT生成一个token,在响应头中带给前端
2. 当用户访问其他资源时,在后端对携带过来的token进行验证,验证通过,才能访问
2. JWT实现token机制
2.1. token组成
JWT生成token分为三部分:Header,Payload,Signature
2.1.1. Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
alg:algorithm的缩写,即采用的加密算法,默认是 HMAC SHA256(HS256),采用同一个密钥进行
加密和解密,这种方式叫做对称加密。为了更好的安全性,一般采用RS256,非对称加密
typ:JWT,固定值,通常都写成JWT即可;
最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。
2.1.2. Payload
Payload是携带的数据,也是个 JSON 对象比如我们可以将用户的id和name放到payload中。JWT 规定了7个官方字段,供选用
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
2.1.3. Signature
Signature 部分是对前两部分的签名,防止数据篡改
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
2.1.4. Base64URL
前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。
2.1.5. 非对称加密
前面提到 HS256 是个对称加密算法,如果密钥暴露,就是件非常危险的事情。
这个时候我们可以使用非对称加密,RS256:
私钥(private key):用于发布令牌
公钥(public key):用于验证令牌
可以使用openssl来生成一对私钥和公钥:
Mac直接使用terminal终端即可
Windows默认的cmd终端是不能直接使用的,建议直接使用git bash终端
openssl
> genrsa -out private.key 1024
> rsa -in private.key -pubout -out public.key
生成的公钥 私钥文件如下
3. Node.js模拟用户验证
3.1. 创建工程结构,安装所需包
所需依赖如下
3.2. 用户注册
步骤:
1. 用户输入注册用户名和密码,后端获取到进行验证。
2. 首先验证用户名和密码是否有效,比如是否为空,现有的数据库是否已有相同的用户名
3. 数据有效后,对密码进行md5加密,放入数据库中
核心代码如下:
router:
const Router = require('koa-router');
const userRouter = new Router({prefix: '/users'});
const {verifyUsers,handlePassword} = require("../middleware/user.middleware.js")
const UserController = require('../controller/userController.js')
//中间件是一步步过去的,先是验证用户有效性,再对密码进行加密,最后创建用户。
userRouter.get('/',verifyUsers,handlePassword, UserController.create);
user.middleware.js
const errTypes = require("../constants/error-types.js");
const service = require("../service/userService.js")
const md5paassword = require("../utils/password-handle.js")
const verifyUsers = async (ctx, next) => {
// 1.获取用户名和密码
const { userName, password } = ctx.request.body;
//判断用户账号或者密码不为空
if (!userName || !password) {
const error = new Error(errTypes.NAME_OR_PASSWORD_IS_REQUIRED);
return ctx.app.emit("error", error,ctx);
}
// 3.判断这次注册的用户名是没有被注册过
const result = await service.getUserByName(userName);
if (result.length) {
const error = new Error(errTypes.USER_ALREADY_EXISTS);
return ctx.app.emit('error', error, ctx);
}
//如果有效,则进入下一个中间件
await next();
};
const handlePassword = async(ctx,next)=>{
//将密码加密后继续调用后面的中间键
const { password } = ctx.request.body;
ctx.request.body.password = md5paassword(password)
await next()
}
module.exports = { verifyUsers,handlePassword };
userController.js
const UserService =require('../service/UserService.js')
class UserController {
async create(ctx, next) {
//1.获取用户传递的参数
const user = ctx.request.body;
//2.查询参数
const result = await UserService.create(user);
//3.返回数据
ctx.body = "hello";
}
}
module.exports = new UserController()
userService.js
const connect = require("../app/dataBase.js");
class UserService {
async create(user) {
const { userName, password } = user;
const statement = `insert into user (name,password) values (?,?)`;
const result = await connect.execute(statement, [userName, password]);
return result[0];
}
async getUserByName(userName) {
const statement = `SELECT * FROM user WHERE name = ?;`;
const result = await connect.execute(statement, [userName]);
return result[0];
}
}
module.exports = new UserService();
现在我们来验证下:使用postman调用/users接口,将参数带入验证。
数据库有数据了,并且密码加密了。
如果我们再一次访问
3.3. token验证
token的验证机制上面提过了,现在就直接代码
auth.router.js
const Router = require('koa-router');
const authRouter = new Router();
const {
login,
success
} = require('../controller/auth.controller');
const {
verifyLogin,
verifyAuth
} = require('../middleware/auth.middleware');
//登陆时只验证用户名 密码是否正确,然后返回一个token回去
authRouter.post('/login', verifyLogin,login);
//访问其他资源时,就比对token
authRouter.get('/test', verifyAuth, success);
module.exports = authRouter;
auth.middleware.js
const jwt = require("jsonwebtoken");
const { PUBLIC_KEY } = require("../app/config");
const errTypes = require("../constants/error-types");
const service = require("../service/userService");
const md5password = require("../utils/password-handle")
const verifyLogin = async (ctx, next) => {
//1.获取用户名和密码
const { userName, password } = ctx.request.body;
//2.判断用户名和密码是否为空
if (!userName || !password) {
const error = new Error(errTypes.NAME_OR_PASSWORD_IS_REQUIRED);
return ctx.app.emit("error", error, ctx);
}
// 3.判断用户是否存在
const result = await service.getUserByName(userName);
const user = result[0]
if (!user) {
const error = new Error(errTypes.USER_DOES_NOT_EXISTS);
return ctx.app.emit("error", error, ctx);
}
// 4.判断密码是否和数据库中的密码是一致(加密)
if(md5password(password) !== user.password ){
const error = new Error(errTypes.PASSWORD_IS_INCORRENT);
return ctx.app.emit("error", error, ctx);
}
//将user信息放入ctx,一边后面的中间件处理
ctx.user = user
await next();
};
const verifyAuth =async(ctx,next)=>{
console.log('验证登陆的授权');
//获取token
const authorization =ctx.headers.authorization
if(!authorization){
const error =new Error(errTypes.UNAUTHORIZATION);
return ctx.app.emit('error',error,ctx)
}
//因为是postman发送过来的,因此会多出Bearer这个字符
const token = authorization.replace('Bearer ', '');
//验证token
try {
const result =jwt.verify(token,PUBLIC_KEY,{
algorithms:['RS256']
})
ctx.user =result
await next()
} catch (error) {
const err =new Error(errTypes.UNAUTHORIZATION)
return ctx.app.emit('error',err,ctx)
}
}
module.exports ={
verifyLogin,
verifyAuth
}
auth.controller.js
const jwt = require('jsonwebtoken');
const authService =require('../service/auth.servicece')
const { PRIVATE_KEY } = require('../app/config')
class authController {
async login(ctx, next) {
const {id,userName} = ctx.user
const token =jwt.sign({id,userName},PRIVATE_KEY,{
//24h后失效
expiresIn:60*60*24,
//非对称加密
algorithm:'RS256'
})
ctx.body = {id,userName,token};
}
async success(ctx,next){
ctx.body = "授权成功"
}
}
module.exports = new authController()
auth.servicece.js
const connect = require("../app/dataBase.js");
class authService {
async create(user) {
const { userName, password } = user;
const statement = `insert into user (name,password) values (?,?)`;
const result = await connect.execute(statement, [userName, password]);
return result[0];
}
async getUserByName(userName) {
const statement = `SELECT * FROM user WHERE name = ?;`;
const result = await connect.execute(statement, [userName]);
return result[0];
}
}
module.exports = new authService();
现在我们登陆下试试:
可以看到response里面是由token返回给我们。在postman里面设置一个全局变量,这样,在test接口我们直接用这个变量放到header里面
可以看到是授权成功的
我们在token里面更改一下,看看会发生什么事情。
此外还可以在代码里修改token的过期时间,postman里面进行验证。
最后贴上 github地址
参考文章