安全概述
前面章节讲解的接口是裸露的、不安全的!使用post、get模拟可以轻松对api进行请求,最简单的攻击就可以瞬间完成近万会员的注册!
所以在进行api接口通讯的同时我们应该进行数据的验证工作!
加密原理及流程
1、从服务器端获取一个唯一性的token,我们称之为 accessToken;
2、前端对accessToken进行随机性拆分及md5加密,产生签名(保存在本地存储中);
3、前端在与后端进行交互时传递签名;
4、后端接收数据是验证签名。
签名准备
1、在 commons 文件夹内创建
1.1 md5.js //js md5 加密 [ 在课程内获取此 js文件 ]
1.2 sign.js // 签名函数
sign.js
var md5 = require('./md5.js'); module.exports = { sign : function(apiServer){ // 环境判断非uni环境不支持 if(!uni){return '...';} // 连接服务器获取一个临时的accessToken uni.request({ url: apiServer+'getAccessToken', method: 'GET', success: res => { if(res.data.status != 'ok'){return ;} var data = res.data.data; // 对 accessToken 进行md5加密 var accessToken = md5.hex_md5(data.token + data.time); // 签名 = md5(accessToekn + time) + '-' + 'accessToekn'; var sign = accessToken + '-' + data.token; //console.log(sign); // 记录在本地 uni.setStorage({ key:"sign", data:sign }); } }); } }
数据库
DROP TABLE IF EXISTS `yuedu_access_tokens`; CREATE TABLE `yuedu_access_tokens` ( `token` varchar(30) NOT NULL, `time` int(11) DEFAULT NULL, PRIMARY KEY (`token`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
php 端代码
<?php //getAccessToken.php namespace hsC; class getAccessToken{ public function index(){ $db = \hsTool\db::getInstance('access_tokens'); $token = array( 'token' => uniqid(), 'time' => time() ); $db->add($token); exit(jsonCode('ok', $token)); } }
使用说明
在数据提交页面提交之前进行预签名,提交数据时携带此签名!
更合理的签名保存
因为大家后端基础不一样,本课程使用数据库保存了accessToken,更好的方式是 redis 或 memcache,可以设置变量有效期并能自动失效!
在登录环节使用签名验证策略
后端验证签名原理
// 签名验证 function checkSign(){ if(empty($_POST['sign'])){exit(jsonCode('error', 'sign error'));} $sign = explode('-', $_POST['sign']); if(count($sign) != 2){exit(jsonCode('error', 'sign error'));} $db = \hsTool\db::getInstance('access_tokens'); $token = $db->where('token = ?', array($sign[1]))->fetch(); if(empty($token)){exit(jsonCode('error', 'sign error'));} $signMd5 = md5($token['token'].$token['time']); if($signMd5 != $sign[0]){exit(jsonCode('error', 'sign error'));} // 验证成功则删除 $db->where('token = ?', array($sign[1]))->delete(); }
登录页面签名机制改进
<template> <view> <!-- #ifdef MP-WEIXIN --> <button type="primary" open-type="getUserInfo" @getuserinfo="getUserInfo">使用微信登录</button> <!-- #endif --> </view> </template> <script> var _self, pageOptions, session_key, openid; var sign = require('../../commons/sign.js'); export default { data() { return { }; }, methods:{ // #ifdef MP-WEIXIN getUserInfo : (info) => { info = info.mp.detail.userInfo; //userInfo {"nickName":"深海","gender":1,...avatarUrl":"https://7tdPvkPaJlkaLFFbLAffGVApluZdanLkDVplNlAhq1EJA/132"} // 与服务器交互将数据提交到服务端数据库 var sign = uni.getStorageSync('sign'); uni.request({ url: _self.apiServer+'member&m=login', method: 'POST', header: {'content-type' : "application/x-www-form-urlencoded"}, data: { openid : openid, name : info.nickName, face : info.avatarUrl, sign : sign }, success: res => { console.log(res); res = res.data; // 登录成功 记录会员信息到本地 if(res.status == 'ok'){ uni.showToast({title:"登录成功"}); uni.setStorageSync('SUID' , res.data.u_id + ''); uni.setStorageSync('SRAND', res.data.u_random + ''); uni.setStorageSync('SNAME', res.data.u_name + ''); uni.setStorageSync('SFACE', res.data.u_face + ''); // 跳转 if(pageOptions.backtype == 1){ uni.redirectTo({url:pageOptions.backpage}); }else{ uni.switchTab({url:pageOptions.backpage}); } }else{ uni.showToast({title:res.data}); } }, fail: (e) => { console.log(JSON.stringify(e)); } }); }, // #endif }, onLoad:function(options){ // 预先签名 sign.sign(this.apiServer); _self = this; pageOptions = options; // #ifdef MP-WEIXIN // 调用 微信 login 获取 code uni.login({ success: (res) => { uni.request({ url:_self.apiServer+'member&m=codeToSession&code='+res.code, success: (sessions) => { // sessions.date 数据格式 // expires_in:7200 // openid:"oS6of0V0rdp9nY_BuvCnQUasOHYc" // session_key:"87sE2oDD8lc+aDJj0tB6+g==" // 获取 unionId session_key = sessions.data.session_key; openid = sessions.data.openid; }, }); } }); // #endif //app 端微信登录 // 手册位置 https://uniapp.dcloud.io/api/plugins/login?id=getuserinfo // #ifdef APP-PLUS uni.login({ success: (res) => { // res 对象格式 //{"code":"***", //"authResult":{ //"openid":"***", //"scope":"snsapi_userinfo", //"refresh_token":"**", //"code":"****", //"unionid":"***", //"access_token":"***", //"expires_in":7200 //}, //"errMsg":"login:ok"} uni.getUserInfo({ success: (info) => { // info 对象格式 // {"errMsg":"getUserInfo:ok", // "rawData":"... // "userInfo":{ //"openId":"***", //"nickName":"***", //"gender":1, // "city":"Xi'an", // "province":"Shaanxi", // "country":"China", // "avatarUrl":"头像", // "unionId":"oU5Yyt_agt547zWyA0v0eLfFPqxo" //},"signature":""} // 与服务器交互将数据提交到服务端数据库 var sign = uni.getStorageSync('sign'); uni.request({ url: _self.apiServer+'member&m=login', method: 'POST', header: {'content-type' : "application/x-www-form-urlencoded"}, data: { openid : info.userInfo.openId, uni.setStorageSync('SRAND', res.data.u_random + ''); uni.setStorageSync('SNAME', res.data.u_name + ''); face : info.userInfo.avatarUrl, sign : sign }, success: res => { console.log(JSON.stringify(res)); res = res.data; // 登录成功 记录会员信息到本地 if(res.status == 'ok'){ uni.showToast({title:"登录成功"}); uni.setStorageSync('SUID' , res.data.u_id + ''); uni.setStorageSync('SRAND', res.data.u_name + ''); uni.setStorageSync('SFACE', res.data.u_face + ''); // 跳转 if(options.backtype == 1){ uni.redirectTo({url:options.backpage}); }else{ uni.switchTab({url:options.backpage}); } }else{ uni.showToast({title:res.data}); } }, fail: (e) => { console.log(JSON.stringify(e)); } }); }, fail: () => { uni.showToast({title:"微信登录授权失败"}); } }) }, fail: () => { uni.showToast({title:"微信登录授权失败"}); uni.hideLoading(); } }) // #endif } } </script> <style> </style>