如何在node和vue前后端分离的项目中使用极客验证,用node的方式
1.用express的脚手架和vue-cli的脚手架搭建的项目目录如下图
2.在vue-client的src/component新建一个login.vue文件,作为登录页面,代码如下
1 <template> 2 <div class="login"> 3 <h1>滑动模式</h1> 4 <form id="form"> 5 <div> 6 <label for="username">用户名:</label> 7 <input class="inp" id="username" type="text" value="用户名"> 8 </div> 9 <br> 10 <div> 11 <label for="password">密码:</label> 12 <input class="inp" id="password" type="password" value="123456"> 13 </div> 14 <br> 15 <div> 16 <label>完成验证:</label> 17 <div id="captcha"> 18 <p id="wait" class="show">正在加载验证码......</p> 19 </div> 20 </div> 21 <br> 22 <div id="btn" class="btn">提交</div> 23 </form> 24 </div> 25 </template> 26 27 <script> 28 import jqu from "../../static/js/jquery-2.1.0.js" ; //在验证插件里面有用到jquery方便对节点的操作 29 import gt from "../../static/js/gt.js" ;//用于加载id对应的验证码库,并支持宕机模式 * 暴露 initGeetest 进行验证码的初始化 30 import slid from "../../static/js/slider.js" ; //页面对接口的调用 31 32 33 34 export default { 35 name: 'login', 36 data () { 37 return { 38 msg: '' 39 } 40 } 41 } 42 </script> 43 44 <!-- Add "scoped" attribute to limit CSS to this component only --> 45 <style scoped> 46 body { 47 margin: 50px 0; 48 text-align: center; 49 font-family: "PingFangSC-Regular", "Open Sans", Arial, "Hiragino Sans GB", "Microsoft YaHei", "STHeiti", "WenQuanYi Micro Hei", SimSun, sans-serif; 50 } 51 52 .inp { 53 border: 1px solid #cccccc; 54 border-radius: 2px; 55 padding: 0 10px; 56 width: 278px; 57 height: 40px; 58 font-size: 18px; 59 } 60 61 .btn { 62 display: inline-block; 63 box-sizing: border-box; 64 border: 1px solid #cccccc; 65 border-radius: 2px; 66 width: 100px; 67 height: 40px; 68 line-height: 40px; 69 font-size: 16px; 70 color: #666; 71 cursor: pointer; 72 background: white linear-gradient(180deg, #ffffff 0%, #f3f3f3 100%); 73 } 74 75 .btn:hover { 76 background: white linear-gradient(0deg, #ffffff 0%, #f3f3f3 100%) 77 } 78 79 #captcha { 80 width: 300px; 81 display: inline-block; 82 } 83 84 label { 85 vertical-align: top; 86 display: inline-block; 87 width: 80px; 88 text-align: right; 89 } 90 91 #wait { 92 text-align: left; 93 color: #666; 94 margin: 0; 95 } 96 </style>
3.这里是gt.js,放在static/js文件夹下面
1 /* initGeetest 1.0.0 2 * 用于加载id对应的验证码库,并支持宕机模式 3 * 暴露 initGeetest 进行验证码的初始化 4 * 一般不需要用户进行修改 5 */ 6 (function (global, factory) { 7 "use strict"; 8 if (typeof module === "object" && typeof module.exports === "object") { 9 // CommonJS 10 module.exports = global.document ? 11 factory(global, true) : 12 function (w) { 13 if (!w.document) { 14 throw new Error("Geetest requires a window with a document"); 15 } 16 return factory(w); 17 }; 18 } else { 19 factory(global); 20 } 21 })(typeof window !== "undefined" ? window : this, function (window, noGlobal) { 22 "use strict"; 23 if (typeof window === 'undefined') { 24 throw new Error('Geetest requires browser environment'); 25 } 26 var document = window.document; 27 var Math = window.Math; 28 var head = document.getElementsByTagName("head")[0]; 29 30 function _Object(obj) { 31 this._obj = obj; 32 } 33 34 _Object.prototype = { 35 _each: function (process) { 36 var _obj = this._obj; 37 for (var k in _obj) { 38 if (_obj.hasOwnProperty(k)) { 39 process(k, _obj[k]); 40 } 41 } 42 return this; 43 } 44 }; 45 function Config(config) { 46 var self = this; 47 new _Object(config)._each(function (key, value) { 48 self[key] = value; 49 }); 50 } 51 52 Config.prototype = { 53 api_server: 'api.geetest.com', 54 protocol: 'http://', 55 type_path: '/gettype.php', 56 fallback_config: { 57 slide: { 58 static_servers: ["static.geetest.com", "dn-staticdown.qbox.me"], 59 type: 'slide', 60 slide: '/static/js/geetest.0.0.0.js' 61 }, 62 fullpage: { 63 static_servers: ["static.geetest.com", "dn-staticdown.qbox.me"], 64 type: 'fullpage', 65 fullpage: '/static/js/fullpage.0.0.0.js' 66 } 67 }, 68 _get_fallback_config: function () { 69 var self = this; 70 if (isString(self.type)) { 71 return self.fallback_config[self.type]; 72 } else if (self.new_captcha) { 73 return self.fallback_config.fullpage; 74 } else { 75 return self.fallback_config.slide; 76 } 77 }, 78 _extend: function (obj) { 79 var self = this; 80 new _Object(obj)._each(function (key, value) { 81 self[key] = value; 82 }) 83 } 84 }; 85 var isNumber = function (value) { 86 return (typeof value === 'number'); 87 }; 88 var isString = function (value) { 89 return (typeof value === 'string'); 90 }; 91 var isBoolean = function (value) { 92 return (typeof value === 'boolean'); 93 }; 94 var isObject = function (value) { 95 return (typeof value === 'object' && value !== null); 96 }; 97 var isFunction = function (value) { 98 return (typeof value === 'function'); 99 }; 100 var callbacks = {}; 101 var status = {}; 102 var random = function () { 103 return parseInt(Math.random() * 10000) + (new Date()).valueOf(); 104 }; 105 var loadScript = function (url, cb) { 106 var script = document.createElement("script"); 107 script.charset = "UTF-8"; 108 script.async = true; 109 script.onerror = function () { 110 cb(true); 111 }; 112 var loaded = false; 113 script.onload = script.onreadystatechange = function () { 114 if (!loaded && 115 (!script.readyState || 116 "loaded" === script.readyState || 117 "complete" === script.readyState)) { 118 119 loaded = true; 120 setTimeout(function () { 121 cb(false); 122 }, 0); 123 } 124 }; 125 script.src = url; 126 head.appendChild(script); 127 }; 128 var normalizeDomain = function (domain) { 129 return domain.replace(/^https?:\/\/|\/$/g, ''); 130 }; 131 var normalizePath = function (path) { 132 path = path.replace(/\/+/g, '/'); 133 if (path.indexOf('/') !== 0) { 134 path = '/' + path; 135 } 136 return path; 137 }; 138 var normalizeQuery = function (query) { 139 if (!query) { 140 return ''; 141 } 142 var q = '?'; 143 new _Object(query)._each(function (key, value) { 144 if (isString(value) || isNumber(value) || isBoolean(value)) { 145 q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&'; 146 } 147 }); 148 if (q === '?') { 149 q = ''; 150 } 151 return q.replace(/&$/, ''); 152 }; 153 var makeURL = function (protocol, domain, path, query) { 154 domain = normalizeDomain(domain); 155 156 var url = normalizePath(path) + normalizeQuery(query); 157 if (domain) { 158 url = protocol + domain + url; 159 } 160 161 return url; 162 }; 163 var load = function (protocol, domains, path, query, cb) { 164 var tryRequest = function (at) { 165 166 var url = makeURL(protocol, domains[at], path, query); 167 loadScript(url, function (err) { 168 if (err) { 169 if (at >= domains.length - 1) { 170 cb(true); 171 } else { 172 tryRequest(at + 1); 173 } 174 } else { 175 cb(false); 176 } 177 }); 178 }; 179 tryRequest(0); 180 }; 181 var jsonp = function (domains, path, config, callback) { 182 if (isObject(config.getLib)) { 183 config._extend(config.getLib); 184 callback(config); 185 return; 186 } 187 if (config.offline) { 188 callback(config._get_fallback_config()); 189 return; 190 } 191 var cb = "geetest_" + random(); 192 window[cb] = function (data) { 193 if (data.status === 'success') { 194 callback(data.data); 195 } else if (!data.status) { 196 callback(data); 197 } else { 198 callback(config._get_fallback_config()); 199 } 200 window[cb] = undefined; 201 try { 202 delete window[cb]; 203 } catch (e) { 204 } 205 }; 206 load(config.protocol, domains, path, { 207 gt: config.gt, 208 callback: cb 209 }, function (err) { 210 if (err) { 211 callback(config._get_fallback_config()); 212 } 213 }); 214 }; 215 var throwError = function (errorType, config) { 216 var errors = { 217 networkError: '网络错误' 218 }; 219 if (typeof config.onError === 'function') { 220 config.onError(errors[errorType]); 221 } else { 222 throw new Error(errors[errorType]); 223 } 224 }; 225 var detect = function () { 226 return !!window.Geetest; 227 }; 228 if (detect()) { 229 status.slide = "loaded"; 230 } 231 var initGeetest = function (userConfig, callback) { 232 var config = new Config(userConfig); 233 if (userConfig.https) { 234 config.protocol = 'https://'; 235 } else if (!userConfig.protocol) { 236 config.protocol = window.location.protocol + '//'; 237 } 238 jsonp([config.api_server || config.apiserver], config.type_path, config, function (newConfig) { 239 var type = newConfig.type; 240 var init = function () { 241 config._extend(newConfig); 242 callback(new window.Geetest(config)); 243 }; 244 callbacks[type] = callbacks[type] || []; 245 var s = status[type] || 'init'; 246 if (s === 'init') { 247 status[type] = 'loading'; 248 callbacks[type].push(init); 249 load(config.protocol, newConfig.static_servers || newConfig.domains, newConfig[type] || newConfig.path, null, function (err) { 250 if (err) { 251 status[type] = 'fail'; 252 throwError('networkError', config); 253 } else { 254 status[type] = 'loaded'; 255 var cbs = callbacks[type]; 256 for (var i = 0, len = cbs.length; i < len; i = i + 1) { 257 var cb = cbs[i]; 258 if (isFunction(cb)) { 259 cb(); 260 } 261 } 262 callbacks[type] = []; 263 } 264 }); 265 } else if (s === "loaded") { 266 init(); 267 } else if (s === "fail") { 268 throwError('networkError', config); 269 } else if (s === "loading") { 270 callbacks[type].push(init); 271 } 272 }); 273 }; 274 window.initGeetest = initGeetest; 275 return initGeetest; 276 });
4.下面是slider.js,也是放在static/js文件夹下面(使用中只用改动两个接口的地址,和自己写的后台地址对应上)
1 var handler = function (captchaObj) { 2 captchaObj.appendTo('#captcha'); 3 captchaObj.onReady(function () { 4 $("#wait").hide(); 5 }); 6 $('#btn').click(function () { 7 var result = captchaObj.getValidate(); 8 if (!result) { 9 return alert('请完成验证'); 10 } 11 $.ajax({ 12 url: 'http://localhost:3000/gt/validate-slide', //这里的地址是根据你的后台接口的地址,我这里是这样的 13 type: 'POST', 14 dataType: 'json', 15 data: { 16 geetest_challenge: result.geetest_challenge, 17 geetest_validate: result.geetest_validate, 18 geetest_seccode: result.geetest_seccode 19 }, 20 success: function (data) { 21 if (data.status === 'success') { 22 alert('登录成功'); 23 } else if (data.status === 'fail') { 24 alert('登录失败,请完成验证'); 25 captchaObj.reset(); 26 } 27 } 28 }); 29 }) 30 // 更多接口说明请参见:http://docs.geetest.com/install/client/web-front/ 31 }; 32 33 34 $.ajax({//这个地址也是需要根据自己的后台接口地址来改动 35 url: "http://localhost:3000/gt/register-slide?t=" + (new Date()).getTime(), // 加随机数防止缓存 36 type: "get", 37 dataType: "json", 38 success: function (data) { 39 console.log(data) 40 // 调用 initGeetest 进行初始化 41 // 参数1:配置参数 42 // 参数2:回调,回调的第一个参数验证码对象,之后可以使用它调用相应的接口 43 44 initGeetest({ 45 // 以下 4 个配置参数为必须,不能缺少 46 gt: data.gt, 47 challenge: data.challenge, 48 offline: !data.success, // 表示用户后台检测极验服务器是否宕机 49 new_captcha: data.new_captcha, // 用于宕机时表示是新验证码的宕机 50 51 product: "float", // 产品形式,包括:float,popup 52 width: "300px" 53 // 更多配置参数说明请参见:http://docs.geetest.com/install/client/web-front/ 54 }, handler); 55 } 56 });
5.后台的配置,在serviceget文件夹的主入口文件app.js中插入验证的接口处理;
1 var express = require('express'); 2 var path = require('path'); 3 var favicon = require('serve-favicon'); 4 var logger = require('morgan'); 5 var cookieParser = require('cookie-parser'); 6 var bodyParser = require('body-parser'); 7 8 var index = require('./routes/index'); 9 var users = require('./routes/users'); 10 11 12 /*这里引入极客验证的包 */ 13 var Geetest = require('gt3-sdk'); 14 var slide = require('./public/javascripts/slide'); 15 16 var app = express(); 17 18 19 //实现跨域 20 app.all('*',function(req,res,next){ 21 res.header('Access-Control-Allow-Origin','http://localhost:8081'); 22 res.header('Access-Control-Allow-Methods','PUT,GET,POST,DELETE,OPTIONS'); 23 res.header('Access-Control-Allow-Headers','X-Requested-With'); 24 res.header('Access-Control-Allow-Headers','Content-Type'); 25 res.header('Access-Control-Allow-Credentials',true); 26 next(); 27 }) 28 29 //session,需要引用下面的包 30 var session = require('express-session'); 31 app.use(session({ 32 secret: 'classweb531234', //设置 session 签名 33 name: 'classweb', 34 cookie: { 35 maxAge: 60 * 1000 * 60 * 24 36 }, // 储存的时间 24小时 37 resave: false, // 每次请求都重新设置session 38 saveUninitialized: true 39 })); 40 41 42 43 //极客验证 44 app.get("/gt/register-slide", function (req, res) { 45 slide.register(null, function (err, data) { 46 if (err) { 47 console.error(err); 48 res.status(500); 49 res.send(err); 50 return; 51 } 52 53 if (!data.success) { 54 req.session.fallback = true; 55 res.send(data); 56 } else { 57 req.session.fallback = false; 58 res.send(data); 59 } 60 }); 61 }); 62 63 64 65 66 // view engine setup 67 app.set('views', path.join(__dirname, 'views')); 68 app.set('view engine', 'jade'); 69 70 // uncomment after placing your favicon in /public 71 //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 72 app.use(logger('dev')); 73 app.use(bodyParser.json()); 74 app.use(bodyParser.urlencoded({ extended: false })); 75 app.use(cookieParser()); 76 app.use(express.static(path.join(__dirname, 'public'))); 77 78 //极客验证的二次验证,这里可以对用户名,和密码验证 79 //这里用了req.body所以要放在bodyparser中间件申明后的地方 80 app.post("/gt/validate-slide", function (req, res) { 81 // 对ajax提供的验证凭证进行二次验证 82 slide.validate(req.session.fallback, { 83 geetest_challenge: req.body.geetest_challenge, 84 geetest_validate: req.body.geetest_validate, 85 geetest_seccode: req.body.geetest_seccode 86 }, function (err, success) { 87 88 if (err) { 89 // 网络错误 90 res.send({ 91 status: "error", 92 info: err 93 }); 94 95 } else if (!success) { 96 97 // 二次验证失败 98 res.send({ 99 status: "fail", 100 info: '登录失败' 101 }); 102 } else { 103 104 res.send({ 105 status: "success", 106 info: '登录成功' 107 }); 108 } 109 }); 110 }); 111 112 113 114 115 116 117 app.use('/', index); 118 app.use('/users', users); 119 120 // catch 404 and forward to error handler 121 app.use(function(req, res, next) { 122 var err = new Error('Not Found'); 123 err.status = 404; 124 next(err); 125 }); 126 127 // error handler 128 app.use(function(err, req, res, next) { 129 // set locals, only providing error in development 130 res.locals.message = err.message; 131 res.locals.error = req.app.get('env') === 'development' ? err : {}; 132 133 // render the error page 134 res.status(err.status || 500); 135 res.render('error'); 136 }); 137 138 module.exports = app;
6.在后台serviceget/public/javascripts 文件夹下面复制进gt-sdk.js 和slide.js(gt-sdk.js是对sdk包的处理,slide.js是保存的自己的密钥,和对gt-sdk.js方法的引用)
gt-sdk.js代码如下:
1 "use strict"; 2 var crypto = require('crypto'), 3 request = require('request'), 4 pkg = require("../../package.json"); //这个地方地址根据自己文件位置来找,找到package.json的相对路径 5 6 var md5 = function (str) { 7 return crypto.createHash('md5').update(String(str)).digest('hex'); 8 }; 9 var randint = function (from, to) { 10 // range: from ~ to 11 return Math.floor(Math.random() * (to - from + 1) + from); 12 }; 13 function Geetest(config) { 14 if (typeof config.geetest_id !== 'string') { 15 throw new Error('Geetest ID Required'); 16 } 17 if (typeof config.geetest_key !== 'string') { 18 throw new Error("Geetest KEY Required"); 19 } 20 if (typeof config.protocol === 'string') { 21 this.PROTOCOL = config.protocol; 22 } 23 if (typeof config.api_server === 'string') { 24 this.API_SERVER = config.api_server; 25 } 26 if (typeof config.timeout === 'number') { 27 this.TIMEOUT = config.timeout; 28 } 29 30 this.geetest_id = config.geetest_id; 31 this.geetest_key = config.geetest_key; 32 } 33 Geetest.prototype = { 34 PROTOCOL: 'http://', 35 API_SERVER: 'api.geetest.com', 36 VALIDATE_PATH: '/validate.php', 37 REGISTER_PATH: '/register.php', 38 TIMEOUT: 2000, 39 NEW_CAPTCHA: true, 40 JSON_FORMAT: 1, 41 register: function (data, callback) { 42 var that = this; 43 return new Promise(function (resolve, reject) { 44 that._register(data, function (err, data) { 45 if (typeof callback === 'function') { 46 callback(err, data); 47 } 48 if (err) { 49 reject(err); 50 } else { 51 resolve(data); 52 } 53 }); 54 }); 55 }, 56 _register: function (data, callback) { 57 data = data || {}; 58 var that = this; 59 request({ 60 url: this.PROTOCOL + this.API_SERVER + this.REGISTER_PATH, 61 method: 'GET', 62 timeout: this.TIMEOUT, 63 json: true, 64 qs: { 65 gt: this.geetest_id, 66 json_format: this.JSON_FORMAT, 67 sdk: 'Node_' + pkg.version, 68 client_type: data.client_type || 'unknown', 69 ip_address: data.ip_address || 'unknown' 70 } 71 }, function (err, res, data) { 72 var challenge; 73 if (err || !data || !data.challenge) { 74 // fallback 75 challenge = that._make_challenge(); 76 callback(null, { 77 success: 0, 78 challenge: challenge, 79 gt: that.geetest_id, 80 new_captcha: that.NEW_CAPTCHA 81 }); 82 } else { 83 challenge = md5(data.challenge + that.geetest_key); 84 callback(null, { 85 success: 1, 86 challenge: challenge, 87 gt: that.geetest_id, 88 new_captcha: that.NEW_CAPTCHA 89 }); 90 } 91 }); 92 }, 93 validate: function (fallback, result, callback) { 94 var that = this; 95 return new Promise(function (resolve, reject) { 96 that._validate(fallback, result, function (err, data) { 97 if (typeof callback === 'function') { 98 callback(err, data); 99 } 100 if (err) { 101 reject(err); 102 } else { 103 resolve(data); 104 } 105 }); 106 }) 107 }, 108 _validate: function (fallback, result, callback) { 109 var challenge = result.challenge || result.geetest_challenge; 110 var validate = result.validate || result.geetest_validate; 111 var seccode = result.seccode || result.geetest_seccode; 112 if (fallback) { 113 if (md5(challenge) === validate) { 114 callback(null, true); 115 } else { 116 callback(null, false); 117 } 118 } else { 119 var hash = this.geetest_key + 'geetest' + challenge; 120 if (validate === md5(hash)) { 121 request({ 122 url: this.PROTOCOL + this.API_SERVER + this.VALIDATE_PATH, 123 method: 'POST', 124 timeout: this.TIMEOUT, 125 json: true, 126 form: { 127 gt: this.geetest_id, 128 seccode: seccode, 129 json_format: this.JSON_FORMAT 130 } 131 }, function (err, res, data) { 132 if (err || !data || !data.seccode) { 133 callback(err); 134 } else { 135 callback(null, data.seccode === md5(seccode)); 136 } 137 }); 138 } else { 139 callback(null, false); 140 } 141 } 142 }, 143 _make_challenge: function () { 144 var rnd1 = randint(0, 90); 145 var rnd2 = randint(0, 90); 146 var md5_str1 = md5(rnd1); 147 var md5_str2 = md5(rnd2); 148 return md5_str1 + md5_str2.slice(0, 2); 149 } 150 }; 151 152 module.exports = Geetest;
slide.js代码如下:
var Geetest = require('./gt-sdk'); var captcha = new Geetest({ geetest_id: '***************************', //注册极客获得的id geetest_key: '**************************' //注册极客获得的key }); module.exports = captcha;