2024年1月

前两天群里有人问到这个问题,我想着在网上找个教程发给他,没想到这玩意还挺新鲜?网上基本上找不到实质性的回答...

关于这个问题,其实挺简单的,微信的版本号其实就写在注册表中,读取它就完事了~

打开注册列表

找到【计算机\HKEY_CURRENT_USER\Software\Tencent\WeChat】,就看的到版本号了

他这里的value,是一个DWORD类型:0x63090217(1661534743)
我现在的微信版本是3.9.2.23

看到这里大家应该看出点端倪了
把最十六进制的高位替换成0,也就是【0x03090217】
这样就更明显了,把中间的0替换为【.】这不就是3.9.2了嘛

但是最后的17跟23还是对应不上,那它会是什么呢?
打开计算器看看:十六进制的17 不就是 十进制的23嘛
那么按照这种方法,前面加上个【.】就是版本号了

画个图比较好理解

读到这里,你要是想着通过切割字符串的方式来提取版本号的话,那也太LOW了吧...
敲黑板!!!看下面!!!

版本号的十六进制:0x63090217
去掉最高位:0x03090217
二进制是:【0000 0011 0000 1001 0000 0010 0001 0666666】

它的类型是DWORD,也就是4字节,每个字节有8位,所以每8位进行一次计算
从最高位开始:右移到低8位
看下面的分析【注意标红的字体】

十六进制:0x
03
090217
右移24位后:0000 0000 0000 0000 0000 0000
0000 0011
取低8位的十进制:3

十六进制:0x03
09
0217
右移16位后:0000 0000 0000 0000 0000 0011
0000 1001
取低8位的十进制:9

十六进制:0x0309
02
17
右移8位后:0000 0000 0000 0011 0000 1001
0000 0010
取低8位的十进制:2

十六进制:0x030902
17
右移0位后:0000 0011 0000 1001 0000 0010
0001 0666666
取低8位的十进制:23

到此就完成了版本号的获取,完整Python代码如下(直接复制粘贴运行就完事了):

import winreg


def get_wx_version():
    """获取微信版本号"""

    try:
        # 打开注册表上下文
        with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Tencent\WeChat", 0, winreg.KEY_READ) as key:
            # 读取版本号:10进制
            int_version = winreg.QueryValueEx(key, "Version")[0]

            # 转16进制字符串
            hex_version = hex(int_version)

            # 去掉0x
            hex_str = hex_version[2:]

            # 把第一个字符(最高位)替换为 0
            new_hex_str = "0" + hex_str[1:]

            # 转回10进制
            new_hex_num = int(new_hex_str, 16)

            # 按位还原版本号
            major = (new_hex_num >> 24) & 0xFF
            minor = (new_hex_num >> 16) & 0xFF
            patch = (new_hex_num >> 8) & 0xFF
            build = (new_hex_num >> 0) & 0xFF

            # 拼接版本号
            wx_version = "{}.{}.{}.{}".format(major, minor, patch, build)
            print(wx_version)
    except Exception as e:
        print("打开注册表失败:{}".format(e))
        return None


if __name__ == '__main__':
    get_wx_version()

补充:
有小伙伴私我:为什么要加个【& 0xFF】,是什么意思...
因为我要取低8位啊,与运算同真才真 0xFF=6666661 6666661

更多相关文章:
PC微信逆向分析之《获取登录二维码》
PC微信逆向分析之《刷新登录二维码》
PC微信逆向分析之《收发消息》

文章转载自:
https://blog.csdn.net/weixin_44678558/article/details/135853762

Nginx被它打败了?

我们经常使用Nginx作为我们系统的代理,通常情况下是完全够用的,但是有时候业务的复杂性,我们也需要使用在网关中去代理中做一些业务,这个时候Nginx也可以使用lua脚本扩展,但是并非所有人都会lua,比如我这个.NET开发,这个时候我看到了微软开发的YARP这个SDK,果然还是微软给力,将这么牛逼的东西封装成SDK,供我们使用,下面我们对于
Nginx

Yarp
进行简单的性能测试。

部署测试环境

我们提供nginx环境,proxy_pass代理的是我们的一个Gitea。

server {
    listen 80;
    server_name localhost;
    location / {
	    add_header 'Access-Control-Allow-Origin' 'http://localhost:8088';
	    add_header 'Cache-Control' 'public, max-age=604800';
	    add_header 'Access-Control-Allow-Credentials' 'true';
	    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
	    proxy_pass http://token-ai.cn:1001/; 
	  
    }
}

docker-compose.yml文件,我们使用默认的nginx镜像,然后映射我们的端口,然后执行compose文件

docker compose up -d
version: "2.1"
services:
  nginx:
    image: nginx
    restart: always
    ports:
      - 12001:80
    volumes:
      - ./conf.d/:/etc/nginx/conf.d/

下面我们构建我们的yarp代码,我们使用.NET 8

Progarm.cs
文件

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
await app.RunAsync();

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ReverseProxy": {
    "Routes": {
      "route1" : {
        "ClusterId": "cluster1",
        "Match": {
          "Path": "{**catch-all}"
        }
      }
    },
    "Clusters": {
      "cluster1": {
        "Destinations": {
          "destination1": {
            "Address": "http://token-ai.cn:1001/"
          }
        }
      }
    }
  }
}

然后我们构建我们的程序,依赖框架构建(服务器需要安装.NET 8)

然后放置服务器当中,在执行之前给与可执行权限

chmod +x Yarp

然后执行我们的程序

./Yarp Urls="http://*:12002"

压测结果

nginx和yarp部署在同服务器下

使用压测工具
ApiPost
(压测工具是基于GO实现的)

压测条件: 并发数
100

按压测时长:
10s

服务器配置

Nginx压测:

Yarp压测

原接口压测:

性能测试结果,由于nginx使用docker运行对比yarp确实基本上是打平,对于原接口压测性能俩个性能损耗基本忽略不计。

Kestrel相关资料

https://github.com/xljiulang/KestrelApp

https://www.cnblogs.com/kewei/p/16955086.html

根据大佬的描述Kestrel跟nginx是一样在传输层的。

来着token的分享

技术交流群:737776595

概述

众所周知,SpringBoot最腻害的地方就是容器,开发人员的日常工作就是编写bean,并由框架扫描存到容器里面,当程序跑起来的时候,各种bean协同工作完成了软件功能。

那么容器是什么呢?

从概念层面来讲,容器是一个池子;从物理层面来讲,容器是一个内存块。

SpringBoot中默认是以单例形式装载bean的,所以
大多数
情况下,我们创建的bean对象在程序启动的时候都会被装载到
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry-singletonObjects
中,这是一个
ConcurrentHashMap

一方面我们需要关注容器中的bean能够提供哪些功能,这是程序工作的细粒度单元,是提供软件功能的基石;另外一方面我们也需要关注bean的装配,处理好它们的依赖关系才能让它们协同工作,共同完成造物主(
码农
)安排的任务。

本文总结了在SpringBoot中常用的bean装配方法:

  • profile
  • conditional
  • ConditionalOn

Profile

profile 顾名思义,就是环境相关的装配条件。常见的如静态资源的存储,开发环境我们期望存储到硬盘,生产环境可能会存到MinIO中,那么此时可以通过profile根据环境的不同装配不同的文件存储处理bean到容器中,消费者无需关心当前什么环境,直接从容器中获取文件存储处理bean并使用即可。

如下示例代码:


import com.ramble.springbootzgnetsdk.service.DiskResourceServiceImpl;
import com.ramble.springbootzgnetsdk.service.MinIoResourceServiceImpl;
import com.ramble.springbootzgnetsdk.service.ResourceService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

/**
 * Project     springboot-zgnetsdk
 * Package     com.ramble.springbootzgnetsdk.config
 * Class       ResourceServiceConfig
 * date        2024/1/26 10:49
 * author      cml
 * Email       liangchen_beijing@163.com
 * Description
 */


@Configuration
public class ResourceServiceConfig {

    @Profile("dev")
    @Bean
    public ResourceService initDiskResourceService() {
        return new DiskResourceServiceImpl();
    }


    @Profile("prod")
    @Bean
    public ResourceService initMinIoResourceService() {
        return new MinIoResourceServiceImpl();
    }
}

当前所属环境通过配置文件中的
spring.profiles.active
配置项约束

  • @Profile("dev"):当active的值为dev的时候,此注解注释的方法才会生效,结合@bean注解,方法的返回对象将被注入到容器中。
  • @Profile("!dev"):也可使用
    !
    来表示取反的操作,即不是dev的环境此注解注释的方法才生效

Conditional

Conditional 位于
org.springframework.context.annotation
中,常常会结合Condition这个接口来完成条件装配,具体来说,Condition的match方法负责编写装配条件,返回true则表示允许装载,否则就不会装载。

假设我们有这样一个需求,程序需要和海康网络设备SDK做集成,那么我们可以在配置文件中通过一个配置项来做开关,
hikvision.enable
,若此开关打开则装配海康网络设备SDK到容器中,方便其它开发人员使用,否则就不装配。

示例代码如下:


import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

import java.util.Objects;

/**
 * Project     springboot-hcnetsdk
 * Package     com.ramble.springboothcnetsdk.condition
 * Class       HikvisionSdkInitCondition
 *
 * @author 
 * Email
 * Description   海康sdk初始化条件装配
 * @date 2024/1/10 13:19
 */
public class HikvisionSdkInitCondition implements Condition {


    /**
     * 装配规则,根据配置文件中hikvision.enable的值,为true或者1则装配
     *
     * @param context  the condition context
     * @param metadata the metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
     *                 or {@link org.springframework.core.type.MethodMetadata method} being checked
     * @return 返回true,允许初始化;否则不允许
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String property = context.getEnvironment().getProperty("hikvision.enable");
        if (Objects.nonNull(property)) {
            return property.contains("true") || property.contains("1");
        } else {
            return false;
        }
    }
}

HikvisionSdkInitCondition ,首先定义一个条件类,此类继承Condition,通过重写matches方法来处理装配条件。

  • context:通过context对象的getEnvironment获取配置文件中的hikvision.enable
    配置项
  • 若hikvision.enable值为true或者1,表示允许初始化,即允许装配到容器中

import com.ramble.springboothcnetsdk.condition.HikvisionSdkInitCondition;
import com.ramble.springboothcnetsdk.support.HikvisionSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

/**
 * Project     springboot-hcnetsdk
 * Package     com.ramble.springboothcnetsdk.config
 * Class       SdkInitConfig
 *
 * @author 
 * Email        liangchen_beijing@163.com
 * Description
 * @date 2024/1/10 13:27
 */

@Configuration
public class SdkInitConfig {

    /**
     * 初始化海康sdk。
     * 若满足Conditional注解,则向容器中注入 HikvisionSupport
     * @return
     */
    @Conditional(HikvisionSdkInitCondition.class)
    @Bean
    HikvisionSupport initHikvisionSdk() {
        return new HikvisionSupport();
    }
}


SdkInitConfig,定义一个sdk初始化配置类,通过此类将sdk装入容器中。

  • @Configuration:添加此注解,让容器可以扫描到此配置类
  • @Conditional(HikvisionSdkInitCondition.class):Conditional注解需要一个装配条件,当条件允许的时候就执行此方法,而条件具体逻辑已经在HikvisionSdkInitCondition中编写了
  • @Bean:将方法返回值注入容器

@Autowired(required = false)
private HikvisionSupport hikvisionSupport;

在消费的地方注入的时候,必须添加
required = false
,否则编译无法通过。

ConditionalOn

ConditionalOn是一个总称,其中包含了很多具体的注解,常用的如下:

  • @ConditionalOnBean:当容器中有指定Bean的条件下进行实例化。
  • @ConditionalOnMissingBean:当容器里没有指定Bean的条件下进行实例化。
  • @ConditionalOnClass:当classpath类路径下有指定类的条件下进行实例化。
  • @ConditionalOnMissingClass:当类路径下没有指定类的条件下进行实例化。
  • @ConditionalOnWebApplication:当项目是一个Web项目时进行实例化。
  • @ConditionalOnNotWebApplication:当项目不是一个Web项目时进行实例化。
  • @ConditionalOnProperty:当指定的属性有指定的值时进行实例化。
  • @ConditionalOnExpression:基于SpEL表达式的条件判断。
  • @ConditionalOnJava:当JVM版本为指定的版本范围时触发实例化。
  • @ConditionalOnResource:当类路径下有指定的资源时触发实例化。
  • @ConditionalOnJndi:在JNDI存在的条件下触发实例化。
  • @ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个,或者有多个但是指定了首选的Bean时触发实例化。

ConditionalOnProperty

ConditionalOnProperty位于
org.springframework.boot.autoconfigure.condition
中,表示当配置文件中存在某配置项,并且该项值为具体某值的时候才装配bean。

还是以程序需要和第三方网络设备SDK做集成的需求举例说明。

示例代码如下:


import com.ramble.springbootzgnetsdk.support.ZgSupport;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

/**
 * Project     springboot-zgnetsdk
 * Package     com.ramble.springbootzgnetsdk.config
 * Class       SdkInitConfig
 * date        2024/1/24 10:17
 * author      
 * Email       liangchen_beijing@163.com
 * Description
 */


@Configuration
@ConditionalOnProperty(value = "sdk.enable", havingValue = "true")
public class SdkInitConfig {


    @Bean    
    @ConditionalOnMissingBean
    ZgSupport initZgSdk() {
        return new ZgSupport();
    }


}

  • @Configuration:添加此注解,让容器可以扫描到此配置类
  • @ConditionalOnProperty(value = "sdk.enable", havingValue = "true"):当配置文件中存在
    sdk.enable
    配置项,并且配置项值为
    true
    的时候,才会执行此配置类
  • @Bean:将方法返回值注入容器
  • @ConditionalOnMissingBean:确保此bean不会重复注入

近来在帮忙处理一个IOS端,指纹/面容登录的需求。 独立的原生IOS开发人员,已经被优化掉了,我是革命一块儿砖,哪里需要哪里搬,-_-|| 。在此,对期间遇到的一些实践问题,做一个梳理备忘,也希望可以给其他产品及码农提供参考。

本文主要侧重于,整体的移动端指纹/面容实现用户登录的解决方案逻辑的概述,并不叙述指纹识别技术原理, 或者API具体编码过程等具体技术细节。

一、移动端指纹识别API能力

虽然实际参与开发的是IOS端,但从最终安卓iOS都能完整实现相关需求的角度推测, 安卓也应当具有相同的 API接口能力。

主要的接口能力应该能支持实现:

1. 判断设备是否支持/开启了指纹识别功能。

2.判断指纹数据是否发生了变化。

3.进行指纹验证过程,验证用户指纹。

用途上来说:

1 比较好理解, 设备是否已启用了指纹识别功能。

3 同样好理解,通俗的说,就是弹出个框,提示用户按指纹,识别成功,失败,取消之类,会有对应的回调通知给应用。

关于2 ,主要考虑的是一个安全场景。提供一种机制,让程序能够判断,设备的指纹识别数据是否发生了变化。具体来说,例如增加了几个指纹, 删除了一些指纹等。安全角度来说,可能是设备更换/增加了一个新的使用用户。应用应该考虑这个场景对其本身账号安全的影响。

一些感觉应该可以,但实际可能并不支持的能力:

1 无法取得具体的指纹数据, 这属于用户隐私

2 无法取得指纹设置,验证的具体信息,例如设置了几个手指,用哪个手指验证成功之类。可能你会认为判断指纹数据变化会依赖于此,但起码在IOS的角度,系统只是提供了一个类似数据签名的机制,你只能比较签名是否发生了变化。而不能取得具体的设置变化信息。

一个重要的概念变化:

区别于账号,手机验证码等登录方式,指纹登录的一个重要概念变化在于,
指纹登录鉴权的是对设备有使用权的生物特征,这些特征可能属于一个或多个人,并不与应用的账号一一对应。

用户角度,具体来说,你的pad,设置了通过你和你老婆的指纹可以对设备进行指纹解锁。 你pad 上安装了一个银行app,里边存着你的私房钱,同时登录这个App后按照APP的提示,设置了指纹登录。以后就可以方便的使用指纹来登录这个APP,查看你的私房钱余额。很多人没有意识到,如此一来,你老婆也可以通过她的指纹,来登录这个应用,方便的查看你的余额了。

因为对于系统而言,你录入指纹在同一个当前系统用户时(通常是这样),系统是无法区别,是你的两个手指,还是一个是你的,一个是你老婆的。根据账号独占原则,系统的理解是, 这两个指纹是你的两根手指,都代表了你。

这个概念的变化,会需要一些app,基于此做出一些安全上和功能上的考虑。

二、简陋而基本的指纹登录流程

1. 使用账号/密码,鉴权方式登录应用,应用内激活指纹登录功能。

2. 应用记录了指纹登录功能的开启, 同时记录指纹登录对应的账号/密码于本地。

3. 登录界面调用API的指纹验证过程。验证通过则取出记录的账号/密码信息,进行自动登录。

上面的流程虽然简陋,忽略了很多问题。但是他体现了实现指纹登录的基本原理。

原理来说,
在启用指纹登录时,记录用户的日后可用于登录鉴权的凭证于本地(上述而言是密码),当用户交互通过指纹验证后,使用保存的鉴权凭证进行登录鉴权。

三、较完善的方案

前面简述的简陋流程,忽略遗留的问题。

1.如果用一个新的账号密码,登录系统后,也同样启用了指纹登录,第一个账号的指纹登录对应凭据会被覆盖,只能支持一个应用账号启用指纹。

2.如果账号修改了密码,指纹登录将会失效。

3.如果设备的指纹数据发生变化,设备更换了使用者,或者录入了新的指纹。没有做安全上的考虑。

4.其他安全考虑。启用指纹登录功能的身份核验,抵抗重放等。

完善如下:

1. 如前所述,指纹验证仅鉴权,使用者是否有权使用设备,与应用账号并非一一对应,因此一对多的情况必须人为选择。UI上可能是通过指纹验证后,显示一个开通指纹用户的列表,用户自行选择要登录哪一个。简陋的方案中描述的其实是限定了,指纹登录,只绑定最后一个开通的账号信息。

2. 指纹登录的凭据使用密码引起的耦合,因此要为指纹登录提供独立的鉴权凭证。并可能需要提供类似密码的对应配套管理机制,如重置,过期之类。具体实践,可以用UUID,由服务器生成进行发送之类。

3. 通常应对数据变化做出对应处理,禁止登录,并清空指纹凭证。可以使用API提供的对应机制能力进行判断。

4. 其他一些细节的安全考虑。通信要加密;启用指纹登录功能时,应当验证是否为账号拥有者(验证账号密码),以确保其知情;登录凭据可以配合挑战码签名使用,以避免被重放攻击。

拜了个拜~

微软在发布 Windows 11 系统的时候,发布过一个开源的 Emoji 表情
fluentui-emoji
。因为我经常需要里面的一些表情图片,在仓库一个个查找特别的不方便,所以我做了一个表情查看器,可以很方便的查看所有表情,同时可以定位到表情文件的位置。这套 fluentui-emoji 表情一共有 1545 个。

开源地址:
https://github.com/he55/EmojiViewer

功能实现

fluentui-emoji 下的 assets 文件夹下的每一个子文件夹对应一个 Emoji 表情文件夹,表情文件夹里面的
metadata.json
文件储存着 Emoji 表情的元数据。
3D
文件夹里面储存的是 256x256 的
png
图片,其他文件夹储存的是
svg
矢量图片。然后要做的就是遍历每一个文件夹,解析里面的元数据和图片文件

  • 资产文件夹结构

  • Emoji 表情结构

  • metadata.json
    文件结构

{
"cldr": "zany face",
"fromVersion": "5.0",
"glyph": "