【技术推荐】前端JS攻防对抗

news2024/11/24 6:37:32

简介


网络爬虫一直以来是让网站维护人员头痛的事情,即要为搜索引擎开方便之门,提升网站排名、广告引入等,又要面对恶意爬虫做出应对措施,避免数据被非法获取,甚至出售。因此促生出爬虫和反爬虫这场旷日持久的战斗。

爬虫的开发从最初的简单脚本到PhantomJs、selenium再进化到puppeteer、playwright等,和浏览器结合越来越密切。

反爬虫的手段从ua、Header检测到IP频率检测再到网站重构、验证码、JS加密等,手段越来越多样。

下表是爬虫攻防手段发展一个简单的对比:

阶段攻击防御
1简单脚本编写爬虫,Python、Java等通过检测User-Agent、HTTP Headers来区分是否为机器人
2添加正常浏览器请求头部,伪装成正常用户访问通过检测IP访问频率,分析出短时间出现访问次数异常的IP,进行封禁
3使用IP代理池,秒拨等技术,拥有大量IP避免通过HTML访问直接获取数据,进行网站重构,使用Ajax动态传输数据
4分析数据传输接口,直接访问接口获取数据添加验证码、JS参数加密
5深度学习破解验证码、JS调试破解参数加密寻求第三方安全产品

反爬虫的手段到现在已经成体系化了,访问令牌(身份认证)、验证码(滑动、逻辑、三维等)、行为&指纹检测(人机区分)、请求&响应加密等。所有这些功能的实现都是依靠前端JS代码,对于攻击者,如何去绕过反爬虫手段,分析前端JS代码就成为了必经之路。那么JS如何不被破解,也成为了反爬虫的关键。

本文只探讨JS如何防破解,其它反爬虫手段不展开讨论。

JS防破解


JS防破解主要客户分为两个部分:代码混淆反调试

代码混淆

从代码布局、数据、控制三个方面入手,进行混淆。

布局混淆

常见手段有无效代码删除,常量名、变量名、函数名等标识符混淆等。

  • 无效代码删除

    1. 注释文本对于理解代码逻辑有很多帮助,生产环境需要删除。

    2. 调试信息对于开发者调试Bug有很大的帮助,生产环境需要删除。

    3. 无用函数和数据需要删除,避免攻击者能够猜到开发者意图,和垃圾代码添加不同。

    4. 缩进、换行符删除,减小代码体积,增加阅读难度。

  • 标识符重命名

    1. 单字母。还可以是aaa1等,需要注意避免作用域内标识符冲突。

           var animal = 'shark' //源代码
      

      var a = ‘shark’ //重命名

    2. 十六进制。

           var animal = 'shark' //源代码
      

      var _0x616e696d616c = ‘shark’ //重命名

使用十六进制重命名可以衍生到其它方法,但重命名最重要的还要 使用简短的字符替换所有的标识符,并且作用域内不碰撞,不同作用域尽量碰撞

这种重命名方式对于常量同样有效。

            var _$Qo = window , _$Q0 = String, _$Q0O = Array, _$QO = document, _$$Q0O = Date

变量名不同作用碰撞。函数名和函数局部变量碰撞,不用函数内局部变量碰撞,全局变量和局部变量碰撞等等。

            function _$QQO(){
    	var _$QQO,
    }
  • 垃圾代码

在源代码中填写大量的垃圾代码达到混淆视听的效果。

数据混淆

常见数据类型混淆有数字、字符串、数组、布尔等。

  • 数字

数字类型混淆主要是进制转换,还有一些利用数学技巧。

    var number = 233 //十进制
var number = 0351 //八进制
var number = 0xe9 //十六进制
var number = 0b11101001 //二进制
  • 字符串

字符串的混淆主要是编码。

1. 十六进制
    
            var user = 'shark' //混淆前
    var user = '\x73\x68\x61\x72\x6b' //十六进制
    

2. Unicode
    
            var user = 'shark' //混淆前
    var user3 = '\u0073\u0068\u0061\u0072\u006b' //unicode编码
    

3. 转数组,把字符串转为字节数组
    
            console.log('s'.charCodeAt(0)) //115
    console.log('h'.charCodeAt(0)) //104
    console.log('a'.charCodeAt(0)) //97
    console.log('r'.charCodeAt(0)) //114
    console.log('k'.charCodeAt(0)) //107
    console.log(String.fromCharCode(115,104,97,114,107)) //shark
    
    function stringToByte(str){
        var bytearr = [];
        for(var i =0;i<str.length;i++){
            bytearr.push(str.charCodeAt(i));
        }
        return bytearr
    }
    stringToByte('shark')
    Array(5) [ 115, 104, 97, 114, 107 ]
    

4. Base64
    
            var user = 'shark'
    var user = 'c2hhcms=' //base64编码后
    
    常量编码
    'slice' ---> 'c2xpY2U='

还有其它的手法,比如拆分字符串,加密字符串然后解密,这里不展开说明。

  • 数组

数组的混淆主要是元素引用和元素顺序。

    var arr = ['log','Date','getTime']
console[arr[0]](new window[arr[1]]()[arr[2]]()) // console.log(new window.Date().getTime())

加入编码后字符串
var arr = ['\u006C\u006F\u0067','\u0044\u0061\u0074\u0065','\u0067\u0065\u0074\u0054\u0069\u006D\u0065']
console[arr[0]](new window[arr[1]]()[arr[2]]()) //同上

在对元素做编码之后,之后进行引用会有一个问题,数组索引和数组元素是一一对应的,这样可以很直观的找出元素。可以进行元素顺序打乱,再通过函数还原。

    var arr = ['\u006C\u006F\u0067','\u0044\u0061\u0074\u0065','\u0067\u0065\u0074\u0054\u0069\u006D\u0065']
(function(arr,num){
  var shuffer = function(nums)  {
      while(--nums){
          arr.unshift(arr.pop());
      }
  };
  shuffer(++num);
}(arr,0x10)) //打乱数组元素
Array(3) [ "getTime", "log", "Date" ]
console[arr3[1]](new window[arr3[2]]()[arr3[0]]()) //同上
  • 布尔值

主要是使用一些计算来替代truefalse

    undefined //false
null //false
+0、-0、NaN //false

!undefined //true
!null //true
!0 //true
!NaN //true
!"" //true
!{} //true
![] //true
!void(0) //true

控制混淆

通过上面的混淆手段可以把代码混淆的已经很难读了,但是代码的执行流程没有改变,接下来介绍下混淆代码执行流程的方法。

  • 控制流平坦化

代码原始流程是一个线性流程执行,通过平坦化之后会变成一个循环流程进行执行。

原流程

image

平坦化流程

image

    function source(){
    var a = 1;
    var b = a + 10;
    var c = b + 20;
    var d = c + 30;
    var e = d + 40;
    return e;
}
console.log(source()); //101

函数内执行流程进行平坦化
switch(seq){
    case '1':
        var e = d + 40;
        continue;
    case '2':
        var d = c + 30;
        continue;
    case '3':
        var b = a + 10;
        continue;
    case '4':
        var c = b + 20;
        continue;
    case '5':
        var a = 1;
        continue;
    case '6':
        return e;
        continue;
}

加上分发器
function controlflow(){
    var controlflow_seq = '5|3|4|2|1|6'.split('|'),i = 0
    while(!![]){
        switch(controlflow_seq[i++]){
            case '1':
                var e = d + 40;
                continue;
            case '2':
                var d = c + 30;
                continue;
            case '3':
                var b = a + 10;
                continue;
            case '4':
                var c = b + 20;
                continue;
            case '5':
                var a = 1;
                continue;
            case '6':
                return e;
                continue;
        }
        break;
    }
}
console.log(controlflow()); //101

上面是一个比较简单示例,平坦化一般有几种表示,while...switch...casewhile...if....elesif

while...if...eleseif的还原难度更高。比如if(seq == 1)...elseif...可以优化成if(seq & 0x10 ==1)...elseif...

  • 逗号表达式

通过逗号把语句连接在一起,还可以结合括号进行变形。

    function source(){
    var a = 1;
    var b = a + 10;
    var c = b + 20;
    var d = c + 30;
    var e = d + 40;
    return e;
}
console.log(source()); //101

function source(){
    var a,b,c,d,e;
    return a = 1,b = a + 10,c = b + 20,d = c + 30,e = d + 40,e
}
console.log(source());

 function source(){
    var a,b,c,d,e;
    return e = (d = ( c = (b = (a = 1, a+10),b+20),c+30),d+40);
}
console.log(source());

混淆工具

  • 在线混淆

在线obfuscator混淆网站

能够满足基本混淆的力度,但也要自己调整,否则可能会很耗性能。不过ob的混淆现在网上有很多还原的工具。

  • AST

对Javascript来说,用AST可以按照自己的需求进行混淆,也可以很好的用来解混淆。是一个终极工具。

AST在线转换,利用这个网站进行AST解析后,再本地使用AST库进行语法树转换、生成。

1. AST处理控制流平坦化

平坦化代码

            var array = '4|3|8|5|4|0|2|3'.split('|'), index = 0;
    
    while (true) {
        switch (array[index++]) {
            case '0':
                console.log('This is case 0');
                continue;
            case '1':
                console.log('This is case 1');
                continue;
            case '2':
                console.log('This is case 2');
                continue;
            case '3':
                console.log('This is case 3');
                continue;
            case '4':
                console.log('This is case 4');
                continue;
            case '5':
                console.log('This is case 5');
                continue;
            case '6':
                console.log('This is case 6');
                continue;
            case '7':
                console.log('This is case 7');
                continue;
            case '8':
                console.log('This is case 8');
                continue;
            case '9':
                console.log('This is case 9');
                continue;
            default:
                console.log('This is case [default], exit loop.');
        }
        break;
    }

先把上面的代码放到AST网站进行解析生成语法树。

这里使用babel进行转换。

image
image

还原的思路:先获取分发器生成的顺序,随后把分支语句和条件对应生成case对象,再利用分发器顺序从case对象获取case,最后输出即可。

            // 转换为 ast 树
    let ast = parser.parse(jscode);
    
    const visitor =
    {
      WhileStatement(path){
        let {body} = path.node;
        let switch_statement = body.body[0]; //获取switch的节点
        //判断switch结构
        if (!types.isSwitchStatement(switch_statement)) {
          return;
        }
        //获取条件表达式和case组合
        let { discriminant, cases } = switch_statement;
        // 条件表达式进一步进行特征判断
        if (!types.isMemberExpression(discriminant) || !types.isUpdateExpression(discriminant.property)) {
          return;
        }
        //获取条件表示引用的变量名,"array"
        let array_binding = path.scope.getBinding(discriminant.object.name)
        //表达式执行,获取"array"的值,"['4', '3', '8', '5', '4', '0', '2', '3']"
        let {confident, value} = array_binding.path.get('init').evaluate()
        if (!confident) {
          return;
        }
        let array = value,case_map = {},tmp_array = [];
        /**
         * 遍历所有case,生成case_map
         */
        for (let c of cases){
          let {consequent, test} = c;
          let test_value;
          /**
           * case值
           */
          if (test){
            test_value = test.value;
          }
          else{
            test_value = 'default_case';
          }
          /**
           * 获取所有的case下语句
           */
          let statement_array = [];
          for (let i of consequent)
          {
            /**
             * 丢弃continue语句
             */
            if (types.isContinueStatement(i)) {
              continue;
            }
            statement_array.push(i)
          }
          case_map[test_value] = statement_array;
        }
        /**
         * 根据array执行顺序拼接case语句
         */
        for (let i of array){
          tmp_array = tmp_array.concat(case_map[i]);
        }
        if (case_map.hasOwnProperty('default_case')) {
          tmp_array = tmp_array.concat(case_map['default_case'])
        }
        //替换节点
        path.replaceWithMultiple(tmp_array);
        /**
         * 手动更新scope
         */
        path.scope.crawl();
      }
    }
    
    //调用插件,处理待处理 js ast 树
    traverse(ast, visitor);

上面时AST处理的核心代码,转换后如下

            var array = '4|3|8|5|4|0|2|3'.split('|'),
        index = 0;
    console.log('This is case 4');
    console.log('This is case 3');
    console.log('This is case 8');
    console.log('This is case 5');
    console.log('This is case 4');
    console.log('This is case 0');
    console.log('This is case 2');
    console.log('This is case 3');
    console.log('This is case [default], exit loop.');

当然其它的控制流平坦化也是可以还原的,有兴趣的可以自己探索,有问题可以探讨交流。

反调试

​ 代码混淆只能给攻击者增加代码阅读的难度,但是如果进行动态调试分析结合本地静态代码分析还是可以找到代码关键逻辑。那么如何防调试就是很重要的一点。

​ 下面从JS调试的攻防角度做一个统计:

攻击手法防御手法
控制台打开控制台快捷删除,宽度检测
控制台调试器打开debugger
控制台输出控制台清空,内置函数重写
控制台打断点scope检测、debugger
控制台调用DOM事件堆栈检测
函数、对象属性修改函数防劫持、对象冻结
NodeJS本地调式分析代码格式化检测

控制台

打开

删除打开控制台的快捷键阻止控制台打开。

绕过:从菜单启动开发者工具。

window.addEventListener('keydown', function(event){ 
    console.log(event);
    if (event.key == "F12" || ((event.ctrlKey || event.altKey) && (event.code == "KeyI" || event.key == "KeyJ" || event.key == "KeyU"))) {
        event.preventDefault(); 
        return false;
    }
});
window.addEventListener('contextmenu', function(event){ 
    event.preventDefault();
    return false;
});

宽度检测判断窗口是否变化。可能会存在误检测的情况,需要注意。

(function () {
	'use strict';

	const devtools = {
		isOpen: false,
		orientation: undefined
	};

	const threshold = 160;

	const emitEvent = (isOpen, orientation) => {
        let string = "<p>DevTools are " + (isOpen ? "open" : "closed") + "</p>";
        console.log(string);
        document.write(string);
	};

	setInterval(() => {
		const widthThreshold = window.outerWidth - window.innerWidth > threshold;
		const heightThreshold = window.outerHeight - window.innerHeight > threshold;
		const orientation = widthThreshold ? 'vertical' : 'horizontal';

		if (
			!(heightThreshold && widthThreshold) &&
			((window.Firebug && window.Firebug.chrome && window.Firebug.chrome.isInitialized) || widthThreshold || heightThreshold)
		) {
			if (!devtools.isOpen || devtools.orientation !== orientation) {
				emitEvent(true, orientation);
			}

			devtools.isOpen = true;
			devtools.orientation = orientation;
		} else {
            console.log(devtools.isOpen);
			if (devtools.isOpen) {
				emitEvent(false, undefined);
			}

			devtools.isOpen = false;
			devtools.orientation = undefined;
		}
	}, 500);

	if (typeof module !== 'undefined' && module.exports) {
		module.exports = devtools;
	} else {
		window.devtools = devtools;
	}
})();

image

调试器

开发者工具打开之后,需要选择调试器功能进行调试分析。通过设置debugger来阻止调试器调试。

  1. 定时debugger
function debug() {
    debugger;
    setTimeout(debug, 1);
}
debug();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HWcDd3cX-1673936374030)(https://image.3001.net/images/20220512/1652358483_627cfd53a6b5bab56df3c.gif!small)]

这种debugger会一直停住,对调试影响很大。

  1. 时间差debugger

    addEventListener(“load”, () => {
    var threshold = 500;
    const measure = () => {
    const start = performance.now();
    debugger;
    const time = performance.now() - start;
    if (time > threshold) {
    document.write(“

    DevTools were open since page load

    ”);
    }
    }
    setInterval(measure, 300);
    });

image

由于debugger会停止使调试器停止,可以通过计算时间差来判断时否打开调试器。还可以单独时间差进行检测,debugger放在其它地方。

绕过:借助的调试器的"条件断点"、'“Never pause here”'功能。

输出

清空控制台的打印,可以避免攻击者修改代码打印对象等。

function clear() {
    console.clear();
    setTimeout(clear, 10);
}
clear();

绕过:对于直接在控制台打印变量没有影响,并且调试时可以直接查看变量。

断点调试

如何去检测攻击者是否在打断点调试,有两种思路,一种是通过时间来检测,另外一种是依靠scope来检测。两种都有各自的问题。

  1. 时间检测断点调试

    var timeSinceLast;
    addEventListener(“load”, () => {
    var threshold = 1000;
    const measure = () => {
    if (!timeSinceLast) {
    timeSinceLast = performance.now();
    }
    const diff = performance.now() - timeSinceLast;
    if (diff > threshold) {
    document.write(“

    A breakpoint was hit

    ”);
    }
    timeSinceLast = performance.now();
    }
    setInterval(measure, 300);
    });

当页面加载完成时,执行函数,定义一个时间基线,检测代码执行时间差是不是超过时间基线,一旦存在断点,必然会超过时间基线,那么就检出断点调试。但这里有个问题是如果浏览器执行代码的时间差也超过时间基线也会被检出,也就是误检。这种情况出现的机率还挺高,如果业务前端比较复杂(现在一般都是),使用性能不好的浏览器就会出现误检。

2. scope检测
    
            function malicious() {
        const detect = (function(){
            const dummy = /./;
            dummy.toString = () => {
                alert('someone is debugging the malicious function!');
                return 'SOME_NAME';
            };
            return dummy;
        }());
    }
    function legit() {
        // do a legit action
        return 1 + 1;
    }
    function main() {
        legit();
        malicious();
    }
    debugger;
    main();

image

变量在被定义之后,调试器在断点执行的时候获取其scope,从而触发toString函数。浏览器的兼容性是这个方法的缺陷。

事件调用

攻击者经常利用控制台执行事件调用,例如通过获取按钮元素后,点击,提交用户名和密码登录。函数堆栈就可以检测出这种情况。

function test(){
            console.log(new Error().stack); //Chrome、Firefox

            IE 11
             try {
                 throw new Error('');
             } catch (error) {
                stack = error.stack || '';
             }

             console.log(stack); 

            console.log(1);
        }
        test()
  • Firefox

image

  • Chrome

image

从Firefox和Chrome的结果可以看出来,代码自执行的堆栈和控制台执行的堆栈是不同的。

函数、对象属性修改

攻击者在调试的时,经常会把防护的函数删除,或者把检测数据对象进行篡改。可以检测函数内容,在原型上设置禁止修改。

// eval函数
function eval() {
    [native code]
}

//使用eval.toString进行内容匹配”[native code]”,可以轻易饶过
window.eval = function(str){
        /*[native code]*/
        //[native code]
        console.log("[native code]");
    };

//对eval.toString进行全匹配,通过重写toString就可以绕过
window.eval = function(str){
        //....
    };
    window.eval.toString = function(){
        return `function eval() {
            [native code]
        }`
    };

//检测eval.toString和eval的原型
function hijacked(fun){
        return "prototype" in fun || fun.toString().replace(/\n|\s/g, "") != "function"+fun.name+"(){[nativecode]}";
    }

image

//设置函数属性之后,无法被修改
Object.defineProperty(window, 'eval', {
        writable: false,configurable: false,enumerable: true
    });

image

NodeJS调试

攻击者在本地分析调试时需要把代码进行格式化后才能够分析。

//格式化后
function y() {
    var t = (function() {
        var B = !![];
        return function(W, i) {
            var F = B ? function() {
                if (i) {
                    var g = i['apply'](W, arguments);
                    i = null;
                    return g;
                }
            } : function() {};
            B = ![];
            return F;
        };
    }());
    var l = t(this, function() {
        return l['toString']()['search']('(((.+)+)+)+$')['toString']()['constructor'](l)['search']('(((.+)+)+)+$');
    });
    l();
    console['log']('aaaa');
    console['log']('ccc');
};
y();

function Y() {
    console['log']('bbbbb');
};
Y();

//格式化前
function y(){var t=(function(){var B=![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TwpAJKOW-1673936374032)(W,arguments)];i=null;return g;}}:function(){};B=![];return F;};}());var l=t(this,function(){return l['toString'](null)+)+)+$')['toString']()['constructor'](l)['search']('(((.+)+)+)+$');});l();console['log']('aaaa');console['log']('ccc');};y();function Y(){console['log']('bbbbb');};Y();

执行格式化后的代码会出现递归爆炸的情况,因为匹配了换行符。

image

结语


本文只列举了一些常见的前端JS防破解手段,还有一些更高级的手段,例如:自定义编译器、WebAssembly、浏览器特性挖掘等。结合自身的业务合理的使用代码混淆和反调试手法,来保证业务不被恶意分析,避免遭到爬虫的危害。

String’]()‘constructor’‘search’;
});
l();
console’log’;
console’log’;
};
y();

function Y() {
    console['log']('bbbbb');
};
Y();

//格式化前
function y(){var t=(function(){var B=![外链图片转存中...(img-TwpAJKOW-1673936374032)];i=null;return g;}}:function(){};B=[外链图片转存中...(img-8RBAPvv6-1673936374033)]+)+)+$')['toString']()['constructor'](l)['search']('(((.+)+)+)+$');});l();console['log']('aaaa');console['log']('ccc');};y();function Y(){console['log']('bbbbb');};Y();

最后

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

同时每个成长路线对应的板块都有配套的视频提供:


当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料&工具,并且已经帮大家分好类了。

因篇幅有限,仅展示部分资料,有需要的小伙伴,可以【扫下方二维码】免费领取:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/169727.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java中的Arrays类

1、问题Arrays类是什么&#xff0c;Arrays常用方法。2、方法了解Arrays类的概念Arrays 位于java.util包下,Arrays是一个操作数组的工具类。Arrays常用方法Arrays.fill&#xff1a;替换数组原元素&#xff1b;Arrays.sort:对数组进行排序&#xff08;递增&#xff09;&#xff1…

23种设计模式(六)——装饰模式【单一职责】

文章目录意图什么时候使用装饰真实世界类比装饰模式的实现装饰模式的优缺点亦称&#xff1a; 装饰者模式、装饰器模式、Wrapper、Decorator 意图 装饰者模式&#xff08;Decorator Pattern&#xff09;允许向一个现有的对象扩展新的功能&#xff0c;同时不改变其结构。主要解决…

Allegro如何快速找到差分不耦合处操作指导

Allegro如何快速找到差分不耦合处操作指导 做PCB设计的时候,需要检查差分对不耦合的地方,让差分不耦合的地方高亮出来 具体操作如下 用Allegro172版本打开pcb,选择View选择Vision Manager

抖快社交,变道求生

配图来自Canva可画 抖音、快手再一次杀回了社交市场。 2022年12月底&#xff0c;快手App store版本更新&#xff0c;在原有的快手热榜、朋友动态、快手拍摄的基础上&#xff0c;新增亲密贴贴、快手直播等新锁屏组件&#xff0c;通过强化产品的交互功能&#xff0c;增强用户的…

针对游戏开发CG制作的搬砖人员的资源搜索技巧分享—【持续补充篇】

一.常用搜索技巧分享 1.视频参考类(bilibili,youtube,常用的视频官网,其实可以满足了,再不行就在百度/Google搜一下) 2.教程和代码类 github Bootstrap Well magebox realtimevfx

Python项目(Django框架)天天生鲜在CentOS7.9搭建运行

CentOS安装python3 为方便管理&#xff0c;在CentOS桌面创建一个文件夹&#xff0c;将软件包下载到这里&#xff0c;右键--在终端打开 安装python3.9.7 : wget https://www.python.org/ftp/python/3.9.7/Python-3.9.7.tgz &#xff08;命令前的sudo如果是root用户可以去掉&…

深度学习目标检测基础_sigmoid和softmax函数

文章目录sigmoid和softmaxsigmoid函数softmax函数总结sigmoid和softmax sigmoid和softmax都是分类函数&#xff0c;他们的区别如下 sigmoid函数 Sigmoid 多标签分类问题多个正确答案非独占输出&#xff08;例如胸部X光检查、住院&#xff09;。构建分类器&#xff0c;解决有…

威纶通触摸屏配方功能的使用方法示例

威纶通触摸屏配方功能的使用方法示例 本次和大家分享通过触摸屏内部指针+偏移地址+控制元件实现配方功能的具体方法, 另外以前给大家分享过利用宏指令实现配方功能的方法,具体可参考以下链接中的内容: 威纶通触摸屏的配方功能具体使用方法介绍(宏指令写入PLC) 如下图所示…

Dubbo 服务引用

Dubbo 服务引用 0. 概述 Dubbo 服务引用的时机有两个&#xff0c;第一个是在 Spring 容器调用 ReferenceBean 的 afterPropertiesSet 方法时引用服务&#xff0c;第二个是在 ReferenceBean 对应的服务被注入到其他类中时引用。这两个引用服务的时机区别在于&#xff0c;第一个…

锅圈的新消费“第三条路”走得通吗?

文|螳螂观察 作者|kinki 临近春节&#xff0c;线下餐饮行业在经历了三年的寒冬之后&#xff0c;相信会在今年迎来一个“暖春”。不过&#xff0c;年夜饭一直存在“一桌难求”的现象&#xff0c;结合疫情因素&#xff0c;相信今年仍有不少消费会选择在家用餐。 因此&#xff…

生成随机用户名的测试数据

大家好&#xff0c;才是真的好。 记得我们以前讲过一篇《自动批量生成Notes应用测试数据&#xff01;》&#xff0c;利用Java自动生成大批量的测试数据&#xff0c;今天我们介绍使用LotusScript代码来实现自动生成随机数据&#xff0c;主要是随机的用户名。 我们的方法很简单…

〖百宝书-思维锻炼②〗——知识是人类的供需和营养品

大家好&#xff0c;我是涵子&#xff0c;今天我们来聊聊知识。 &#x1f4ac; 人生格言&#xff1a;Stay foolish, stay kind.&#x1f4ac; &#x1f4eb; 如果文章知识点有错误的地方&#xff0c;请指正&#xff01;和大家一起学习&#xff0c;一起进步&#x1f440; &#x…

Feed 流系统

差不多十年前&#xff0c;随着功能机的淘汰和智能机的普及&#xff0c;互联网开始进入移动互联网时代&#xff0c;最具代表性的产品就是微博、微信&#xff0c;以及后来的今日头条、快手等。这些移动互联网时代的新产品在过去几年间借着智能手机的风高速成长。 这些产品都是Fee…

VueJs中的shallowRef与shallowReactive的使用比较

01shallowRef()函数如果传入基本数据类型,那么shallowRef与ref的作用基本没有什么区别,也就是浅层的ref的内部值将会原样的存储和暴露,并不会被深层递归地转为响应式但如果是对象的话,那么就存在区别了的,shallowRef不处理对象类型的数据其实,它就是只处理基本数据类型的响应式…

从 JMM 透析 volatile 与 synchronized 原理

在面试、并发编程、一些开源框架中总是会遇到 volatile 与 synchronized 。synchronized 如何保证并发安全&#xff1f;volatile 语义的内存可见性指的是什么&#xff1f;这其中又跟 JMM 有什么关系&#xff0c;在并发编程中 JMM 的作用是什么&#xff0c;为什么需要 JMM&#…

信用评分分卡简介introduction of credit score card

背景 随着金融科技初创企业的兴起&#xff0c;过去 5 年中出现了许多新的消费信贷机构&#xff0c;与传统银行展开竞争。他们通常瞄准银行认为规模太小或因金融危机期间发生的后期损失而不得不削减贷款的细分市场。通俗的讲就是消费金融公司瞄准了银行的次贷市场。 这些新的消…

修改Pom文件需要注意的问题

1.从远程nuxaus拉不回来个别包该如何解决 进入仓库目录下&#xff0c;把该包的目录删除了&#xff0c;重新拉 rm -r 包目录 如果还是不行&#xff0c;可能是idea内存不够&#xff0c;尝试关闭暂时不用但是已经打开的项目&#xff0c;减少内存使用&#xff0c;删除包目录重试…

肌电信号采集电路分析

最近在开发肌电信号的采集&#xff0c;表面肌电信号是非常微弱的生物信号&#xff0c;正常人体表面肌电信号赋值为0--1.5mV&#xff0c;主要能量频段集中在10--150Hz。电路主要是根据原始信号&#xff0c;设计相应的放大电路、滤波电路&#xff0c;下面直接放原理图说明。一级放…

生物素点击试剂1255942-07-4,DBCO-PEG4-Biotin,生物素-PEG4-二苯基环辛炔

中英文别名&#xff1a;CAS号&#xff1a;1255942-07-4| 英文名&#xff1a;DBCO-PEG4-Biotin |中文名&#xff1a;二苯基环辛炔-PEG4-生物素&#xff0c;二苯基环辛炔-四聚乙 二醇-生物素物理参数&#xff1a;CASNumber&#xff1a;1255942-07-4Molecular formula&#xff1a;…

2023的网安玩家,会和布洛芬退烧一样“凉”得快吗?

2021年&#xff0c;《数据安全法》《网络安全产业高质量发展三年行动计划》《个人信息保护法》《网络产品安全漏洞管理规定》等政策法规扎堆发布&#xff0c;二级市场网安公司市值一度起飞&#xff0c;网络安全行业如日中天&#xff0c;业内大佬纷纷感慨总算熬出了头&#xff0…