使用node+typescript搭建mvc后台(五)--添加登录校验

1,121 阅读7分钟

添加登录校验

rsa加密

RSA算法是非对称加密算法,在1977年被罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的,故取名为RSA非对称加密算法,而今在计算机数据加密领域以及电子商业中广泛使用.

非对称加密

非对称加密算法是一种密钥的保密方法。 非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。 非对称加密算法实现机密信息交换的基本过程是:甲方生成一对密钥并将其中的一把作为公用密钥向其它方公开;得到该公用密钥的乙方使用该密钥对机密信息进行加密后再发送给甲方;甲方再用自己保存的另一把专用密钥对加密后的信息进行解密。

rsa加密使用

在这里rsa加密主要用途在前后端的密码传输上,有人说http请求前端是无意义的。毕竟js在前端总是透明的。那么做这块rsa加密是因为一个请求到达真正的后端。需要经过很多层的路径。中间也有可能使用到某些中间件。在这些中间件里也有可能被拦截的情况。使用rsa加密在前端进行加密后也算是多了一层保险。

md5加密

注册时等到加密的密码到达后端后。后端拿到的密码要再进行一次md5加密才能存入数据库。为什么选择md5加密方式?是因为md5是一种不可逆的加密方式。曾经就有发生过这样的一个事件。某个企业的数据库用户数据被工具。大量用户的其他网站。等系统被解。主要原因就是用户使用了同样的账号密码。这样就导致了一个网站的用户信息暴露。其他网站的同用户信息也造成了极大的危害。使用md5加密方式。即时数据库被获取。或者说内部员工可有权限看到数据库的内容。也无法知道用户真正的密码明文是什么。

RSA加密代码实现。

这里的rsa部分我们用node-rsa来实现。首先先放一个完整的代码块。

import fs from "fs";
import { join } from 'path'
import nodeRsa from 'node-rsa'
export class CryptHelper {
    public static generateKey(){
        const key = new nodeRsa({ b: 1024 }); // 生成新的1024位长度密钥
        key.setOptions({encryptionScheme: 'pkcs1'}); // 必须加上,加密方式问题。
        const publicDer = key.exportKey('pkcs8-public'); // 输出公钥
        const privateDer = key.exportKey('pkcs8-private'); // 输出秘钥
        const pubFile = join(__dirname,'./pub.key');
        const priFile = join(__dirname,'./pri.key');
        fs.writeFile(pubFile,publicDer,function (err) {
            if (err) {
                console.log('写入失败', err);
            } else {
                console.log('写入成功');
            }
        })
        fs.writeFile(priFile,privateDer,function (err) {
            if (err) {
                console.log('写入失败', err);
            } else {
                console.log('写入成功');
            }
        })
    }
    public static encryptByKey(plaintext: string) {
        const pubFile = join(__dirname,'./pub.key');
        const publicStr =  fs.readFileSync('./pub.key');
        const a_public_key = new nodeRsa(publicStr,'pkcs8-public');
        a_public_key.setOptions({encryptionScheme: 'pkcs1'}); //  设置加密方式问题。
        const encrypted = a_public_key.encrypt(plaintext, 'base64');
        return encrypted;
    }
    public static decryptByKey(cypher: string) {
        const priFile = join(__dirname,'./pri.key');
        const priavteStr =  fs.readFileSync( priFile).toString('utf8');
        const a_private_key = new nodeRsa(priavteStr,'pkcs8-private');// 导入私钥
        a_private_key.setOptions({encryptionScheme: 'pkcs1'}); //  设置加密方式问题。
        const encrypted = a_private_key.decrypt(cypher, 'utf8');
        return encrypted;
    }
}

/**
 * require.main 可以判断是否为应用主模块。所以导入的时候就不会运行这个。
 * 只有直接使用node来使用的时候才会生成秘钥公钥
 */
if (require.main === module) {
    CryptHelper.generateKey();
}


生成秘钥私钥

这里先编写一个generateKey的静态方法。用于生成加密所需要的私钥,秘钥。
这里需要注意的一点的是导出的秘钥和私钥必须是pkcs8的方式的。因为pkcs1的导出秘钥私钥在还原的时候会有一定的问题。(这块没有深入学习过。还望大佬指点)

最后要执行脚本的时候要区分是否为主模块。否者在引入本模块的时候。也会触发这个脚本。

编写脚本命令

那么在我们启动服务的时候要先生成私钥和秘钥。我们就需要编写一条脚本命令来生成。一样的写在package.json里即可。

"createKey":"ts-node ./src/utils/CryptHelper.ts"

加密密码

在启动之前使用npm run createKey来执行脚本。然后将获取到的公钥发送给前端来生成密码就好。也可以使用请求来返回公钥。前端可以使用**jsencrypt **来加密密码,如:

<script src="https://cdn.bootcss.com/jsencrypt/3.0.0-rc.1/jsencrypt.js"></script>
	<script type="text/javascript">
		var encrypt = new JSEncrypt();
		const publicStr = '-----BEGIN PUBLIC KEY-----\n'+
'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCpUAkw3Y4IjMA0HMn85Yq1KumU\n'+
'mGkXMR3MLnFm2+w6YknmikrYv7ZaEbNiad1ADv2Sb8WE+QRF5i7TyMHJ4gObR9kK\n'+
'O6741U/d8CAESm5m4bNayaha+KX5XtDh61r3xmA4HLbog5j91ezXUcTmh8rq9sEY\n'+
'LV1XkFfW/4bCw+TLbwIDAQAB\n'+
'-----END PUBLIC KEY-----';
		encrypt.setPublicKey(publicStr);
		 var encrypted = encrypt.encrypt('hello');
		 console.log(encrypted);
	</script>


或者也可以使用这里的工具类来生成密码,既CryptHelper.encryptByKey
这里首先读取公钥文件获取公钥,注意:获取的文件内容是buffer格式,需要转化为字符串。使用toString的方法。
然后使用nodeRsa新建一个nodeRsa的对象。设置他的加密方式a_public_key.setOptions({encryptionScheme: 'pkcs1'}); 导入获取到的公钥。 接着就是用对象的加密解密方法了。

密码解密

和密码加密使用同样的过程。只不过加密解密的方法不一样。

MD5代码实现。

import crypto from 'crypto';

export class MD5 {
    // 随机盐值
    public static getRandomSalt():string {
        return Math.random().toString().slice(2, 6);
    }
    // md5加密数据
    public static  cryptPwd(password:string, salt:string) {
        // 密码加盐
        let saltPassword = `${password}:${salt}`;
        // 加盐密码的md5值
        let md5 = crypto.createHash('md5');
        let result = md5.update(saltPassword).digest('hex');
        return result;
    }
}

md5的实现就比较简单。使用了crypto的库。主要这里有一个salt的使用。每个用户的salt不同。不同的密码会根据salt加密成的md5密文也不同。这里的salt生成后后面要保存到用户的数据库中。等到后面校验的时候使用。

添加导出工具里

上面的这两个工具类我在文件夹中新建了一个utils的ts文件。到时候所有使用到的工具都统一从此入口获取。统一管理更方便。

export {CryptHelper} from './CryptHelper';
export {MD5} from './MD5';

## 实现用户注册登录 ### 修改用户类 因为用户类添加了salt,和password的功能。所以也要在类上加上。swagger也一样。详情请看源码。
 @Column({
        default: null
    })
    public salt!: string;

    @Column({
        default: null
    })
    public password!: string;

修改controller

接下来我们把之前的indexController改为通用的控制类。实现两个函数。一个是注册。一个是登录。

import { Get, JsonController, QueryParam, Body, Post } from "routing-controllers";
import { env } from '../../env'
import AjaxResponse from "../common_class/AjaxResponse";
import {CryptHelper,MD5} from "../../utils"
import { User } from "../models/User";
import { UserService } from "../services/UserService";

@JsonController('/')
export default class UserController {
    constructor(private userService: UserService) {
    }

    @Get('login')
    public async login(@QueryParam('username') username: string, @QueryParam('password') password: string):Promise<AjaxResponse>  {
        console.log('password=',password);
        const user:User = new User();
        user.wx_name = username;
        const findUser:User|undefined = await this.userService.findOneByUser(user);
        if(!findUser){
            return new AjaxResponse(-1, '未找到用户');
        }
        try{
            const decPassword =  CryptHelper.decryptByKey(password);
            const encryptPassword = MD5.cryptPwd(decPassword, findUser.salt);
            if(encryptPassword !== findUser.password){
                return new AjaxResponse(-1, '密码错误');
            }
            return new AjaxResponse(1, '登录成功', findUser);
        }catch(e){
            console.error(e);
            return new AjaxResponse(1, '密码不正确');
        }
    }

    @Post('register')
    public register(@Body() user: User):Promise<AjaxResponse>{
        user.salt = MD5.getRandomSalt();
        const decPassword =  CryptHelper.decryptByKey(user.password);
        user.password = MD5.cryptPwd(decPassword, user.salt);
        return this.userService.create(user);
    }
}

注册

先来看看注册类。主要有以下几个步骤。

  1. 生成随机盐
  2. 解密用户注册时经过加密的密码
  3. 使用md5将密码加密
  4. 最后保存用户

登录

  1. 获取用户名密码
  2. 通过用户名找到用户判断
  3. 通过解析密码检验加密格式是否正确
  4. 通过md5加密后的密码匹配获取到的用户密码

修改swagger文件

最后要在swagger上添加login和register接口

·······
"/register": {
            "post": {
                "tags": [
                    "Common"
                ],
                "summary": "注册",
                "description": "用户注册",
                "operationId": "register",
                "produces": [
                    "application/json"
                ],
                "parameters": [
                    {
                        "in": "body",
                        "name": "body",
                        "description": "创建一个用户对象",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/User"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "ok",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "code": {
                                            "type": "number"
                                        },
                                        "message": {
                                            "type": "string"
                                        },
                                        "data": {
                                            "$ref": "#/definitions/User"
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/login": {
            "get": {
                "tags": [
                    "Common"
                ],
                "summary": "登录",
                "description": "使用用户名密码登录",
                "operationId": "login",
                "produces": [
                    "application/json"
                ],
                "parameters": [
                    {
                        "name": "username",
                        "in": "query",
                        "description": "用户名",
                        "required": true,
                        "type": "string"
                    },
                    {
                        "name": "password",
                        "in": "query",
                        "description": "密码",
                        "required": true,
                        "type": "string"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "ok",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "code": {
                                            "type": "number"
                                        },
                                        "message": {
                                            "type": "string"
                                        },
                                        "data": {
                                            "$ref": "#/definitions/User"
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
·······


同时要新建一个common的标签标示一些通用的方法

"tags": [
        {
            "name": "User",
            "description": "关于用户类的操作"
        },
        {
            "name": "Common",
            "description": "通用的一类操作"
        }
    ],

启动

然后我们就可以重启项目啦。进行测试

image.png

注册和登录时如果觉得写前端的加密太麻烦可以先用www.bejson.com/enc/rsa/在线的rsa加密先。

代码仓库