Java Web 基础:登录认证-会话技术、过滤器与拦截器、事务管理
登录认证-会话技术
会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同意浏览器,以便在同一次会话的多次请求间共享数据。
会话跟踪方案:
- 客户端会话跟踪技术:Cookie
- 服务端会话跟踪技术:Session
- 令牌技术
Cookie
浏览器响应头 Set-Cookie:name=value
客户端请求头 Cookie:name=value
- 优点:Http协议支持
- 缺点:移动端App无法使用Cookie;不安全,客户可以自己禁用Cookie;不能跨域(跨域:客户端和服务端协议/ip/域名/端口不同都会跨域);【无状态】
Session
底层基于Cookie
优点:状态存储在服务端端的内存中,安全;【状态性,Session方案的核心优势在于“状态性”。服务器端有一个“指挥中心”,它知道哪些会话是活跃的、哪些是有效的。它可以随时吊销任何会话的访问权限。】
缺点:服务器集群环境下无法直接使用Session;Cookie的缺点
状态性就是,服务器有存储用户信息,用户只要一个token,无状态就是,用户自己存着信息,无论是否加密
令牌技术
优点:支持PC端和移动端;解决集群环境下的认证问题;减轻服务器端存储压力
1.解决扩展性,实现集群和分布式【多服务端】
使用令牌技术的服务器端不存储任何数据。传统Session存储在单个服务器的内存中,Session的旧解决方案:通过Session复制(性能开销大)或持久化到中央数据库(如Redis)(引入单点依赖和网络延迟)来缓解,但非根本解决。令牌的解决方案:采用无状态设计。服务器不存储会话数据,用户状态自包含在令牌中。任何服务器实例只需验证令牌有效性即可处理请求,天然支持分布式集群与水平扩展。
2.解决架构耦合,实现多平台通用【多客户端】
Session通常与特定的服务器端技术栈(如Java Servlet、PHP、.NET)紧密耦合。它依赖于Cookie来传递Session ID,这限制了客户端的类型。
而令牌(尤其是JWT)是一个开放标准,任何语言、任何平台都能生成和验证。它不依赖Cookie,可以通过HTTP Header(如 Authorization: Bearer <token>
)轻松传递,这使得它适用于:
原生手机App;API网关;不同域名的前端应用(解决跨域);第三方登录(OAuth 2.0的核心):你可以用一个令牌授权一个第三方应用访问你的部分资源,而无需提供用户名和密码
3.空间换时间,解决查询性能问题【快】
传统的session,即使使用Redis,每个请求都需要根据Session ID执行一次网络查询来获取用户数据。在高并发场景下,这会给中央存储带来巨大的压力和单点故障风险。而令牌技术:用户信息(如user_id, username)直接编码在令牌里,验证成功后即可直接使用,无需查询数据库。这用更大的网络传输量(令牌体积比Session ID大)换取了更低的服务器端延迟和负载。
JWT不是一个简单的“加密Cookie”,而是一种更安全、更标准、更通用的无状态凭证机制。
特性 | 加密的Cookie(自研方案) | JWT令牌(标准方案) |
---|---|---|
1. 安全核心 | 保密性。依赖加密算法隐藏内容。如果密钥泄露,一切皆空。 | 完整性与验证。依赖数字签名证明真伪。Payload可公开读取,但无法篡改。签名密钥泄露是灾难性的,但内容本身不依赖加密。 |
2. 标准化 | 自定义的、私有的。格式和加解密方法由开发者自己定义。 | 开放的、标准的。遵循RFC 7519标准,有固定的结构(Header.Payload.Signature),各语言都有现成库,互通性好。 |
3. 验证方式 | 服务器必须解密整个Cookie才能验证其有效性。 | 服务器只需验证签名(一个数学计算),而不需要先解密整个令牌。验证通过后,可直接解码读取Payload。 |
4. 内容可信度 | 服务器解密后,直接信任其中的内容。 | 服务器验证签名后,才信任其中的内容。这明确区分了“验证”和“信任”两个步骤。 |
5. 适用边界 | 通常与Web域名绑定,通过Cookie机制管理。 | 协议无关、域无关。可用于任何类型的客户端(App、桌面应用)、任何API,不受同源策略限制。 |
过滤器与拦截器
1. 过滤器示例:记录请求耗时和字符编码
// 使用 @WebFilter 注解或通过 FilterRegistrationBean 配置
@Component
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
long startTime = System.currentTimeMillis();
// 1. 前置处理:设置编码
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
System.out.println("Filter: 请求URI - " + httpRequest.getRequestURI());
// 2. 放行
chain.doFilter(request, response);
// 3. 后置处理:记录耗时
long endTime = System.currentTimeMillis();
System.out.println("Filter: 请求处理耗时 - " + (endTime - startTime) + "ms");
}
}
2. 拦截器示例:登录验证
@Component
public class AuthInterceptor implements HandlerInterceptor {
// 可以直接注入Spring管理的Bean
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 在Controller执行之前
String token = request.getHeader("Authorization");
if (token != null && userService.validateToken(token)) {
// 验证通过,放行
return true;
} else {
// 验证失败,直接返回401未授权,不再执行Controller
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 在Controller执行之后,视图渲染之前
// 可以对ModelAndView进行修改
System.out.println("Interceptor: Controller执行完毕");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 在整个请求结束之后,视图渲染完毕之后
// 常用于资源清理、异常日志记录等
if (ex != null) {
System.err.println("Interceptor: 请求处理发生异常 - " + ex.getMessage());
}
}
}
// 配置拦截器,需要实现 WebMvcConfigurer
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**") // 拦截/api开头的路径
.excludePathPatterns("/api/login"); // 排除登录接口
}
}
事务管理
事务回滚
@Transaction
默认情况下,只有出现RuntimeException才回滚异常,rollbackFor属性用于控制出现何种异常时回滚事务。
@Transaction(rollbackFor = Exception.class)
@Override
public void delete(Integer id) throws Exception {
deptMapper.deleteById(id);
if (true) throw new Exception("error!");
empMapper.deleteByDeptId(id);
}
事务属性-传播行为
根据propagation属性确定传播行为
@Transactional(propagation = Propagation.REQUIRED)
REQUIRED:(默认)需要事务,有则加入,无则创建新事务
REQUIRES_NEW:需要新事务,无论有无,总是创建新事务
eg. 需求:解散部门时,无论是成功还是失败,都要记录操作日志。
步骤:
①.解散部门:删除部门、删除部门下的员工
②.记录日志到数据库表中
@Transactional
@Override
public void delete(Integer id) {
try {
deptMapper.deleteById(id); //删除部门
int i = 1/0;
empMapper.deleteByDeptId(id); //删除部门下的员工
} finally {
DeptLog log = new DeptLog();
log.setCreateTime(LocalDateTime.now());
log.setDescription("执行了解散部门操作,此次解散的是"+id+"号部门");
deptLogService.insert(log); //记录操作日志
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
这个insert方法如果不设置为Propagation.REQUIRES_NEW,在delete失败时执行finally时事务回滚(insert事务也加入了delete事务),导致insert也被回滚,导致记录日志失败。
- REQUIRED:大部分情况下都是用该传播行为即可。
- REQUIRES_NEW:当我们不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。
本文系作者 @xiin 原创发布在To Future$站点。未经许可,禁止转载。
暂无评论数据