背景
本文是《JavaEE 后端从小白到大神》修仙系列第九篇,正式进入JavaEE后端世界。若想详细学习请点击首篇博文,我们开始吧。
第九篇:安全与事务管理
- JavaEE 安全模型;
- JAAS / Jakarta Security;
- 角色与权限控制;
- JDBC + JTA(Java Transaction API);
- 分布式事务;
- 事务传播与隔离级别。
一、JavaEE 安全模型
1. 什么是 JavaEE 安全模型
JavaEE 安全模型是一套声明式和编程式相结合的安全框架,用于保护企业级应用中的资源。它定义了认证(Authentication)、授权(Authorization)、数据完整性和机密性的标准机制。
一句话总结:JavaEE 安全模型 = 认证 + 授权 + 机密性 + 完整性,且以声明式为主、编程式为辅。
2. 安全模型的核心概念
| 概念 |
说明 |
| 认证(Authentication) |
验证用户身份(你是谁?)—— 登录 |
| 授权(Authorization) |
验证用户权限(你能做什么?)—— 角色/权限检查 |
| 机密性(Confidentiality) |
数据加密传输(HTTPS) |
| 完整性(Integrity) |
确保数据未被篡改(数字签名) |
| 主体(Principal) |
已认证的用户身份 |
| 角色(Role) |
一组权限的集合(如 admin、user、manager) |
| 安全域(Security Realm) |
用户身份信息的存储来源(数据库、LDAP、文件等) |
3. 安全模型的层次
JavaEE 安全模型可分为四个层次:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
┌─────────────────────────────────────────────────────────────────┐
│ 1. 传输层安全(Transport Layer Security)—— HTTPS │
│ 加密客户端与服务器之间的通信 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 2. 认证(Authentication)—— 验证用户身份 │
│ 登录验证:用户名/密码、客户端证书、Kerberos 等 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 3. 授权(Authorization)—— 控制资源访问 │
│ 基于角色的访问控制(RBAC)、声明式安全(@RolesAllowed) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 4. 审计(Auditing)—— 记录安全事件 │
│ 登录日志、操作审计、安全告警 │
└─────────────────────────────────────────────────────────────────┘
|
二、JAAS(Java Authentication and Authorization Service)
1. 什么是 JAAS
JAAS(Java Authentication and Authorization Service)是 Java SE 原生提供的认证与授权服务框架。Jakarta EE 的安全模型基于 JAAS 构建,是 JavaEE 安全的底层基础。
一句话总结:JAAS 是 Java 平台的身份认证和访问控制底层框架,提供了可插拔的认证模块(PAM 风格)。
2. JAAS 的核心组件
| 组件 |
作用 |
Subject |
代表已认证的实体(用户或服务) |
Principal |
主体的身份标识(用户名、角色名等) |
LoginContext |
认证的入口,调用 login() 方法触发认证流程 |
LoginModule |
具体的认证逻辑(数据库认证、LDAP 认证、Kerberos 认证等) |
CallbackHandler |
与用户交互获取认证信息(如用户名/密码输入) |
3. 认证流程
1
2
3
4
5
|
1. 应用创建 LoginContext,传入 CallbackHandler
2. LoginContext 调用配置的 LoginModule 执行认证
3. LoginModule 通过 CallbackHandler 获取用户名/密码
4. LoginModule 验证凭证,成功后填充 Subject 的 Principal
5. 应用通过 Subject 获取已认证的身份信息
|
4. JAAS 配置示例
LoginModule 配置(jaas.conf):
MyApp {
com.example.CustomLoginModule required
debug=true
dbUrl="jdbc:mysql://localhost:3306/userdb";
};
代码中使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
public class JaasExample {
public static void main(String[] args) throws Exception {
// 设置 JAAS 配置文件
System.setProperty("java.security.auth.login.config", "jaas.conf");
// 创建 LoginContext,传入 CallbackHandler 获取用户名/密码
LoginContext loginContext = new LoginContext("MyApp", new MyCallbackHandler());
try {
loginContext.login();
Subject subject = loginContext.getSubject();
System.out.println("认证成功,用户:" + subject.getPrincipals());
} catch (Exception e) {
System.out.println("认证失败:" + e.getMessage());
}
}
}
|
5. 在 Jakarta EE 中直接使用 JAAS 的场景
在实际开发中,我们几乎不需要直接编写 JAAS 代码,因为:
- Jakarta EE 应用服务器(WildFly、Payara)已经内置了 JAAS 集成;
- 开发者通过声明式安全(
@RolesAllowed 等注解)即可完成权限控制;
- 认证逻辑通常由容器统一处理(如通过
web.xml 配置认证方式)。
但在以下场景中需要理解 JAAS:
- 自定义认证方案(如集成企业 SSO);
- 实现自定义 LoginModule;
- 调试容器级的安全问题。
三、Jakarta Security(Jakarta EE 安全标准)
1. 什么是 Jakarta Security
Jakarta Security(原 Java EE Security)是 Jakarta EE 中用于认证和授权的统一规范,它整合并简化了 JavaEE 的安全编程模型。从 Jakarta EE 8 开始引入,替代了原先分散在多个规范中的安全机制。
核心目标:提供一套简单、类型安全、可移植的安全 API。
2. 核心注解
| 注解 |
作用 |
@BasicAuthenticationMechanismDefinition |
定义 HTTP Basic 认证方式 |
@FormAuthenticationMechanismDefinition |
定义表单登录认证方式 |
@CustomFormAuthenticationMechanismDefinition |
自定义表单认证 |
@RememberMe |
记住我功能 |
@LoginToContinue |
登录后跳转配置 |
3. 认证机制(Authentication Mechanism)
Jakarta Security 定义了三种内置认证机制:
| 机制 |
说明 |
适用场景 |
| HTTP Basic |
通过 Authorization: Basic 头传递用户名/密码 |
RESTful API |
| Form Login |
通过登录表单(用户名/密码 + 提交) |
传统 Web 应用 |
| Custom Form Login |
自定义登录页面和逻辑 |
需要定制化 UI 的场景 |
示例:使用 Form Login
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import jakarta.security.enterprise.authentication.mechanism.http.FormAuthenticationMechanismDefinition;
import jakarta.security.enterprise.authentication.mechanism.http.LoginToContinue;
import jakarta.enterprise.context.ApplicationScoped;
@FormAuthenticationMechanismDefinition(
loginToContinue = @LoginToContinue(
loginPage = "/login.html",
errorPage = "/login-error.html"
)
)
@ApplicationScoped
public class ApplicationConfig {
// 无需额外代码,容器自动处理认证
}
|
4. 身份存储(Identity Store)
身份存储定义了用户身份信息的来源。Jakarta Security 支持多种实现:
| 类型 |
说明 |
InMemoryIdentityStore |
内存中的用户列表(测试使用) |
DatabaseIdentityStore |
从数据库查询用户和角色 |
LDAPIdentityStore |
从 LDAP 目录服务查询 |
| 自定义 IdentityStore |
实现 IdentityStore 接口 |
数据库身份存储示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import jakarta.security.enterprise.identitystore.DatabaseIdentityStoreDefinition;
import jakarta.security.enterprise.identitystore.PasswordHash;
import jakarta.enterprise.context.ApplicationScoped;
@DatabaseIdentityStoreDefinition(
dataSourceLookup = "java:jboss/datasources/UserDS",
callerQuery = "SELECT password FROM users WHERE username = ?",
groupsQuery = "SELECT role FROM user_roles WHERE username = ?",
hashAlgorithm = PasswordHash.class,
priority = 80
)
@ApplicationScoped
public class MyIdentityStore {
// 容器自动读取配置
}
|
5. 编程式安全(Programmatic Security)
除了声明式安全(注解),Jakarta Security 也提供了编程式安全访问接口:
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
|
import jakarta.security.enterprise.SecurityContext;
import jakarta.inject.Inject;
@Stateless
public class DocumentService {
@Inject
private SecurityContext securityContext;
public void deleteDocument(Long docId) {
// 获取当前用户
String caller = securityContext.getCallerPrincipal().getName();
// 检查角色
if (!securityContext.isCallerInRole("admin")) {
throw new SecurityException("只有管理员可以删除文档");
}
// 删除逻辑...
}
public boolean hasPermission(String action) {
return securityContext.isCallerInRole("admin") ||
securityContext.isCallerInRole("manager");
}
}
|
6. 安全上下文(SecurityContext)
SecurityContext 提供了访问当前认证信息的统一 API:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import jakarta.security.enterprise.SecurityContext;
// 获取认证状态
boolean isAuthenticated = securityContext.getCallerPrincipal() != null;
// 获取当前用户名
String username = securityContext.getCallerPrincipal().getName();
// 检查角色
boolean isAdmin = securityContext.isCallerInRole("admin");
// 获取 Subject(底层 JAAS 主体)
Subject subject = securityContext.getSubject();
|
四、角色与权限控制(声明式安全)
1. @RolesAllowed——角色授权
@RolesAllowed 指定允许访问的角色列表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import jakarta.annotation.security.RolesAllowed;
import jakarta.ejb.Stateless;
@Stateless
public class AdminService {
// 仅 admin 角色可访问
@RolesAllowed("admin")
public void deleteAllUsers() {
// 删除所有用户
}
// admin 和 manager 角色可访问
@RolesAllowed({"admin", "manager"})
public void updateUser(User user) {
// 更新用户
}
}
|
2. @PermitAll / @DenyAll
@PermitAll:所有角色都能访问(包括未认证用户)
@DenyAll:所有角色都不能访问
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Stateless
public class PublicService {
@PermitAll
public String getPublicInfo() {
return "所有人都能看到";
}
@DenyAll
public void internalMethod() {
// 外部调用会抛出 SecurityException
}
}
|
3. 类级别与方法级别的组合
类级别的注解作为默认策略,方法级别的注解覆盖类级别:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Stateless
@RolesAllowed({"admin"}) // 类级别:默认要求 admin 角色
public class UserService {
// 继承类级别,需要 admin
public void deleteUser(Long id) { ... }
// 方法级别覆盖:只需要 user 角色
@RolesAllowed({"user", "admin"})
public User getUser(Long id) { ... }
// 方法级别覆盖:所有人可访问
@PermitAll
public String getPublicInfo() { ... }
// 方法级别覆盖:禁止访问
@DenyAll
public void sensitiveOperation() { ... }
}
|
4. web.xml 中的安全约束
除了注解,也可以在 web.xml 中配置安全约束(与注解互补):
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
|
<!-- web.xml -->
<security-constraint>
<web-resource-collection>
<web-resource-name>Admin Area</web-resource-name>
<url-pattern>/admin/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<!-- 定义用户角色 -->
<security-role>
<role-name>admin</role-name>
</security-role>
<security-role>
<role-name>user</role-name>
</security-role>
<!-- 认证方式 -->
<login-config>
<auth-method>FORM</auth-method>
<form-login-config>
<form-login-page>/login.html</form-login-page>
<form-error-page>/login-error.html</form-error-page>
</form-login-config>
</login-config>
|
5. 授权最佳实践
- 最小权限原则:默认拒绝,显式授权;
- 类级别做粗略划分,方法级别做精细控制;
- 敏感操作使用
@RolesAllowed 严格控制;
- 公共接口使用
@PermitAll 显式声明;
- 避免硬编码角色名,使用常量或配置文件管理。
五、JDBC + JTA(Java Transaction API)
1. 什么是 JTA
JTA(Java Transaction API)是 Java 平台的事务管理标准 API,用于管理分布式事务和跨资源事务。
核心作用:协调多个资源(数据库、JMS 队列、JCA 连接等)参与同一个事务,确保 ACID 特性。
一句话总结:JTA 是 Java 分布式事务的标准 API,实现了 XA 协议,让多个资源在一个事务中保持一致。
2. JTA 核心接口
| 接口 |
作用 |
UserTransaction |
面向应用的编程式事务 API(开始、提交、回滚) |
TransactionManager |
面向容器的底层事务管理器接口 |
Transaction |
代表一个分布式事务实例 |
XAResource |
资源(数据库、JMS 等)实现此接口参与分布式事务 |
3. JDBC 事务 vs JTA 事务
| 对比维度 |
JDBC 本地事务 |
JTA 分布式事务 |
| 资源范围 |
单一数据库连接 |
多个资源(DB + JMS + 其他) |
| 事务管理器 |
无(由数据库驱动管理) |
应用服务器的 JTA 事务管理器 |
| XA 支持 |
否 |
是 |
| 使用复杂度 |
简单 |
复杂 |
| 性能 |
高 |
稍低(两阶段提交开销) |
| 适用场景 |
单一数据源操作 |
跨数据源、跨服务的事务一致性 |
4. UserTransaction(编程式事务)
通过 UserTransaction 手动控制事务边界:
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
|
import jakarta.annotation.Resource;
import jakarta.ejb.Stateless;
import jakarta.transaction.UserTransaction;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@Stateless
public class OrderService {
@Resource
private UserTransaction utx;
@PersistenceContext
private EntityManager em;
public void createOrder(Order order, Payment payment) throws Exception {
utx.begin(); // 手动开启事务
try {
// 操作数据库
em.persist(order);
em.persist(payment);
// 可能还有其他资源操作(如 JMS 发送)
// ...
utx.commit(); // 提交事务(两阶段提交)
} catch (Exception e) {
utx.rollback(); // 回滚事务
throw e;
}
}
}
|
5. 声明式事务(CMT)——最推荐
绝大多数场景下,使用 EJB 的 CMT(容器管理事务) 是最佳选择,无需编写任何事务 API 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@Stateless
public class OrderService {
@PersistenceContext
private EntityManager em;
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void createOrder(Order order, Payment payment) {
// 一个事务中执行多个数据库操作
em.persist(order);
em.persist(payment);
// 方法正常结束 → 自动提交事务
// 抛出 RuntimeException → 自动回滚事务
}
}
|
6. CMT 事务属性回顾
| 事务属性 |
行为 |
适用场景 |
REQUIRED(默认) |
有事务则加入,无则新建 |
绝大多数场景 |
REQUIRES_NEW |
总是新建独立事务 |
审计日志、操作记录(即使外层回滚也不影响) |
MANDATORY |
必须已有事务,否则抛异常 |
要求调用方已在事务中 |
SUPPORTS |
有则加入,无则非事务执行 |
只读查询 |
NOT_SUPPORTED |
非事务执行,如有事务则挂起 |
不需要事务的操作 |
NEVER |
必须非事务执行,否则抛异常 |
严格禁止事务的操作 |
六、分布式事务(Distributed Transaction)
1. 什么是分布式事务
分布式事务是指在多个独立的资源管理器之间保持事务一致性的场景。例如:
- 一个操作同时更新两个不同数据库;
- 一个操作同时更新数据库和JMS 消息队列;
- 一个操作同时访问数据库和文件系统(通过 JCA)。
2. XA 协议与两阶段提交(2PC)
分布式事务基于 XA 协议(X/Open Distributed Transaction Processing) 实现,核心是两阶段提交(2PC,Two-Phase Commit)。
两阶段提交流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
┌─────────────────────────────────────────────────────────────────────┐
│ 第一阶段:准备(Prepare) │
├─────────────────────────────────────────────────────────────────────┤
│ 事务管理器(TM) → 资源1(Resource1) : "可以提交吗?" │
│ → 资源2(Resource2) : "可以提交吗?" │
│ 各资源记录准备日志,锁定资源,返回 "READY" 或 "FAILED" │
└─────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ 第二阶段:提交/回滚(Commit/Rollback) │
├─────────────────────────────────────────────────────────────────────┤
│ 若所有资源都返回 "READY": │
│ 事务管理器 → 资源1:提交(Commit) │
│ → 资源2:提交(Commit) │
│ 若有任一资源返回 "FAILED": │
│ 事务管理器 → 资源1:回滚(Rollback) │
│ → 资源2:回滚(Rollback) │
└─────────────────────────────────────────────────────────────────────┘
|
2PC 的缺点:
- 阻塞性:准备阶段资源被锁定,其他事务无法访问;
- 单点故障:事务管理器崩溃可能导致资源长时间锁定;
- 性能开销:多次网络往返,响应时间较长。
3. JMS 与数据库的分布式事务示例
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
|
import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.jms.*;
import jakarta.annotation.Resource;
@Stateless
public class OrderService {
@PersistenceContext
private EntityManager em;
@Resource(lookup = "java:jboss/queue/orderQueue")
private Queue orderQueue;
@Resource(lookup = "java:jboss/ConnectionFactory")
private ConnectionFactory connectionFactory;
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void createOrderAndNotify(Order order) throws Exception {
// 1. 写入数据库(参与 JTA 事务)
em.persist(order);
// 2. 发送 JMS 消息(参与同一个 JTA 事务)
try (JMSContext context = connectionFactory.createContext(
JMSContext.AUTO_ACKNOWLEDGE)) {
TextMessage msg = context.createTextMessage("Order:" + order.getId());
context.createProducer().send(orderQueue, msg);
}
// 方法结束:数据库提交 + JMS 发送 在同一个事务中
// 要么一起成功,要么一起回滚
}
}
|
4. 分布式事务的替代方案
在现代微服务架构中,XA/2PC 分布式事务因性能问题逐渐被替代,常见替代方案:
| 方案 |
说明 |
适用场景 |
| 最终一致性 |
通过消息队列异步保证最终一致性 |
大部分微服务场景 |
| TCC(Try-Confirm-Cancel) |
预留资源→确认执行→取消回滚 |
高并发、强一致性 |
| Saga 模式 |
一系列本地事务 + 补偿事务 |
长事务、跨服务 |
| 事务消息(RocketMQ) |
消息与本地事务原子提交 |
消息驱动的分布式事务 |
七、事务传播与隔离级别
1. 事务传播(Transaction Propagation)
事务传播定义了方法间调用时事务如何传递。在 EJB 的 CMT 中,通过 @TransactionAttribute 控制:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Stateless
public class OuterService {
@Inject
private InnerService innerService;
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void outerMethod() {
// 已有事务
innerService.innerMethod(); // 加入同一事务(REQUIRED 默认)
// 若 inner 方法使用 REQUIRES_NEW,则创建新事务,挂起外层事务
}
}
@Stateless
public class InnerService {
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void innerMethod() {
// 始终在新事务中执行
}
}
|
传播行为总结:
| 传播行为 |
存在事务时 |
无事务时 |
典型场景 |
REQUIRED |
加入当前事务 |
新建事务 |
默认,绝大多数场景 |
REQUIRES_NEW |
挂起当前事务,新建新事务 |
新建事务 |
审计日志、独立操作 |
MANDATORY |
加入当前事务 |
抛出异常 |
强依赖事务的调用 |
SUPPORTS |
加入当前事务 |
非事务执行 |
查询操作(可有可无事务) |
NOT_SUPPORTED |
挂起当前事务,非事务执行 |
非事务执行 |
不需要事务的操作 |
NEVER |
抛出异常 |
非事务执行 |
严格禁止事务 |
2. 事务隔离级别(Isolation Level)
隔离级别定义了事务间数据的可见性,解决了并发事务的读写问题。在 JPA 中可通过 @Transactional 或底层 JDBC 设置:
| 隔离级别 |
脏读 |
不可重复读 |
幻读 |
说明 |
READ_UNCOMMITTED |
✅ 允许 |
✅ 允许 |
✅ 允许 |
最低隔离,性能最高,数据最不一致 |
READ_COMMITTED |
❌ 禁止 |
✅ 允许 |
✅ 允许 |
大多数数据库默认级别 |
REPEATABLE_READ |
❌ 禁止 |
❌ 禁止 |
✅ 允许 |
同一事务多次读结果一致(MySQL 默认) |
SERIALIZABLE |
❌ 禁止 |
❌ 禁止 |
❌ 禁止 |
最高隔离,性能最低,完全串行化 |
并发问题的含义:
- 脏读(Dirty Read):事务读取了另一个未提交事务的修改数据;
- 不可重复读(Non-Repeatable Read):同一事务中两次读取同一数据,结果不同(因其他事务修改并提交);
- 幻读(Phantom Read):同一事务中两次查询,结果集不同(因其他事务插入/删除了数据)。
EJB 中设置隔离级别(JPA):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import jakarta.ejb.Stateless;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.LockModeType;
@Stateless
public class AccountService {
@PersistenceContext
private EntityManager em;
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
// 使用悲观锁(相当于 SERIALIZABLE 级别)
Account from = em.find(Account.class, fromId, LockModeType.PESSIMISTIC_WRITE);
Account to = em.find(Account.class, toId, LockModeType.PESSIMISTIC_WRITE);
// ... 转账逻辑
}
}
|
隔离级别与 JTA 的关系:在 EJB + JTA 环境中,隔离级别通常由数据库连接的默认设置决定,也可以通过 @TransactionAttribute 结合 JPA 的 LockModeType 来控制并发行为。
八、实战案例:银行转账系统
1. 业务需求
实现一个银行转账系统,包含:
- 从 A 账户扣款,向 B 账户存款;
- 使用分布式事务保证资金一致性;
- 记录转账日志(独立事务);
- 发送转账成功消息(与数据库事务一起);
- 只有管理员角色可以查看所有转账记录;
- 普通用户只能查看自己的转账记录。
2. 实体类
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
|
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "t_account")
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String accountNumber;
private String owner;
private BigDecimal balance;
// getter/setter
}
@Entity
@Table(name = "t_transfer_log")
public class TransferLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long fromAccountId;
private Long toAccountId;
private BigDecimal amount;
private String status; // SUCCESS, FAILED
private LocalDateTime createTime;
private String errorMessage;
// getter/setter
}
|
3. Service 层(事务控制)
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.LockModeType;
import jakarta.security.enterprise.SecurityContext;
import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.PermitAll;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Stateless
public class TransferService {
@PersistenceContext
private EntityManager em;
@Inject
private AuditService auditService; // 审计日志(独立事务)
@Inject
private SecurityContext securityContext;
/**
* 转账方法:使用分布式事务保证一致性
* 使用悲观锁防止并发问题
*/
@RolesAllowed({"user", "admin"})
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void transfer(Long fromId, Long toId, BigDecimal amount) throws Exception {
// 检查当前用户是否有权限操作 fromId 账户
String currentUser = securityContext.getCallerPrincipal().getName();
if (!securityContext.isCallerInRole("admin")) {
// 非管理员只能操作自己的账户
Account account = em.find(Account.class, fromId);
if (!account.getOwner().equals(currentUser)) {
throw new SecurityException("无权操作该账户");
}
}
// 悲观锁:锁定两个账户,防止并发修改
Account from = em.find(Account.class, fromId, LockModeType.PESSIMISTIC_WRITE);
Account to = em.find(Account.class, toId, LockModeType.PESSIMISTIC_WRITE);
// 校验余额
if (from.getBalance().compareTo(amount) < 0) {
// 记录失败日志(独立事务,不受外层回滚影响)
auditService.logTransfer(fromId, toId, amount, "FAILED", "余额不足");
throw new IllegalArgumentException("余额不足");
}
// 执行转账
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
// 记录成功日志(独立事务)
auditService.logTransfer(fromId, toId, amount, "SUCCESS", null);
// 方法正常结束:主事务提交(数据库更新 + 可能还有其他资源)
// 如果方法抛出异常,主事务回滚,但审计日志已提交(独立事务)
}
/**
* 查询所有转账记录(仅管理员)
*/
@RolesAllowed("admin")
@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public List<TransferLog> getAllTransfers() {
return em.createQuery("SELECT t FROM TransferLog t ORDER BY t.createTime DESC",
TransferLog.class).getResultList();
}
/**
* 查询用户自己的转账记录
*/
@RolesAllowed({"user", "admin"})
@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public List<TransferLog> getMyTransfers() {
String currentUser = securityContext.getCallerPrincipal().getName();
return em.createQuery(
"SELECT t FROM TransferLog t WHERE t.fromAccountId IN " +
"(SELECT a.id FROM Account a WHERE a.owner = :owner) " +
"ORDER BY t.createTime DESC", TransferLog.class)
.setParameter("owner", currentUser)
.getResultList();
}
}
|
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
28
29
30
31
32
|
import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Stateless
public class AuditService {
@PersistenceContext
private EntityManager em;
/**
* 记录转账日志:使用 REQUIRES_NEW 独立事务
* 即使外层事务回滚,日志也能保留
*/
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void logTransfer(Long fromId, Long toId, BigDecimal amount,
String status, String errorMessage) {
TransferLog log = new TransferLog();
log.setFromAccountId(fromId);
log.setToAccountId(toId);
log.setAmount(amount);
log.setStatus(status);
log.setCreateTime(LocalDateTime.now());
log.setErrorMessage(errorMessage);
em.persist(log);
// 该方法的事务独立提交,不受外层影响
}
}
|
5. 安全配置(web.xml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<security-constraint>
<web-resource-collection>
<web-resource-name>Bank Admin</web-resource-name>
<url-pattern>/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>BankRealm</realm-name>
</login-config>
<security-role>
<role-name>admin</role-name>
</security-role>
<security-role>
<role-name>user</role-name>
</security-role>
|
6. 事务传播与隔离级别应用
| 组件 |
事务属性 |
隔离级别策略 |
说明 |
transfer() |
REQUIRED |
悲观锁(LockModeType.PESSIMISTIC_WRITE) |
核心业务,串行化隔离效果 |
logTransfer() |
REQUIRES_NEW |
默认(READ_COMMITTED) |
独立审计,不依赖外层事务 |
getAllTransfers() |
SUPPORTS |
默认 |
只读查询,不需要事务 |
getMyTransfers() |
SUPPORTS |
默认 |
只读查询 |
7. 架构总览
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
|
┌─────────────────────────────────────────────────────────────────────┐
│ 客户端(浏览器/REST) │
└──────────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Web 层(Servlet / JAX-RS) │
│ SecurityContext 认证检查 │
└──────────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ EJB 层(TransferService + AuditService) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ TransferService(@Stateless) │ │
│ │ ① @RolesAllowed 声明式角色控制 │ │
│ │ ② @TransactionAttribute(REQUIRED) 主事务 │ │
│ │ ③ LockModeType.PESSIMISTIC_WRITE 悲观锁 │ │
│ │ ④ 调用 AuditService 记录日志(独立事务) │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ AuditService(@Stateless) │ │
│ │ @TransactionAttribute(REQUIRES_NEW) 独立事务 │ │
│ └──────────────────────────────────────────────────────────────┘ │
└──────────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ JPA(数据持久化) │
│ EntityManager + 数据库连接池 │
└──────────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 数据库(MySQL / PostgreSQL) │
│ 支持 XA 的 InnoDB / PostgreSQL │
└─────────────────────────────────────────────────────────────────────┘
|
九、总结
1. 核心知识脉络
| 层级 |
技术 |
核心概念 |
| 安全底层 |
JAAS |
Subject、Principal、LoginContext、LoginModule |
| 安全标准 |
Jakarta Security |
@BasicAuthenticationMechanismDefinition、IdentityStore |
| 声明式安全 |
@RolesAllowed、@PermitAll、@DenyAll |
角色授权、类/方法级别控制 |
| 编程式安全 |
SecurityContext |
获取当前用户、角色检查 |
| 本地事务 |
JDBC 事务 |
Connection.commit() / rollback() |
| 分布式事务 |
JTA + XA |
UserTransaction、CMT(@TransactionAttribute) |
| 事务传播 |
@TransactionAttribute |
REQUIRED、REQUIRES_NEW、MANDATORY 等 |
| 隔离级别 |
数据库隔离级别 |
READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE |
2. 关键理解
- 安全模型是分层的:传输安全(HTTPS)→ 认证(验证身份)→ 授权(控制权限)→ 审计(记录安全事件);
- 声明式安全优于编程式安全:使用
@RolesAllowed 等注解,代码更清晰、更易维护;
- CMT 是事务管理的最佳实践:绝大多数场景使用 EJB 的容器管理事务,无需编写事务 API 代码;
- 分布式事务的两阶段提交:保证跨资源一致性,但性能有开销,需合理评估;
- 事务传播决定方法间事务行为:
REQUIRES_NEW 实现独立事务,REQUIRED 实现事务合并;
- 隔离级别需谨慎选择:太高影响性能,太低导致数据不一致,默认
READ_COMMITTED 适合大多数场景;
- 悲观锁 vs 乐观锁:悲观锁(
LockModeType.PESSIMISTIC_WRITE)适合高冲突场景,乐观锁(@Version)适合低冲突场景。
3. 最佳实践总结
| 最佳实践 |
说明 |
| 声明式安全 |
优先使用 @RolesAllowed,而非编程式检查 |
| CMT 事务 |
优先使用 @TransactionAttribute,而非 UserTransaction |
| 独立事务 |
审计日志、操作记录使用 REQUIRES_NEW |
| 只读查询 |
使用 SUPPORTS 提升性能 |
| 并发控制 |
高冲突用悲观锁,低冲突用乐观锁(@Version) |
| 分布式事务 |
优先考虑最终一致性,实在需要强一致性才用 JTA/XA |
| 安全审计 |
记录所有认证和授权事件 |