为什么有时候后端需要维护一张Refresh_Tokens表

正常 JWT 请求流程 sequenceDiagram participant User as 用户 participant Frontend as 前端(浏览器/App) participant Auth as 认证服务器 participant Backend as 后端API服务器 Note over User,Backend: 1. 登录阶段 User->>Frontend: 输入用户名/密码 Frontend->>Auth: POST /login (凭证) Auth->>Auth: 验证凭证 Auth-->>Frontend: 返回 access_token + refresh_token Frontend->>Frontend: 存储token(内存/localStorage) Frontend-->>User: 登录成功 Note over User,Backend: 2. 正常请求阶段 User->>Frontend: 请求受保护资源 Frontend->>Backend: GET /api/resourceAuthorization: Bearer access_token Backend->>Backend: 验证access_token签名和过期时间 Backend-->>Frontend: 返回请求的资源 Frontend-->>User: 展示数据 Note over User,Backend: 3. Access Token过期 User->>Frontend: 继续请求 Frontend->>Backend: GET /api/resourceAuthorization: Bearer access_token(已过期) Backend-->>Frontend: 401 Unauthorized (token过期) Frontend->>Frontend: 检测到401,触发刷新逻辑 Note over Frontend,Auth: 4. 刷新Token阶段 Frontend->>Auth: POST /refreshrefresh_token Auth->>Auth: 验证refresh_token Auth-->>Frontend: 返回新的 access_token(可选的新refresh_token) Frontend->>Frontend: 更新存储的access_token Frontend->>Backend: 重试原请求(新access_token) Backend-->>Frontend: 返回请求的资源 Frontend-->>User: 展示数据 为什么不能只靠JWT refresh token 发出去后后端不保存,会变成 不可控的长期通行证 无法主动踢人下线 无法注销单点设备 无法判断 token 是否被盗用(攻击者拿到了 refresh token 可以一直刷新 access token) 无法实现会话管理 使用 refresh tokens 表后的流程图 sequenceDiagram participant User as 用户 participant Frontend as 前端 participant Auth as 认证服务器 participant DB as 数据库(refresh_tokens表) participant Backend as 后端API服务器 Note over User,DB: 1. 登录阶段(签发token + 入库) User->>Frontend: 输入用户名/密码 Frontend->>Auth: POST /login Auth->>Auth: 验证凭证 Auth->>DB: 生成唯一refresh_token_idINSERT INTO refresh_tokens(user_id, token_hash, expires_at, device_info, ip_address, revoked) DB-->>Auth: 插入成功 Auth->>Auth: 生成access_token + refresh_token(refresh_token包含id引用) Auth-->>Frontend: 返回access_token + refresh_token Frontend->>Frontend: 存储token Frontend-->>User: 登录成功 Note over User,Backend: 2. 正常请求(与之前相同) User->>Frontend: 请求资源 Frontend->>Backend: GET /api/resourceAuthorization: Bearer access_token Backend->>Backend: 验证access_token Backend-->>Frontend: 返回资源 Note over Frontend,DB: 3. Access Token过期 → 刷新 Frontend->>Backend: GET /api/resource (access_token过期) Backend-->>Frontend: 401 Unauthorized Frontend->>Auth: POST /refreshrefresh_token Note over Auth,DB: 4. 刷新验证(多步校验) Auth->>Auth: 解析refresh_token,提取token_id Auth->>DB: SELECT * FROM refresh_tokensWHERE id = token_id DB-->>Auth: 返回记录 Auth->>Auth: 校验清单: Note over Auth: ✅ token_hash是否匹配✅ 是否过期 (expires_at > now())✅ 是否被撤销 (revoked = false)✅ 用户是否仍有效✅ 设备信息是否一致(可选) alt 校验全部通过 Auth->>DB: UPDATE refresh_tokensSET last_used_at = now(), last_used_ip = current_ipWHERE id = token_id Auth->>Auth: 生成新access_token(可选:延长refresh_token有效期) Auth-->>Frontend: 返回新access_token Frontend->>Frontend: 更新access_token Frontend->>Backend: 重试原请求(新token) Backend-->>Frontend: 返回资源 else 校验失败 Auth-->>Frontend: 401/403 (刷新失败) Frontend->>Frontend: 清除所有token Frontend->>User: 跳转登录页 end Note over User,DB: 5. 主动登出 User->>Frontend: 点击登出 Frontend->>Auth: POST /logoutrefresh_token Auth->>DB: UPDATE refresh_tokensSET revoked = trueWHERE id = token_id Auth-->>Frontend: 登出成功 Frontend->>Frontend: 清除本地token Frontend-->>User: 已登出 Note over User,DB: 6. 安全场景:密码修改 User->>Frontend: 修改密码 Frontend->>Auth: POST /change-password Auth->>DB: UPDATE refresh_tokensSET revoked = trueWHERE user_id = current_user_id Note over DB: 撤销该用户所有refresh_token(强制所有设备重新登录) Auth-->>Frontend: 密码修改成功 Frontend->>Frontend: 清除本地token Frontend-->>User: 请重新登录

2026-03-02 08:07:07 PM · 2 分钟
后端技术

微服务认证系统的架构梳理和面试应对 更新中 

技术栈:Spring Boot 3 · Spring Cloud Alibaba · OpenFeign · JWT · Gateway · Nacos 如何使用这份文档 面试讲项目,最忌讳从代码细节讲起。正确顺序是: 先讲它解决什么问题 → 再讲整体架构 → 然后按请求流程串一遍 → 最后应对深挖。 本文档按这个顺序组织。带 🎤 标记的引用框,是可以基本照着说的话术。 一、一句话概述(开场用) 🎤 我做了一套基于 Spring Cloud Alibaba 的微服务认证系统。它把「认证」这件事拆成三个角色:auth 服务负责签发令牌、网关负责统一校验令牌、各业务服务通过请求头拿到用户身份。整体用 JWT 做无状态认证,服务之间用 OpenFeign 通信,靠 Nacos 做服务发现。 关键词锚点(面试官会顺着这些追问,要准备好):无状态 JWT、网关统一鉴权、OpenFeign 远程调用、Nacos 服务发现、BCrypt 密码加密、职责分离。 二、它解决什么问题(讲动机) 背景:系统是微服务架构,有多个独立服务(auth、user-service、archive-service 等)。如果每个服务各自做一遍登录校验,会有两个问题: 重复:每个服务都写一遍鉴权逻辑,改规则要改很多处。 有状态难扩展:传统 session 存在单台服务器内存里,多实例之间不共享,扩容困难。 解决思路:「网关统一安检 + JWT 无状态令牌」。所有请求先经过网关校验令牌,业务服务不再重复鉴权;令牌本身自包含用户信息,验证只需用密钥本地验签,不依赖服务端存储。 🎤 类比机场:网关是安检口,严格查一次护照(验签);过了安检给你贴个写着身份的手环(请求头里的用户 id);后面登机口、免税店(各业务服务)只看手环,不再查护照。验签只做一次,业务服务零负担。 三、整体架构(讲骨架) 模块划分:项目是 Maven 多模块微服务,核心模块及职责如下。 模块 类型 职责 gateway 网关 所有请求入口;统一校验 JWT;按路由转发到后端服务 auth 认证服务 登录、注册;验密码;签发 JWT。自己不连数据库 user-service 业务服务 管理用户数据(tb_user 表的增删改查) archive-service 业务服务 档案业务;从请求头读取当前用户身份 api 契约模块 存放各服务的 Feign 接口声明 + 配套 DTO(被各服务依赖) common 基建底座 统一返回 Result、全局配置、ThreadLocal 用户上下文等 一个关键设计——auth 与 user-service 分离:auth 只管「认证动作」(验密码、发令牌),不碰数据库;它要用户数据时,通过 OpenFeign 远程调用 user-service 去查。user-service 只管「用户数据」,不掺和登录逻辑。 ...

2026-01-26 06:05:16 PM · 2 分钟

说一说登录这件麻烦事 更新中 

每个写过后端的人,迟早都要跟"登录"打一场硬仗。它看起来是个小功能——一个用户名、一个密码、一个按钮——但凡是真正做过的人都知道,这是整个系统里坑最深、改起来最痛、出事最致命的一块。 它麻烦,不是因为技术有多难,而是因为它卡在三方利益的正中间:用户想省事,产品想拉新,安全想严防死守。这三件事天然打架。你每往其中一边挪一寸,另外两边就开始喊疼。 这篇文章想把这件"麻烦事"摊开讲清楚:主流厂商现在都怎么做、每种做法烂在哪、未来可能怎么变,以及如果你今天就要动手,应该怎么落地。 一、先看战场:主流厂商现在都在用什么 如果你今天注册任何一个稍微正经点的 App,会发现登录方式早就不是"账号 + 密码"一条路了。现在的主流玩法大致可以分成五类。 1. 手机号 + 短信验证码 这是中国互联网的绝对统治者。点开微博、抖音、美团、拼多多,第一屏几乎清一色是"输入手机号 → 收验证码 → 进"。 它能赢,是因为它一次性解决了三个问题:手机号天然实名(运营商帮你做了 KYC)、不用记密码、注册和登录是同一个动作。对产品经理来说,这意味着注册转化率极高——少一个"设置密码"的步骤,就少漏一批用户。 代价是:你把身份的命脉,交给了运营商和短信通道。 2. 邮箱 + 密码 这是全球(尤其欧美)的默认范式。GitHub、Google、Notion、几乎所有 SaaS 都以邮箱为账号主体。 邮箱的好处是全球通用、跨国可用、不依赖运营商、可以承载找回流程。一个邮箱地址几乎就是你在互联网上的"主键"。坏处后面细说。 3. 第三方登录(OAuth / 社交登录) “用微信登录"“Sign in with Google"“Continue with Apple”——本质上是把身份认证外包给一个你已经信任的大平台。 它的杀手锏是:用户一次都不用输。点一下,授权,进去了。对开发者来说,你还省掉了自己存密码的风险(密码根本不经过你的服务器)。代价是你被绑在了平台生态上,而且用户的账号体系实际上不在你手里。 4. 魔法链接(Magic Link) 无密码的一种:你输入邮箱,系统给你发一封带一次性链接的邮件,点开即登录。Slack、Medium 早期都靠这个。 它把"记密码"这件事彻底删掉了,安全模型简单清晰。但它把整个登录体验的流畅度,押在了"邮件能不能秒到"上——而邮件这玩意,慢起来能慢到你怀疑人生。 5. Passkey / WebAuthn(通行密钥) 这是最新、也是各大厂商正在猛推的方向。Apple、Google、Microsoft 已经全面支持。它基于公私钥密码学,用你设备上的生物识别(指纹、Face ID)来完成认证,服务器端根本不存任何可被盗的密码。 这是目前公认"理论上最优"的方案,我们最后单独讲。 下面这张图,把这五种方式按"用户省心程度"和"安全强度"两个维度摆一摆,你能直观看到它们各自的生态位: graph TD A["登录方式光谱"] --> B["短信验证码省心高 / 安全中"] A --> C["邮箱密码省心低 / 安全中"] A --> D["第三方登录省心高 / 安全中高"] A --> E["魔法链接省心中 / 安全中"] A --> F["Passkey省心高 / 安全高"] B --> B1["依赖运营商换号即失联"] C --> C1["密码要记容易被撞库"] D --> D1["绑死大平台账号不在自己手里"] E --> E1["押注邮件时效慢到怀疑人生"] F --> F1["体验最好但迁移设备是痛点"] style A fill:#1a1a2e,color:#fff style F fill:#16213e,color:#7fff7f 二、麻烦的本质:每一种方式都在某处偷偷塌方 上面每种方式听起来都还行,但它们都有一个藏在水面下的塌方点。用户平时感觉不到,一旦踩中,体验直接归零。 ...

2025-12-26 03:56:40 PM · 2 分钟

Spring Boot 常用代码片段 更新中 

通用 依赖管理 父 pom.xml 固定三版本管控 版本参考 <dependencyManagement> <dependencies> <!-- 1. SpringBoot 基础版本 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.5.0</version> <type>pom</type> <scope>import</scope> </dependency> <!-- 2. Spring Cloud 云原生基础 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2025.0.0</version> <type>pom</type> <scope>import</scope> </dependency> <!-- 3. Spring Cloud Alibaba 阿里全套依赖核心(关键!) --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2025.0.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> 启动类 @MapperScan("asia.liminality.user.mapper") @SpringBootApplication @Slf4j public class UserApplication { public static void main(String[] args) throws UnknownHostException { ConfigurableApplicationContext app = SpringApplication.run(UserApplication.class, args); Environment env = app.getEnvironment(); String protocol = "http"; if (env.getProperty("server.ssl.key-store") != null) { protocol = "https"; } log.info("--/\n---------------------------------------------------------------------------------------\n\t" + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\t{}://localhost:{}\n\t" + "External: \t{}://{}:{}\n\t" + "Profile(s): \t{}" + "\n---------------------------------------------------------------------------------------", env.getProperty("spring.application.name"), protocol, env.getProperty("server.port"), protocol, InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port"), env.getActiveProfiles()); } } domain DTO Result import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class Result<T> { private int code; private String message; private T data; public static <T> Result<T> success(T data) { return new Result<>(200, "success", data); } public static <T> Result<T> success() { return new Result<>(200, "success", null); } public static <T> Result<T> failure(String message) { return new Result<>(500, message, null); } public static <T> Result<T> failure(int code, String message) { return new Result<>(code, message, null); } public static <T> Result<T> failure(String message, T data) { return new Result<>(500, message, data); } } Spring Cloud Gateway AuthGlobalFilter @Slf4j @RequiredArgsConstructor @Component public class AuthGlobalFilter implements GlobalFilter, Ordered { private final JwtUtil jwtUtil; private static final List<String> WHITE_LIST = List.of( "/auth/user/login", "/auth/user/register" ); // 判断是否在白名单内 private boolean isWhiteList(String path){ return WHITE_LIST.stream().anyMatch(path::startsWith); } private Mono<Void> reject(ServerWebExchange exchange){ exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String path = request.getPath().toString(); if (isWhiteList(path)) { return chain.filter(exchange); } String token = request.getHeaders().getFirst("Authorization"); // 令牌为空,拒绝访问 if( token == null || token.isEmpty() ){ return reject( exchange ); } // 验证令牌 try { Integer userId = jwtUtil.parseToken(token); // TODO userId 塞入请求头 log.info("🚪 塞入请求头"); } catch (Exception e) { return reject(exchange); } return chain.filter( exchange ); } // 过滤器优先级,越小越靠前 @Override public int getOrder() { return -1; } }

2025-11-26 09:51:37 AM · 2 分钟

PostgreSQL和java的LocalDateTime不兼容的问题 更新中 

Java 的 LocalDateTime 和 PostgreSQL 的时间类型,“说的不是同一种时间”。 先搞清楚 PostgreSQL 的两种时间类型 TIMESTAMP → 不带时区,就是个裸时间 "2026-06-25 10:00:00" TIMESTAMPTZ → 带时区,内部存 UTC,查询时按会话时区转换 Java 这边 LocalDateTime → 没有时区概念,就是个裸时间 ZonedDateTime → 带时区 OffsetDateTime → 带偏移量(如 +08:00) Instant → UTC 时间戳 为什么会报错 PostgreSQL JDBC 驱动(特别是新版本 42.x+)对类型匹配非常严格: flowchart TD A[Java LocalDateTime] --> B[JDBC驱动] B --> C{PostgreSQL列类型} C -->|TIMESTAMP| D[✅ 可以匹配] C -->|TIMESTAMPTZ| E[❌ 类型不匹配报错] 你的列如果是 TIMESTAMPTZ(带时区),但 Java 传的是 LocalDateTime(无时区),驱动不知道该用哪个时区换算,就直接拒绝了。 常见的三种报错 Cannot convert LocalDateTime to TIMESTAMPTZ Bad value for type timestamp/date column is of type timestamp with time zone but expression is of type timestamp 解决方案 方案一:改 Java 类型(推荐) ...

2025-10-25 12:55:49 PM · 1 分钟
后端疑难杂症解答

Swagger2和Swagger3注解对比表 更新中 

在 Spring Boot 生态中,Swagger 2.0(通常使用 Foxfire 依赖)和 Swagger 3.0(通常使用 Springdoc-openapi 依赖,基于 OpenAPI 3 规范)的注解发生了很大变化。 以下是 Swagger 2.0 与 Swagger 3.0(OpenAPI 3)的常用注释完整对应表: 1. 核心注解对应表 功能描述 Swagger 2.0 注解 (io.swagger.annotations) Swagger 3.0 注解 (io.swagger.v3.oas.annotations) 备注说明 标记控制器类 @Api(tags = "用户接口") @Tag(name = "用户接口") 3.0 中移除了 description 属性,统一使用 name 标记接口方法 @ApiOperation(value = "获取用户") @Operation(summary = "获取用户") 3.0 中 value 变更为 summary 入参实体类 @ApiModel(value = "用户对象") @Schema(description = "用户对象") 3.0 极大简化,统一使用 @Schema 实体类属性 @ApiModelProperty(value = "姓名") @Schema(description = "姓名") 同上,合并为了 @Schema 忽略某个属性 @ApiModelProperty(hidden = true) @Schema(hidden = true) 忽略整个类/方法 @ApiIgnore @Hidden 用于不想暴露在文档中的接口或参数 2. 请求参数注解对应表 对于方法入参(如 URL 路径参数、Query 参数等),3.0 引入了更具结构化的配置: ...

2025-09-25 09:06:35 AM · 1 分钟
后端疑难杂症解答

程序员快速参考小手册 更新中 

有的命令一个月可能就用那么几次,不写手册里谁能记得住啊 AI 常用提示词 老师提示词 你现在是老师,教我这个问题。上课战略是“及时反馈,小步快走”。教学过程中注意: 1. 涉及代码、算法、线程、内存或调用链时,生成mermaid图像或者html+js(简单css即可)的互动网页; 2. 不要堆砌术语;让你认为大概率不知道某个术语含义时,先用简单语言解释。 学习路线提示词 请设计一条以“能实际使用”为目标的学习路线。 要求: 按依赖关系排序 区分必须掌握和进阶内容 避免学习低收益知识 每个阶段说明学到什么程度算合格 给出可以验证掌握程度的小项目或练习 不要简单给课程目录。 查漏补缺提示词 我已经学过这个主题。 请不要从头讲教材。 先帮我建立完整知识地图,然后判断: 我必须掌握什么 哪些知识容易漏掉 哪些知识经常被错误理解 哪些知识在实际工作中最重要 哪些属于低优先级细节 帮我找到知识盲区,而不是重复基础内容。 Claude Code # 安装状态栏 npx -y ccstatusline@latest VSCode 解决 code-runner Java Output 乱码 "code-runner.executorMap": { "javascript": "node", "java": "\"C:/Program Files/Java/jdk-17/bin/java.exe\" -Dfile.encoding=UTF-8", Windows 快速完成端口转发 netsh interface portproxy add v4tov4 ` listenaddress=0.0.0.0 ` listenport=2375 ` connectaddress=127.0.0.1 ` connectport=2375 refreshenv Install-Script Refresh-EnvironmentVariables -Force 查端口占用 杀进程 netstat -aon | findstr :8080 taskkill /PID 39656 /F 自定义常用命令别名 notepad $PROFILE function gacp { param ( [string]$msg = "update" ) git add . git commit -m $msg git push } # 删除当前文件夹下空文件夹 function rme { Get-ChildItem -Directory -Recurse | Where-Object { $_.GetFileSystemInfos().Count -eq 0 } | Remove-Item } 文件整理类常用PS脚本 移动子目录所有文件到当前目录 # randomize_filename.ps1 # Rename every file in the current folder to a unique random number (extension preserved). # Usage: place this script in the target folder and run .\randomize_filename.ps1 $currentDir = (Get-Location).Path $scriptPath = $PSCommandPath Write-Host ("Scanning: {0}" -f $currentDir) -ForegroundColor Cyan # Get all files in the current folder, but skip the script itself so we don't rename our own executable. $files = Get-ChildItem -LiteralPath $currentDir -File | Where-Object { $_.FullName -ne $scriptPath } if (-not $files) { Write-Host "No files to rename." -ForegroundColor Yellow return } Write-Host ("Found {0} file(s)" -f $files.Count) # Digit count scales with file count so random collisions stay rare. # Roughly: digits = ceil(log10(N^2)) -> birthday-paradox-safe for typical inputs. $count = $files.Count $digits = [Math]::Max(4, [int][Math]::Ceiling([Math]::Log10($count * $count))) $min = [int][Math]::Pow(10, $digits - 1) $max = [int][Math]::Pow(10, $digits) - 1 Write-Host ("Using {0}-digit numbers ({1}..{2})" -f $digits, $min, $max) # Pre-compute unique new names. The "used" set contains every current name on disk # AND every target name we've already picked, so collisions are impossible by construction. $used = @{} foreach ($file in $files) { $used[$file.Name] = $true } $renameMap = [ordered]@{} foreach ($file in $files) { do { $num = Get-Random -Minimum $min -Maximum ($max + 1) $numStr = $num.ToString().PadLeft($digits, '0') $newName = "$numStr$($file.Extension)" } while ($used.ContainsKey($newName)) $used[$newName] = $true $renameMap[$file.FullName] = $newName } # Apply renames. .NET Move() bypasses PowerShell wildcard parsing (safe for [ ] in names). $renamed = 0 $failed = 0 foreach ($source in $renameMap.Keys) { $newName = $renameMap[$source] $dest = Join-Path $currentDir $newName try { [System.IO.File]::Move($source, $dest) Write-Host (" {0} -> {1}" -f (Split-Path $source -Leaf), $newName) $renamed++ } catch { $failed++ Write-Warning (" Failed: {0} ({1})" -f $source, $_.Exception.Message) } } Write-Host ("`nDone: renamed {0}, failed {1}" -f $renamed, $failed) -ForegroundColor Green 将文件夹下所有文件进行随机重命名(带容灾恢复) # randomize_filename.ps1 # Rename every non-.ps1 file in the current folder to a unique random number (extension preserved). # Maintains restore_filenames.txt so the change is reversible. # Usage: place this script in the target folder and run .\randomize_filename.ps1 $currentDir = (Get-Location).Path $restoreFile = Join-Path $currentDir 'restore_filenames.txt' Write-Host ("Folder: {0}" -f $currentDir) -ForegroundColor Cyan # ----- Restore mode: if a restore map exists, offer to undo first ----- if (Test-Path -LiteralPath $restoreFile) { $answer = Read-Host ("restore_filenames.txt detected. Restore original names? [Y/N]") if ($answer -match '^[Yy]') { $lines = Get-Content -LiteralPath $restoreFile -Encoding UTF8 $restored = 0 $failed = 0 foreach ($line in $lines) { $trimmed = $line.Trim() if (-not $trimmed -or $trimmed.StartsWith('#')) { continue } $parts = $trimmed -split "`t", 2 if ($parts.Count -ne 2) { Write-Warning ("Skipping malformed line: {0}" -f $line) $failed++ continue } $original = $parts[0] $current = $parts[1] $currentPath = Join-Path $currentDir $current $originalPath = Join-Path $currentDir $original if (-not (Test-Path -LiteralPath $currentPath)) { Write-Warning ("Source missing: {0}" -f $currentPath) $failed++ continue } if (Test-Path -LiteralPath $originalPath) { Write-Warning ("Target already exists: {0}" -f $originalPath) $failed++ continue } try { [System.IO.File]::Move($currentPath, $originalPath) Write-Host (" {0} -> {1}" -f $current, $original) $restored++ } catch { Write-Warning ("Failed: {0} ({1})" -f $current, $_.Exception.Message) $failed++ } } if ($failed -eq 0) { Remove-Item -LiteralPath $restoreFile -Force Write-Host ("`nAll {0} file(s) restored. Removed restore_filenames.txt." -f $restored) -ForegroundColor Green } else { Write-Host ("`nPartial restore: {0} succeeded, {1} failed. Keeping restore_filenames.txt." -f $restored, $failed) -ForegroundColor Yellow } return } Write-Host "Proceeding with rename (restore file will be overwritten)." -ForegroundColor Yellow } # ----- Rename mode ----- # Collect files. Skip ALL .ps1 files and the restore file itself. $allFiles = Get-ChildItem -LiteralPath $currentDir -File $files = $allFiles | Where-Object { $_.Extension -ne '.ps1' -and $_.Name -ne 'restore_filenames.txt' } if (-not $files) { Write-Host "No files to rename." -ForegroundColor Yellow return } Write-Host ("Found {0} file(s) to rename (excluded .ps1 and restore_filenames.txt)" -f $files.Count) # Digit count scales with file count so random collisions stay rare. $count = $files.Count $digits = [Math]::Max(4, [int][Math]::Ceiling([Math]::Log10($count * $count))) $min = [int][Math]::Pow(10, $digits - 1) $max = [int][Math]::Pow(10, $digits) - 1 Write-Host ("Using {0}-digit numbers ({1}..{2})" -f $digits, $min, $max) # Pre-compute unique new names. The "used" set includes EVERY name on disk # (including .ps1 files and the restore file, which we are NOT renaming), # plus every target name we pick, so collisions are impossible by construction. $used = @{} foreach ($f in $allFiles) { $used[$f.Name] = $true } $renameMap = [ordered]@{} foreach ($file in $files) { do { $num = Get-Random -Minimum $min -Maximum ($max + 1) $numStr = $num.ToString().PadLeft($digits, '0') $newName = "$numStr$($file.Extension)" } while ($used.ContainsKey($newName)) $used[$newName] = $true $renameMap[$file.FullName] = $newName } # Write restore file BEFORE renaming, so even partial renames leave us with a recoverable map. # Tab-separated: original<TAB>new (filenames only, no paths) $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $headerLines = @( "# randomize_filename.ps1 restore map" "# created: $timestamp" "# directory: $currentDir" "# format: original`tnew (tab-separated)" "# comment lines starting with '#' are ignored on restore" ) $mappingLines = $renameMap.GetEnumerator() | ForEach-Object { $original = Split-Path $_.Key -Leaf "{0}`t{1}" -f $original, $_.Value } Set-Content -LiteralPath $restoreFile -Value ($headerLines + $mappingLines) -Encoding UTF8 # Apply renames. .NET Move() bypasses PowerShell wildcard parsing (safe for [ ] in names). $renamed = 0 $failed = 0 foreach ($source in $renameMap.Keys) { $newName = $renameMap[$source] $dest = Join-Path $currentDir $newName try { [System.IO.File]::Move($source, $dest) Write-Host (" {0} -> {1}" -f (Split-Path $source -Leaf), $newName) $renamed++ } catch { $failed++ Write-Warning (" Failed: {0} ({1})" -f $source, $_.Exception.Message) } } Write-Host ("`nDone: renamed {0}, failed {1}." -f $renamed, $failed) -ForegroundColor Green Write-Host ("Restore map saved to: {0}" -f $restoreFile) -ForegroundColor Cyan # randomize_filename.ps1 # Rename every non-.ps1 file in the current folder to a unique random number (extension preserved). # Maintains restore_filenames.txt so the change is reversible. # Usage: place this script in the target folder and run .\randomize_filename.ps1 $currentDir = (Get-Location).Path $restoreFile = Join-Path $currentDir 'restore_filenames.txt' Write-Host ("Folder: {0}" -f $currentDir) -ForegroundColor Cyan # ----- Restore mode: if a restore map exists, offer to undo first ----- if (Test-Path -LiteralPath $restoreFile) { $answer = Read-Host ("restore_filenames.txt detected. Restore original names? [Y/N]") if ($answer -match '^[Yy]') { $lines = Get-Content -LiteralPath $restoreFile -Encoding UTF8 $restored = 0 $failed = 0 foreach ($line in $lines) { $trimmed = $line.Trim() if (-not $trimmed -or $trimmed.StartsWith('#')) { continue } $parts = $trimmed -split "`t", 2 if ($parts.Count -ne 2) { Write-Warning ("Skipping malformed line: {0}" -f $line) $failed++ continue } $original = $parts[0] $current = $parts[1] $currentPath = Join-Path $currentDir $current $originalPath = Join-Path $currentDir $original if (-not (Test-Path -LiteralPath $currentPath)) { Write-Warning ("Source missing: {0}" -f $currentPath) $failed++ continue } if (Test-Path -LiteralPath $originalPath) { Write-Warning ("Target already exists: {0}" -f $originalPath) $failed++ continue } try { [System.IO.File]::Move($currentPath, $originalPath) Write-Host (" {0} -> {1}" -f $current, $original) $restored++ } catch { Write-Warning ("Failed: {0} ({1})" -f $current, $_.Exception.Message) $failed++ } } if ($failed -eq 0) { Remove-Item -LiteralPath $restoreFile -Force Write-Host ("`nAll {0} file(s) restored. Removed restore_filenames.txt." -f $restored) -ForegroundColor Green } else { Write-Host ("`nPartial restore: {0} succeeded, {1} failed. Keeping restore_filenames.txt." -f $restored, $failed) -ForegroundColor Yellow } return } Write-Host "Proceeding with rename (restore file will be overwritten)." -ForegroundColor Yellow } # ----- Rename mode ----- # Collect files. Skip ALL .ps1 files and the restore file itself. $allFiles = Get-ChildItem -LiteralPath $currentDir -File $files = $allFiles | Where-Object { $_.Extension -ne '.ps1' -and $_.Name -ne 'restore_filenames.txt' } if (-not $files) { Write-Host "No files to rename." -ForegroundColor Yellow return } Write-Host ("Found {0} file(s) to rename (excluded .ps1 and restore_filenames.txt)" -f $files.Count) # Digit count scales with file count so random collisions stay rare. $count = $files.Count $digits = [Math]::Max(4, [int][Math]::Ceiling([Math]::Log10($count * $count))) $min = [int][Math]::Pow(10, $digits - 1) $max = [int][Math]::Pow(10, $digits) - 1 Write-Host ("Using {0}-digit numbers ({1}..{2})" -f $digits, $min, $max) # Pre-compute unique new names. The "used" set includes EVERY name on disk # (including .ps1 files and the restore file, which we are NOT renaming), # plus every target name we pick, so collisions are impossible by construction. $used = @{} foreach ($f in $allFiles) { $used[$f.Name] = $true } $renameMap = [ordered]@{} foreach ($file in $files) { do { $num = Get-Random -Minimum $min -Maximum ($max + 1) $numStr = $num.ToString().PadLeft($digits, '0') $newName = "$numStr$($file.Extension)" } while ($used.ContainsKey($newName)) $used[$newName] = $true $renameMap[$file.FullName] = $newName } # Write restore file BEFORE renaming, so even partial renames leave us with a recoverable map. # Tab-separated: original<TAB>new (filenames only, no paths) $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $headerLines = @( "# randomize_filename.ps1 restore map" "# created: $timestamp" "# directory: $currentDir" "# format: original`tnew (tab-separated)" "# comment lines starting with '#' are ignored on restore" ) $mappingLines = $renameMap.GetEnumerator() | ForEach-Object { $original = Split-Path $_.Key -Leaf "{0}`t{1}" -f $original, $_.Value } Set-Content -LiteralPath $restoreFile -Value ($headerLines + $mappingLines) -Encoding UTF8 # Apply renames. .NET Move() bypasses PowerShell wildcard parsing (safe for [ ] in names). $renamed = 0 $failed = 0 foreach ($source in $renameMap.Keys) { $newName = $renameMap[$source] $dest = Join-Path $currentDir $newName try { [System.IO.File]::Move($source, $dest) Write-Host (" {0} -> {1}" -f (Split-Path $source -Leaf), $newName) $renamed++ } catch { $failed++ Write-Warning (" Failed: {0} ({1})" -f $source, $_.Exception.Message) } } Write-Host ("`nDone: renamed {0}, failed {1}." -f $renamed, $failed) -ForegroundColor Green Write-Host ("Restore map saved to: {0}" -f $restoreFile) -ForegroundColor Cyan 清理空文件夹 # clear_empty_folders.ps1 # Delete every empty folder under the current directory. # A folder is "empty" if it contains no files and no subfolders (recursively empty). # Usage: place this script in the target folder and run .\clear_empty_folders.ps1 $currentDir = (Get-Location).Path Write-Host ("Scanning: {0}" -f $currentDir) -ForegroundColor Cyan $removed = 0 do { # -LiteralPath avoids wildcard parsing for paths containing [ ] etc. # -Force includes hidden/system items so truly empty dirs are caught. $emptyDirs = Get-ChildItem -LiteralPath $currentDir -Recurse -Directory | Where-Object { -not (Get-ChildItem -LiteralPath $_.FullName -Force) } if (-not $emptyDirs) { break } foreach ($dir in $emptyDirs) { # Never delete the directory the script is running from if ($dir.FullName -eq $currentDir) { continue } try { # .NET API: deletes only if the folder is actually empty [System.IO.Directory]::Delete($dir.FullName) Write-Host (" Removed: {0}" -f $dir.FullName) $removed++ } catch { Write-Warning ("Failed to remove {0}: {1}" -f $dir.FullName, $_.Exception.Message) } } } while ($true) Write-Host ("`nDone: removed {0} empty folder(s)" -f $removed) -ForegroundColor Green # clear_empty_folders.ps1 # Delete every empty folder under the current directory. # A folder is "empty" if it contains no files and no subfolders (recursively empty). # Usage: place this script in the target folder and run .\clear_empty_folders.ps1 $currentDir = (Get-Location).Path Write-Host ("Scanning: {0}" -f $currentDir) -ForegroundColor Cyan $removed = 0 do { # -LiteralPath avoids wildcard parsing for paths containing [ ] etc. # -Force includes hidden/system items so truly empty dirs are caught. $emptyDirs = Get-ChildItem -LiteralPath $currentDir -Recurse -Directory | Where-Object { -not (Get-ChildItem -LiteralPath $_.FullName -Force) } if (-not $emptyDirs) { break } foreach ($dir in $emptyDirs) { # Never delete the directory the script is running from if ($dir.FullName -eq $currentDir) { continue } try { # .NET API: deletes only if the folder is actually empty [System.IO.Directory]::Delete($dir.FullName) Write-Host (" Removed: {0}" -f $dir.FullName) $removed++ } catch { Write-Warning ("Failed to remove {0}: {1}" -f $dir.FullName, $_.Exception.Message) } } } while ($true) Write-Host ("`nDone: removed {0} empty folder(s)" -f $removed) -ForegroundColor Green ffmpeg 当前文件夹下取出所有mkv视频的中文字幕 Get-ChildItem -File *.mkv | ForEach-Object { $file = $_ $base = [System.IO.Path]::GetFileNameWithoutExtension($file.Name) $tracks = ffprobe -v error ` -select_streams s ` -show_entries stream=index:stream_tags=language ` -of json ` "$($file.FullName)" | ConvertFrom-Json $chineseTracks = $tracks.streams | Where-Object { $_.tags.language -match '^(zh|zho|chi|zh-cn|zh-hans|zh-tw|zh-hant)$' } $n = 1 foreach ($track in $chineseTracks) { $output = "$base.zh.$n.srt" Write-Host "提取: $($file.Name) -> $output" ffmpeg -y ` -i "$($file.FullName)" ` -map "0:$($track.index)" ` "$output" $n++ } } Linux Linux开辟虚拟内存(Swap) free -h Linux实现FRP内网穿透 的是让国内用户能连到一台没有公网 IP 的家庭电脑——用的是反向内网穿透: ...

2025-08-24 10:51:14 PM · 18 分钟
工具手册

SCA 2023.X — Nacos Bootstrap 配置失效问题排查与解决方案

Issue 来源:spring-cloud-alibaba#3931 影响版本:spring-cloud-alibaba 2023.0.1.3+ 一、问题描述 在 Spring Cloud Alibaba 2023.X 版本中,将 Nacos 配置(包括 extension-configs、shared-configs 等)放在 bootstrap.yml / bootstrap.properties 中,配置中心的内容无法正常加载,但日志显示 bootstrap 文件本身已被读取。 将相同配置移到 application.yml 后,一切恢复正常。 二、根本原因 flowchart TD A[bootstrap.yml 被读取] --> B{SCA 版本判断} B -- 2023.0.1.2 及以前 --> C[✅ 正常加载 Nacos 配置中心] B -- 2023.0.1.3 及以后 --> D[❌ extension-configs / shared-configs 失效] D --> E[问题根源:SCA 2023.0.1.3 修改了配置加载优先级机制] E --> F[bootstrap 阶段注册的 Nacos PropertySource 被后续流程覆盖或丢弃] 简单来说: SCA 2023.0.1.3 做了一次不向后兼容的内部变更,导致 bootstrap.yml 中的 Nacos 扩展配置在加载链路中被"丢掉",而 application.yml 中的配置走的是新路径,不受影响。 ...

2025-07-24 10:40:26 PM · 2 分钟
后端疑难杂症解答

Nacos 一些常用配置 更新中 

bootstrap 配置 bootstrap.yaml server: port: 10888 tomcat: uri-encoding: UTF-8 spring: profiles: active: dev application: name: archive-service cloud: nacos: config: file-extension: yaml shared-configs: - data-id: shared-spring.yaml refresh: false - data-id: shared-redis.yaml refresh: false - data-id: shared-mybatis.yaml refresh: false - data-id: shared-logs.yaml refresh: false - data-id: shared-feign.yaml refresh: false - data-id: shared-logs.yaml # 共享日志配置 refresh: false # - data-id: shared-feign.yaml # 共享feign配置 # refresh: false lia: jdbc: database: tb_archive bootstrap-dev.yaml spring: cloud: nacos: server-addr: 192.168.2.115:8848 # nacos注册中心 discovery: namespace: f923fb34-cb0a-4c06-8fca-ad61ea61a3f0 group: DEFAULT_GROUP ip: 192.168.2.115 logging: level: asia.liminality: debug nacos 配置 shared-spring.yaml spring: jackson: default-property-inclusion: non_null main: allow-bean-definition-overriding: true mvc: pathmatch: #解决异常:swagger Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException #因为Springfox使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatcher matching-strategy: ant_path_matcher shared-redis.yaml spring: redis: host: ${lia.redis.host:192.168.2.115} password: ${lia.redis.password:github} lettuce: pool: max-active: ${lia.redis.pool.max-active:8} max-idle: ${lia.redis.pool.max-idle:8} min-idle: ${lia.redis.pool.min-idle:1} max-wait: ${lia.redis.pool.max-wait:300} shared-mybatis.yaml mySQL spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${sh.jdbc.host:127.0.0.1}/${sh.jdbc.database}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: ${lia.jdbc.username:root} password: ${lia.jdbc.password:794211} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: logic-delete-field: deletedAt logic-not-delete-value: "null" logic-delete-value: "now()" id-type: assign_id insert-strategy: not_null update-strategy: not_null porstgreSQL spring: datasource: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://${lia.jdbc.host:192.168.2.115}:${lia.jdbc.port:5432}/${lia.jdbc.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai username: ${lia.jdbc.username:postgres} password: ${lia.jdbc.password:github} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: logic-delete-field: deletedAt logic-not-delete-value: "null" logic-delete-value: "now()" id-type: assign_id insert-strategy: ignored update-strategy: ignored select-strategy: not_null shared-logs.yaml logging: pattern: dateformat: HH:mm:ss.SSS console: "%clr(%d{${LOG_DATEFORMAT_PATTERN}}){faint}-[${hostname}][%X{requestId:-sys}] %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n" file: "%d{${LOG_DATEFORMAT_PATTERN}}-[${hostname}][%X{requestId:-sys}]-${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%15.15t] %-40.40logger{39} : %m%n" file: path: "logs/${spring.application.name}" shared-feign.yaml feign: client: config: default: # default全局的配置 loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息 httpclient: enabled: true # 开启feign对HttpClient的支持 max-connections: 200 # 最大的连接数 max-connections-per-route: 50 # 每个路径的最大连接数 sentinel: enabled: true

2025-06-24 07:01:02 PM · 2 分钟

微信小程序开发受苦指南 更新中 

微信小程序和后端用户认证交互流程 微信小程序登录的核心流程:小程序拿临时凭证 code → 后端用 code 换 openid → 后端发自己的 JWT 给小程序。下面是整体流程图。 sequenceDiagram participant MP as 微信小程序 participant WX as 微信服务器 participant GW as gateway participant Auth as auth模块 participant User as user-service participant DB as lia_user库 MP->>WX: wx.login() 获取 code WX-->>MP: 返回临时 code MP->>GW: POST /auth/wx-login {code} GW->>Auth: 转发请求 Auth->>WX: code2session(code) WX-->>Auth: 返回 openid + session_key Auth->>User: OpenFeign 查/建用户 User->>DB: 按 openid 查询 DB-->>User: 用户记录(或空) User-->>Auth: 返回 userId Auth->>Auth: 生成 JWT Auth-->>MP: 返回 token + 用户信息 MP->>MP: 存入 Storage 关键点拆解 1. 小程序端拿 code(前端) ...

2025-04-23 02:02:30 PM · 1 分钟
微信小程序开发 前端开发