OAuth 2.0 + JWT 单点登录

OAuth 2.0 + JWT 单点登录实战 一、从一个登录按钮说起 你在商家后台(merchant.shop.com)点了"登录",页面跳转到了一个统一的登录页,你输入账号密码,然后又跳回了商家后台——你已经在系统里了。然后你打开运营后台(ops.shop.com),不用再输密码,直接进去了。 这就是单点登录(SSO)。背后有三个角色在协作: sequenceDiagram participant Browser as 浏览器 participant Biz as 业务系统\nmerchant.shop.com participant SSO as 认证中心\nsso.company.com Browser->>Biz: 1. 访问商家后台 Biz-->>Browser: 2. 302: 去认证中心登录 Browser->>SSO: 3. 跳转到登录页 SSO-->>Browser: 4. 返回登录页面 Browser->>SSO: 5. 提交用户名密码 SSO-->>Browser: 6. 302: 登录成功,回业务系统 Browser->>Biz: 7. 带着凭证回商家后台 Biz->>SSO: 8. 后端验证凭证 SSO-->>Biz: 9. 返回用户身份 Biz-->>Browser: 10. 登录成功,进入系统 这里面有两个关键问题: 第 5 步中,用户的密码交给了谁? 答案:只交给了认证中心。业务系统从头到尾都没见过用户的密码。 第 7 步中,浏览器带回的"凭证"是什么? 答案:是一个一次性的授权码(code),不是用户名密码,也不是最终的身份令牌。 这就是 OAuth 2.0 授权码模式的核心思路:用户密码只给认证中心,业务系统通过一个间接的"授权码"来确认用户身份。 二、逐帧拆解:一次登录的完整交互 下面以一个真实场景走一遍完整流程。三个参与者: 参与者 对应系统 职责 浏览器 用户正在用的 Chrome / Edge 用户操作的入口,负责跳转和提交凭据 业务系统 CRM 应用(crm.company.com:8080) 用户真正想用的系统,需要确认"你是谁" 认证中心 SSO 服务器(sso.company.com:9000) 唯一能验证用户名密码的地方,签发身份令牌 下面是完整的交互流程——请重点关注每个角色在每一步做了什么: ...

九月 22, 2022 · 13 分钟 · 2617 字 · yaomingye

JWT 与双令牌机制详解

JWT 与双令牌机制详解:从结构原理到 Java 代码实现 🤔 一、一个登录请求背后的困境 你写完了一个登录接口,用户提交用户名密码,服务端验证通过后创建 Session,把用户信息存进去,返回一个 JSESSIONID 的 Cookie。后续请求自动带上这个 Cookie,服务端从 Session 中取出用户信息——这是最传统的 Session 认证方式。 @PostMapping("/login") public String login(HttpSession session, @RequestBody LoginRequest req) { User user = userService.verify(req.getUsername(), req.getPassword()); if (user == null) { return "用户名或密码错误"; } session.setAttribute("currentUser", user); // 存入Session return "登录成功"; } @GetMapping("/info") public User info(HttpSession session) { return (User) session.getAttribute("currentUser"); // 从Session取 } 这段代码在单机部署时没有问题。但当你部署到 3 台服务器、前面挂了一个 Nginx 负载均衡时,问题就出现了: ...

九月 20, 2022 · 12 分钟 · 2361 字 · yaomingye

JWT + Redis 双令牌鉴权实战

JWT + Redis 双令牌鉴权实战:生产环境下的 Token 主动失效与过期管理方案 🔥 一、一个真实的生产事故 先看一段在中小项目中常见的鉴权代码: // 登录:生成JWT,返回给客户端 public String login(String username, String password) { User user = userService.verify(username, password); return Jwts.builder() .setSubject(user.getId().toString()) .setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000)) .signWith(SECRET_KEY) .compact(); } // 拦截器:验证JWT签名和过期时间 public boolean preHandle(HttpServletRequest request, ...) { String token = request.getHeader("Authorization"); Claims claims = Jwts.parserBuilder() .setSigningKey(SECRET_KEY).build() .parseClaimsJws(token).getBody(); // Token签名正确且未过期 → 放行 return true; } 这段代码能跑吗?能。有安全隐患吗?有,而且很严重。 ...

九月 19, 2022 · 19 分钟 · 3916 字 · yaomingye

Spring Security + JWT 企业级鉴权实战

Spring Security + JWT 企业级鉴权实战:从零概念到完整代码实现 阅读前提:本文假设你已经会使用 Spring Boot 写基本的 CRUD 接口。如果你从未接触过 Spring Security,从这篇开始即可。 本文按照"先搞懂概念 → 教程版完整实现 → 生产版逐项升级 → 验证排错“的顺序组织。如果你只想快速跑通一个能用的版本,读完 Part 1 后直接看 Part 2 即可;如果你想理解企业级项目的真实做法,需要完整读完。 Part 1:先搞懂要做什么 在写任何代码之前,先把三个问题搞清楚:认证和授权到底是什么?Spring Security 怎么运作的?JWT 是什么? 一、从一个没有防护的接口说起 假设你用 Spring Boot 写了一个用户管理接口: @RestController @RequestMapping("/api/admin") public class AdminController { @GetMapping("/users") public List<User> listAllUsers() { // 返回系统中所有用户信息 return userService.findAll(); } @DeleteMapping("/users/{id}") public String deleteUser(@PathVariable Long id) { userService.deleteById(id); return "删除成功"; } } 启动项目后,任何人只要知道 URL,就能直接访问这些接口——不需要登录,不需要权限。这在企业生产环境中是不可接受的。 ...

九月 18, 2022 · 23 分钟 · 4761 字 · yaomingye

Spring Boot企业开发高频注解完全指南

Spring Boot企业开发高频注解完全指南:从IoC容器到数据访问全覆盖 🤔 一、为什么需要这份注解清单 初学 Spring Boot 时,打开官方文档会看到上百个注解。但实际企业开发中,真正高频使用的注解只有其中一部分。很多注解你可能工作三五年也用不到一次。 本文筛选出企业开发中使用频率最高的 Spring 注解(不含 SpringMVC 和 SpringSecurity),每个注解都配有可运行的示例代码和一句话说明它的用途。不解释底层原理,只告诉你"这是什么、怎么用、什么时候用"。 注解来源范围:Spring Framework + Spring Boot + Spring Data JPA + Spring AOP + Spring Cache + Spring Scheduling + Spring Retry。 约定:下文所有示例均基于 Spring Boot 项目,包路径省略。示例中 @Service、@Repository 等注解未重复展示之处,默认已配合 @ComponentScan 自动扫描。 🗺️ 二、注解分类全景图 在实际进入每个注解之前,先用一张分类图建立全局认知: flowchart LR classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold; classDef branch fill:#2d1a05,stroke:#f59e0b,stroke-width:2px,color:#fde68a,font-weight:bold; classDef leaf fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb; ROOT[Spring注解体系] ROOT --> B1[1.IoC容器核心] ROOT --> B2[2.Boot启动配置] ROOT --> B3[3.AOP切面] ROOT --> B4[4.事务管理] ROOT --> B5[5.异步与定时] ROOT --> B6[6.缓存管理] ROOT --> B7[7.数据校验] ROOT --> B8[8.JPA数据访问] ROOT --> B9[9.事件监听] ROOT --> B10[10.测试支持] ROOT --> B11[11.条件装配] ROOT --> B12[12.重试机制] class ROOT root; class B1,B2,B3,B4,B5,B6,B7,B8,B9,B10,B11,B12 branch; 📦 三、Spring IoC 容器核心注解 IoC(控制反转)和 DI(依赖注入)是 Spring 的根基。以下是日常开发中必用的注解。 ...

九月 17, 2022 · 17 分钟 · 3415 字 · yaomingye

NIO 性能调优

NIO 性能调优:零拷贝、直接内存与线上问题排查全解析 1 ⚡ 问题切入:文件服务器 CPU 100%,网络带宽却没用满 一个典型的文件下载服务,使用传统 Java I/O 实现: // 传统文件传输:将磁盘文件发送给客户端 public static void sendFile(Socket socket, String filepath) throws IOException { FileInputStream fis = new FileInputStream(filepath); BufferedInputStream bis = new BufferedInputStream(fis); OutputStream os = socket.getOutputStream(); byte[] buf = new byte[8192]; int len; while ((len = bis.read(buf)) != -1) { os.write(buf, 0, len); // 每次循环:内核→用户→内核→网卡 } bis.close(); fis.close(); } 这段代码能工作,但投入生产后出现异常现象:4 核 CPU 全部 100%,但千兆网卡只用了 600Mbps。理论上这台机器完全可以跑满千兆,为什么 CPU 先成了瓶颈? ...

九月 16, 2022 · 9 分钟 · 1755 字 · yaomingye

Netty

Netty:Reactor 线程模型、Pipeline 责任链与四个必写示例全解析 1 ⚡ 问题切入:原生 NIO 能工作,但你敢上生产吗? 在上一篇 Java NIO 博客中,我们手写了一个 NIO EchoServer——单线程管理多个连接,Selector 封装 epoll。这段代码在演示环境中运行良好,但如果直接部署到生产环境,会遇到四个棘手问题: // 原生 NIO EchoServer 的核心循环(看似正确,实则隐患重重) while (true) { selector.select(); for (SelectionKey key : selector.selectedKeys()) { if (key.isAcceptable()) { SocketChannel client = ssc.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { SocketChannel client = (SocketChannel) key.channel(); ByteBuffer buf = ByteBuffer.allocate(1024); int len = client.read(buf); // 问题1: 读到半包怎么办? buf.flip(); // 问题2: 忘了 flip 直接炸 client.write(buf); // 问题3: 写不出去谁管? buf.clear(); // 问题4: clear 还是 compact? } keyIterator.remove(); } } 原生 NIO 的四大生产痛点: ...

九月 15, 2022 · 13 分钟 · 2657 字 · yaomingye

RandomAccessFile 文件指针与任意位置读写

RandomAccessFile 文件指针与任意位置读写:多线程分段下载实现 1 ⚡ 问题切入:流式读写的局限 前面介绍的 FileInputStream / FileOutputStream 都是 顺序流 (Sequential Stream),只能从文件头开始,一个字节接一个字节地读写。如果只想读文件的第 1000 个字节,顺序流必须先把前面 999 个字节读完(或调用 skip()),效率很低。 如果要实现以下场景,顺序流就完全不够用了: 修改一个 1GB 文件的中间某几个字节 在文件末尾追加日志,同时读取文件头部的元数据 多线程分段下载文件,每个线程负责不同区间的数据块 这些场景需要 随机读写 ——可以自由移动文件指针到任意位置,像操作数组下标一样操作文件。Java 提供了 RandomAccessFile 来实现这个能力。 // 随机读:直接跳到第 1000 字节开始读 RandomAccessFile raf = new RandomAccessFile("data.bin", "r"); raf.seek(1000); // 指针移到 position=1000 int b = raf.read(); // 读第 1000 个字节 System.out.printf("第1000字节: 0x%02X%n", b); raf.close(); 跳过前面 999 个字节,直达目标位置——这就是 RandomAccessFile 的价值。 2 🎯 核心概念:文件指针 2.1 ❓ 什么是文件指针 文件指针 (File Pointer)是 RandomAccessFile 内部维护的一个 long 类型偏移量(Offset),表示 下一次读写操作将发生的字节位置 。它类似于数组的下标索引,但作用对象是文件。 ...

九月 12, 2022 · 8 分钟 · 1611 字 · yaomingye

Java IO 进阶

Java IO 进阶:基本类型 IO、打印流与对象序列化实用指南 1 🎮 问题场景:从保存游戏分数说起 假设你正在开发一个本地小游戏,需要把玩家的最高分(int)、胜率(double)和昵称(String)保存到文件,下次启动时读回来。用前面学过的 FileWriter 写文本当然可以,但你需要手动处理类型转换——写的时候 int → String,读的时候 String → int,格式稍微不一致就解析失败。 有没有办法直接把 int 按固定 4 字节的二进制格式写入,读的时候也按 int 原样读出?这就是 基本类型 IO 要解决的问题。 更进一步,如果整个游戏状态是一个复杂的 Java 对象(玩家信息、关卡进度、道具列表),能不能 一键保存整个对象,一键读回 ?这就是 对象序列化 要解决的问题。 本篇覆盖 Java IO 的第四个进阶阶段,依次讲解三种机制: 机制 用途 一句话描述 DataInputStream / DataOutputStream 读写基本类型和字符串 按固定字节数的二进制格式读写,必须按序操作 PrintStream / PrintWriter 格式化文本输出 不抛异常,日常 System.out.println() 就在用它 ObjectInputStream / ObjectOutputStream 对象序列化与反序列化 一键将整个对象转为字节流保存或传输 2 💾 基本类型 IO:DataOutputStream / DataInputStream 2.1 ❓ 是什么 DataOutputStream 和 DataInputStream 是 装饰器流 (包装已有的 OutputStream / InputStream),提供直接读写 Java 基本类型和 String 的能力。数据以 平台无关的二进制格式 写入——比如 writeInt(42) 始终写 4 个字节,无论在什么操作系统上,读出来的值都不变。 ...

九月 9, 2022 · 5 分钟 · 937 字 · yaomingye

Java IO 编码与桥接

Java IO 编码与桥接:字符集、编码转换与乱码解决方案全解析 1 ⚠️ 问题切入:一段乱码代码 先看一段在实际开发中经常遇到的代码。这段代码在不同操作系统上运行,结果 完全不同 : public class GarbledDemo { public static void main(String[] args) throws Exception { // 在 Windows 中文系统上运行(默认 GBK) try (FileWriter writer = new FileWriter("hello.txt")) { writer.write("你好,世界!"); } // 在 Linux 服务器上读取(默认 UTF-8) try (FileReader reader = new FileReader("hello.txt")) { char[] buf = new char[1024]; int len = reader.read(buf); System.out.println(new String(buf, 0, len)); // 输出:你好,世界! ← 正常 // 还是:���← 乱码? // 取决于操作系统! } } } 为什么同一段代码在不同环境下表现不同?因为 FileReader / FileWriter 使用 JVM 默认编码 (通常是操作系统默认编码),而 Windows 中文版默认是 GBK ,Linux 默认是 UTF-8 。写入和读取时编码不一致,就会产生 乱码 (Mojibake,指因字符编码不匹配导致的不可读字符)。 ...

九月 9, 2022 · 8 分钟 · 1535 字 · yaomingye
Cat Radio