聊聊electron代码加密

electron技术就是基于开源的chromium内核,实现了跨端的桌面端开发框架,所以其常用的业务开发脚本就是js代码了

那么在打包后,这些js代码虽然会经过压缩混淆编译等处理,但是仍然可以破解出来

所以就需要一种技术,把js代码转成二进制文件

如果可以的话,还需要对这些文件进行加密,绑定本地机器的mac地址,保证了激活唯一性

接下来,将聊聊怎么做这些事情

bytenode

通过bytenode将所有的js文件编译为字节码

1
2
3
4
5
const bytenode = window.require('bytenode');
await bytenode.compileFile({
filename: "指定js文件路径名称",
output: "输出jsc的目录"
});

webpack插件

用来修改按需加载资源的方式,不再加载js资源了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const pluginName = 'ChangeEntryWebpackPlugin';
const webpack = require('webpack');

const { Template } = webpack;

module.exports = class Plugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
const { mainTemplate } = compilation;
mainTemplate.hooks.requireEnsure.tap(pluginName, (source) => {
return Template.asString([
`if(!window.StartTest){`,
Template.indent([
// ${pluginName} jsc loaded`,
`var installedChunkData = installedChunks[chunkId];`,
`if (installedChunkData !== 0) {`,
Template.indent([
`if (installedChunkData) {`,
Template.indent([`promises.push(installedChunkData[2]);`]),
`} else {`,
Template.indent([
`var src = jsonpScriptSrc(chunkId);`,
`var name = src.replace("./scripts/","").replace(/\.js$/, '');`,
// 这里改为通过require jsc的方式加载每个bundle文件
// 这里的运行时在electron里面
`require("./"+ name + ".jsc");`,
`installedChunks[chunkId] = undefined;`
]),
`}`
]),
`}`,
`return Promise.all(promises);`
]),
`}`,
source
]);
});
});
}
};

步骤

  1. 先将前端单页项目,通过webpack编译后,生成js文件
  2. 启动供编译jsc的electron,在该electron里面,将上一步生成的js文件,编译为jsc的字节码文件。在electron里面编译的目的,是要保证版本一致性,因为该jsc文件,最终也会打包到最后的electron的产物里面
  3. 最后执行需要生成最终包的electron脚本
  4. 运行时加载方式伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 这里使用的是基于award框架的项目示例
import * as path from 'path';

if (document.location.protocol === 'file:') {
if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'uat') {
process.env.NODE_ENV = 'production';

window.onload = function () {
const award = document.getElementById('award');
if (award) {
// 初始化第三方依赖资源
const win = window as any;
win.React = require('react');
win.ReactDOM = require('react-dom');
win.moment = require('moment');
win.antd = require('antd');
win.__INITIAL_STATE__ = { award: {} };

// 加载核心的jsc资源
require('bytenode');
require(path.join(__dirname, 'assets/scripts/common.jsc'));
require(path.join(__dirname, 'assets/scripts/manifest.jsc'));
require(path.join(__dirname, 'assets/scripts/main.jsc'));
}
};
}
}

跨域漫谈

这里主要描述跨域的一些细节

set-cookie被阻止

浏览器在请求一个设置 session 的接口,如果和当前域名不一致,则出现上述这种情况

比如当前浏览器访问地址:http://www.example.com
如果请求接口:http://www.test.com/api,那么就会出现上述提示,这种提示就表明了该接口下的 cookie 没有正常写入,即 cookie 写入失败

解决方案: - session 配置设置如下
go session.Options(sessions.Options{MaxAge: 3600 * 12, Path: "/", HttpOnly: true, SameSite: http.SameSiteNoneMode, Secure: true}) - 请求接口使用https
即访问地址:https://www.test.com/api,那么 cookie 就可以正常写入了

但是内部系统,往往没有使用 https,如果还需要这种情况进行 cookie 写入,就可以考虑自动提交 form 表单

1
2
3
4
5
6
7
8
9
10
11
12
// form提交js代码说明
const div = document.createElement("div");
div.innerHTML = `
<form id="login-form" method="POST" action="http://www.test.com/api">
<input name="secret" type='hidden' value='${responseData.data}'/>
</form>
`;
document.body.appendChild(div);

const run = document.createElement("script");
run.innerHTML = `document.getElementById("login-form").submit();`;
document.body.appendChild(run);

这样,cookie 就正常在www.test.com这个域名下写入了,这个时候别忘了加个回调地址,再重定向回原来访问的页面

已经注入 cookie,跨域请求时,cookie 协带说明

通常,我们使用 fetch 函数请求接口,服务端解除跨域限制且支持 cookie 接收,一般这样设置

前端

1
2
3
4
fetch("http://www.test.com/api/list", {
// 支持携带cookie
credentials: "include",
});

后端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// c表示 *gin.Context

method := c.Request.Method
origin := c.Request.Header.Get("Origin")

if origin != "" {
c.Header("Access-Control-Allow-Origin", origin)
//服务器支持的所有跨域请求的方法
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE, PATCH")
// 允许跨域设置可以返回其他子段,可以自定义字段
c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token,Authorization,Token,ops_env,ops_version")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
// 允许客户端传递校验信息比如 cookie (重要)
c.Header("Access-Control-Allow-Credentials", "true")
}

//放行所有OPTIONS方法
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}

但是注意,这个时候,你的 cookie 并没有带到后端,因为浏览器访问的地址是http://www.example.com,但是请求的接口是http://www.test.com/api/list

也就说,http://www.test.com/api/这里的 cookie 写入,肯定是在.test.com这个域名下的,而由于当前访问的域名是.example.com,这个时候 cookie 就没有办法带过去

只有请求这种地址的跨域是可以的http://api.example.com/list

dockerfile编写细节

编译 + 部署

通常我们需要在一个指定的目标环境进行代码的编译,比如nodejs12golang:1.15等环境

那么我们就可以编写 dockerfile 来实现

前提:一个 dockerfile 有且只有一个基础镜像,比如在生产环境运行时,其实不需要大而全的 nodejs 包,只需要一个小而美的即可

node:12.19.0,基础镜像300多M
node:12.19.0-alpine,基础镜像不到30M

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 使用node:12.19.0镜像编译源代码
FROM node:12.19.0 AS build
WORKDIR /code

COPY . .

# 安装依赖,将TS代码编译为ES5代码
# 导出到当前目录 dist
RUN yarn
RUN yarn build

# 只安装生产环境运行需要的依赖
# 比如类似TypeScript、webpack、babel等依赖就不必安装了,减少目标镜像的体积
RUN rm -rf node_modules
RUN yarn install --production

# 使用最小的node镜像运行node服务
FROM node:12.19.0-alpine

WORKDIR /code

# 拷贝编译后的资源到当前镜像内
COPY --from=build /code/dist /code/server
COPY --from=build /code/node_modules /code/node_modules

CMD ["node", "server"]

动态的dockerfile

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 声明获取docker build时的动态传参
ARG VERSION

# 可以同时引入多个镜像包
FROM source1:$VERSION AS source1-build
FROM source2:$VERSION AS source2-build
FROM source3:$VERSION AS source3-build

FROM final:alpine

WORKDIR /code

# 如果这里需要VERSION,还需要再通过声明获取
ARG VERSION
RUN echo $VERSION

COPY --from=source1-build /code/dist /code/source1-build
COPY --from=source2-build /code/dist /code/source2-build
COPY --from=source3-build /code/dist /code/source3-build

通过该dockerfile,制作最终的镜像命令

1
2
version=1.0.0
docker build --build-arg VERSION=${version} -f dockerfile -t source:$version .