JS 安全随机整数:Math.random() 还是 crypto.getRandomValues()

Math.random() 的问题 const randomInt = (num, offset) => Math.floor(Math.random() * num) + offset;Math.random() 生成的是伪随机数,不适合安全场景:某些 JS 引擎实现可预测 不能用于:token、验证码、session ID、抽奖、游戏反作弊用于 UI 动画、随机颜色、普通业务逻辑完全没问题,够快够简单。 安全随机整数 浏览器 export const randomInt = (min, max) => { const range = max - min + 1; const bytes = new Uint32Array(1); crypto.getRandomValues(bytes); return min + (bytes[0] % range); };有轻微的 modulo bias(取模偏差)——当 range 不能整除 0xFFFFFFFF 时,某些值出现概率略高。对大多数业务足够,抽奖/密码学场景用下面的拒绝采样版: export const randomInt = (min, max) => { const range = max - min + 1; if (range <= 0 || range > 0xFFFFFFFF) throw new Error("invalid range"); const maxUint = 0xFFFFFFFF; const limit = maxUint - (maxUint % range); // 去掉尾部偏差区间 const bytes = new Uint32Array(1); let rand; do { crypto.getRandomValues(bytes); rand = bytes[0]; } while (rand >= limit); // 拒绝偏差区间内的值 return min + (rand % range); };平均循环次数 < 2,性能影响可以忽略。 Node.js import crypto from "crypto";export const randomInt = (min, max) => { return crypto.randomInt(min, max + 1); // 内部已处理 modulo bias };Node.js 的 crypto.randomInt 直接用就行,自动处理了偏差问题。 API 设计建议 原来的写法 randomInt(num, offset) 语义模糊——不清楚 num 是最大值还是范围长度: // ❌ 不直观 randomInt(10, 5) // 5~14?还是 0~5 + 10 的偏移?改成 (min, max) 更清晰: // ✅ 直观 randomInt(5, 14) // [5, 14] 范围内均匀取整场景选择场景 推荐UI 动画、随机颜色 Math.random()随机数组元素、占位数据 Math.random()验证码、token crypto.getRandomValues()抽奖、游戏随机 crypto.getRandomValues() + 拒绝采样Node.js 服务端 crypto.randomInt()性能上 Math.random() 比 crypto.getRandomValues() 快一个数量级,安全要求不高时不必引入加密随机数的开销。

电商 ERP 商品 JSON 解析:类目 ID、规格模板与 SKU 明细

电商 ERP 接口返回的商品 JSON 通常有几个容易混淆的字段。 类目字段 "categoryId": 1000016, "leafCategoryId": 51000671, "categoryIds": ["1000016", "51000671"]categoryId:主类目(一级) leafCategoryId:叶子类目,即商品实际发布的最末级类目 categoryIds:完整类目路径数组实际使用的是 leafCategoryId,而不是 categoryId。 如果接口返回的 leafCategoryName 为 null,需要单独查询类目名称: SELECT * FROM category WHERE id = 51000671或调用类目接口: GET /category/get?id=51000671specifications 与 skuList 的区别 这是最常混淆的地方:字段 含义 用途specifications 规格定义模板 描述有哪些规格维度和可选值skuList 实际销售 SKU 每个 SKU 的具体价格、库存、规格组合specifications 示例 "specifications": [ { "name": "颜色", "values": [{"value": "米白色"}] }, { "name": "尺码", "values": [ {"value": "M", "note": "建议80-100斤内"}, {"value": "L", "note": "建议100-115斤内"}, {"value": "XL"}, {"value": "2XL"}, {"value": "3XL"}, {"value": "4XL"} ] } ]这是模板,说明商品有"颜色"和"尺码"两个规格维度,以及每个维度的可选值。 skuList 示例 "skuList": [ { "skuOuterId": "12644-米白色-M", "specValue": "米白色;M", "salesPrice": 59.8, "stock": 5000, "_skuid": "v_1776944461271_970-v_M_1776944482303_3644" } ]每个 SKU 对应一个规格组合,有独立的价格和库存。 skuOuterId 与 specValueskuOuterId:商家自定义编码,格式通常是 商品编号-颜色-尺码 specValue:用分号拼接的规格值,如 米白色;M _skuid:系统内部键,由各规格的 valueKey 拼接,用于唯一标识规格组合遍历所有 SKU 的写法 for (const item of products) { for (const sku of item.skuList || []) { console.log({ title: item.title, sku: sku.skuOuterId, spec: sku.specValue, price: sku.salesPrice, stock: sku.stock }); } }for item in products: for sku in item.get("skuList", []): print( item["title"], sku["skuOuterId"], sku["specValue"], sku["salesPrice"], sku["stock"] )核心字段速查字段 含义leafCategoryId 真正的叶子类目 IDspecifications 规格维度定义(模板)skuList 实际销售 SKU 列表specValue SKU 规格值拼接字符串skuOuterId 商家 SKU 编码stock 该 SKU 库存salesPrice 销售价marketPrice 划线价(原价展示)

Spring RestTemplate 发 HTTP 请求:常用姿势速查

Spring 里发 HTTP 请求最常见还是 RestTemplate。虽然官方已经推 WebClient,但存量项目里 RestTemplate 到处都是——写起来简单、同步阻塞、跟传统 Servlet 模型贴合。 注册 Bean @Configuration public class RestConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }注入用: @Autowired private RestTemplate restTemplate;或者构造器注入(推荐): private final RestTemplate rt; public UserService(RestTemplate rt) { this.rt = rt; }GET 最简单: String json = restTemplate.getForObject( "https://api.example.com/users/1", String.class);带 URL 参数(占位符按顺序填): String url = "https://api.example.com/users/{id}?type={type}"; String json = restTemplate.getForObject(url, String.class, 1, "vip");返回对象: @Data public class User { private Integer id; private String name; }User u = restTemplate.getForObject( "https://api.example.com/users/1", User.class);Jackson 会自动反序列化——User 里字段名和 JSON 对得上就行。 POST 发 JSON: Map<String, Object> body = new HashMap<>(); body.put("username", "admin"); body.put("password", "12345");String result = restTemplate.postForObject( "https://api.example.com/login", body, String.class);RestTemplate 会把 Map 序列化成 JSON 并设 Content-Type: application/json(前提是有 Jackson)。 表单: MultiValueMap<String, String> form = new LinkedMultiValueMap<>(); form.add("username", "admin"); form.add("password", "12345");HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);HttpEntity<MultiValueMap<String, String>> req = new HttpEntity<>(form, headers); String result = restTemplate.postForObject(url, req, String.class);自定义 Header 带 Token 的接口: HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Authorization", "Bearer " + token);Map<String, Object> body = new HashMap<>(); body.put("name", "test");HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);ResponseEntity<String> resp = restTemplate.exchange( "https://api.example.com/create", HttpMethod.POST, entity, String.class);if (resp.getStatusCode().is2xxSuccessful()) { System.out.println(resp.getBody()); }exchange 是最灵活的方法——可以自由选择 HTTP method、传 headers、拿完整的 ResponseEntity。 超时配置(生产必配) 默认没有超时。远端 hang 住就一直等: @Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(5_000); // 连接超时 factory.setReadTimeout(10_000); // 读超时 return new RestTemplate(factory); }更强大的选择:Apache HttpClient 5: @Bean public RestTemplate restTemplate() { RequestConfig config = RequestConfig.custom() .setConnectTimeout(Timeout.ofSeconds(5)) .setResponseTimeout(Timeout.ofSeconds(10)) .build(); CloseableHttpClient http = HttpClients.custom() .setDefaultRequestConfig(config) .setMaxConnTotal(100) .setMaxConnPerRoute(20) .build(); HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(http); return new RestTemplate(factory); }带连接池、更好的性能、支持重试。 上传文件 FileSystemResource file = new FileSystemResource("/path/to/a.zip");MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); body.add("file", file); body.add("desc", "备份");HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA);HttpEntity<MultiValueMap<String, Object>> req = new HttpEntity<>(body, headers);String result = restTemplate.postForObject( "https://api.example.com/upload", req, String.class);忽略 SSL 自签证书内网服务: SSLContext ssl = SSLContexts.custom() .loadTrustMaterial((chain, authType) -> true) .build();CloseableHttpClient http = HttpClients.custom() .setConnectionManager( PoolingHttpClientConnectionManagerBuilder.create() .setSSLSocketFactory( SSLConnectionSocketFactoryBuilder.create() .setSslContext(ssl) .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) .build()) .build()) .build();生产上少这么干,会漏中间人。 Spring Boot 3 之后怎么办 Spring Framework 6 起 RestTemplate 进入维护模式——不再加新功能,但不会立刻删。新项目推荐:RestClient(Spring 6.1+)— 同步、fluent API,最平滑的替代 WebClient — 响应式(reactive),跟 Reactor 生态整合RestClient 示例: RestClient client = RestClient.create();User u = client.get() .uri("https://api.example.com/users/{id}", 1) .retrieve() .body(User.class);比 RestTemplate 好看得多。老项目不用急着换,新代码优先用 RestClient。 一句话总结 RestTemplate:GET 用 getForObject、POST 用 postForObject 或 exchange、复杂场景走 exchange 拿 ResponseEntity。生产必配超时和连接池。新项目改用 RestClient。

Spring RestTemplate 发起 HTTP 请求:GET、POST、自定义 Header 与超时配置

配置 Bean @Configuration public class RestConfig { @Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(5000); factory.setReadTimeout(10000); return new RestTemplate(factory); } }然后在 Service/Controller 中注入: @Autowired private RestTemplate restTemplate;GET 请求 简单 GET: String result = restTemplate.getForObject( "https://api.example.com/users/1", String.class );带路径参数: String result = restTemplate.getForObject( "https://api.example.com/users/{id}", String.class, 1 );返回实体类(需要 Jackson): @Data public class User { private Integer id; private String name; }User user = restTemplate.getForObject( "https://api.example.com/users/1", User.class );POST 请求 发送 JSON: Map<String, Object> body = new HashMap<>(); body.put("username", "admin"); body.put("password", "secret");String result = restTemplate.postForObject( "https://api.example.com/login", body, String.class );自定义 Header(带 Token) HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Authorization", "Bearer your-token");Map<String, Object> body = new HashMap<>(); body.put("name", "test");HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);ResponseEntity<String> response = restTemplate.exchange( "https://api.example.com/data", HttpMethod.POST, entity, String.class );String responseBody = response.getBody(); int statusCode = response.getStatusCode().value();GET 请求带 Header HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer your-token");HttpEntity<Void> entity = new HttpEntity<>(headers);ResponseEntity<String> response = restTemplate.exchange( "https://api.example.com/profile", HttpMethod.GET, entity, String.class );处理异常 try { String result = restTemplate.getForObject(url, String.class); } catch (HttpClientErrorException e) { // 4xx 错误 System.out.println("HTTP " + e.getStatusCode() + ": " + e.getResponseBodyAsString()); } catch (HttpServerErrorException e) { // 5xx 错误 System.out.println("Server error: " + e.getMessage()); } catch (ResourceAccessException e) { // 连接超时、网络错误 System.out.println("Connection error: " + e.getMessage()); }Spring Boot 3 注意 RestTemplate 已进入维护模式,Spring Boot 3 推荐使用 WebClient(响应式)或 Spring 6.1 引入的 RestClient(同步): // RestClient(Spring 6.1+) RestClient client = RestClient.create(); String result = client.get() .uri("https://api.example.com/users/1") .retrieve() .body(String.class);老项目继续使用 RestTemplate 完全没问题,新项目可以考虑迁移到 RestClient。

用 Cloudflare Worker 做多域名反代 + Error 1003 的解法

一台 VPS 上跑了七八个服务,每个都占一个端口。想用一个域名的多个子域直接映射过去,还不想暴露 IP。上 Nginx 有点重——一个 Cloudflare Worker 就够了。 Worker 代码 思路:读请求的 Host,从 map 里查目标 IP:端口,重新 fetch 转发过去。 export default { async fetch(request) { const host = request.headers.get("host"); const map = { "app.example.com": "http://origin.example.com:51118", "api.example.com": "http://origin.example.com:51117", "admin.example.com":"http://origin.example.com:51116", "shop.example.com": "http://origin.example.com:51115", }; const target = map[host]; if (!target) return new Response("Host not found", { status: 404 }); const url = new URL(request.url); const proxyUrl = target + url.pathname + url.search; const proxied = new Request(proxyUrl, { method: request.method, headers: request.headers, body: (request.method === "GET" || request.method === "HEAD") ? undefined : request.body, redirect: "manual", }); // 把 Host 改成目标域,避免源站按 Host 路由时错乱 proxied.headers.set("host", new URL(target).host); return fetch(proxied); }, };部署路径Cloudflare Dashboard → Workers & Pages → Create Worker 粘代码,Deploy Worker 详情 → Settings → Triggers → Routes 添加路由:app.example.com/*、api.example.com/* 等 对应的 DNS 记录必须 开小黄云(Proxied),否则 Worker 拦不到动态版本(不用维护 map) 如果子域名和端口有规律(比如 app → 51118、api → 51117),可以从子域直接算端口: const portMap = { app: 51118, api: 51117, admin: 51116, shop: 51115 };const sub = host.split(".")[0]; const port = portMap[sub]; if (!port) return new Response("Not found", { status: 404 });const target = `http://origin.example.com:${port}`;以后加子域只改 map 一处。 Error 1003:direct IP access not allowed 第一次绑域名很容易撞到: error code: 1003含义是"直接用 IP 访问 Cloudflare 不允许"。多半是因为你把域名 CNAME 到了 xxx.workers.dev,Cloudflare 就把这当成"客户端在裸 IP 访问"。 正解:不要 CNAME 到 workers.dev,走 Worker 路由。 DNS 那边:加一条 A 记录,IP 随便填(比如 192.0.2.1),一定要开小黄云:类型 名称 内容 代理A app 192.0.2.1 ProxiedA api 192.0.2.1 ProxiedWorker 那边:Settings → Triggers → Routes,加上 app.example.com/* 等。 之后请求走的是 Worker,A 记录里那个 IP 只是 Cloudflare 需要一条 DNS 记录而已,不会真的连过去。 需要 WebSocket / SSE Workers 支持 WebSocket 升级,fetch 里带上 Upgrade 头就能透传: if (request.headers.get("Upgrade") === "websocket") { return fetch(target, request); }SSE (text/event-stream) 靠 Response.body 流式返回就行,Worker 会自动保持长连接。 顺带的好处源站真实 IP 不会暴露给客户端 免费额度撑得住中小站(10 万请求/天) 加 WAF、限流、缓存全在 Cloudflare 控制台一键切换一句话总结 一个 Worker + 一张 host→port 表 = 多域名反代。踩到 Error 1003 别慌,DNS 走 A 记录 + 小黄云 + Worker Route,不要 CNAME 到 workers.dev。