内容纲要

认证方式

在构建 Web 系统时,认证(Authentication)会话管理(Session Management) 是安全设计的核心环节。传统的 Session-Cookie 和现代的 JWT(JSON Web Token) 是两种最常用的方案。本节将简述二者的差异,并重点讨论JWT的原理和实践。

JWT 的核心原理

jwt介绍

JSON Web Token(JWT) 是一种基于 JSON 的开放标准,用于在各方之间安全传输声明信息。它通常用于互联网应用程序中,用于身份验证和授权,是目前最常用和流行的跨域认证解决方案。

jwt组成

实际的jwt如下:

jwt是一个很长的字符串,通过.字符分割成3个部分:第一部分被称为头部(header),第二部分被称为载荷(payload),第三部分被称为签名(signature)

这三个部分按header.payload.signature的格式组成了jwt字符串。

通过jwt解密平台,我们可以看到这三部分在解密后分别包含的信息:

下面我们来详细讲述一下这3个部分的组成和功能。

header

这部分是一个JSON字符串,包含了签名算法alg和Token类型type信息。这个字符串在经过了Base64编码后成为了JWT的header部分。

{
  "alg": "HS256",
  "typ": "JWT"
}
payload

paylaod也是一个JSON字符串。其中有两种信息,一是官方规定的字段,诸如生效时间、过期时间、主题等。二是自定义的私有字段。这个字符串在经过了Base64编码后成为了JWT的payload部分。

{
    "sub": "1",                                             //官方字段:主题
    "username": "Test User",                    //自定义字段:用户名
    "permissions": ["ORDER_MANAGE"],    //自定义字段:权限
    "iat": 1743515030,                                //官方字段:签发时间
    "exp": 1743518630                                 //官方字段:过期时间
}

⚠️由于包含的自定义信息会被解密出来,所以不要将敏感信息(手机号、邮箱号)等信息自定义在payload中。

signature

这部分是对Header 和 Payload 的签名,防止数据篡改。通过header中定义的加密算法和服务器中存储的密钥,对header.payload进行加密后生成签名

jwt优缺点

优点 缺点
无状态,适合分布式系统 Token 无法主动失效
跨语言支持 Payload 数据明文可见(需加密)
自包含,减少数据库查询 密钥管理复杂

Session-Cookie认证原理

核心流程

  1. 用户登录
    用户提交凭证(如用户名密码),服务端验证后生成 Session ID,存入服务端存储(如内存、Redis),并通过 Set-Cookie 返回给客户端。

  2. 会话保持
    客户端后续请求自动携带 Cookie(含 Session ID),服务端通过 Session ID 查询会话信息(如用户身份、权限)。

    // 服务端获取 Session
    HttpSession session = request.getSession(false); // 不创建新 Session
    if (session != null) {
       String userId = (String) session.getAttribute("userId");
       String role = (String) session.getAttribute("role");
       // 校验用户权限...
    } else {
       response.sendRedirect("/login"); // 会话过期,重定向到登录页
    }
  3. 会话销毁
    Session的销毁包括如下场景:

    • 显式注销:调用 session.invalidate()
    • 超时销毁:通过 session.setMaxInactiveInterval(1800) 设置过期时间(如 30 分钟)。
    • 服务端重启:若 Session 存储在内存中,重启后丢失(需持久化存储解决)。

Session 的生成

当用户首次登录时,服务端会创建一个唯一的 Session ID,并与用户数据绑定存储。

// 服务端创建 Session
HttpSession session = request.getSession(true); // 若不存在则创建新 Session
session.setAttribute("userId", "12345");
session.setAttribute("role", "admin");
  • Session ID:通常是一个随机生成的字符串(如 D79E8C7E3C8B4E2A8F6E1D3C7A8B4E2A)。
  • 存储位置:服务端内存、数据库(如 MySQL)或缓存(如 Redis)。

Session 的验证流程

  1. 客户端发起请求

后续请求中,浏览器自动附加 Cookie 到请求头:

GET /user/profile HTTP/1.1
Host: example.com
Cookie: JSESSIONID=D79E8C7E3C8B4E2A8F6E1D3C7A8B4E2A
  1. 服务端验证 Session

服务端根据 Session ID 查询会话数据:

// 服务端获取 Session
HttpSession session = request.getSession(false); // 不创建新 Session
if (session != null) {
    String userId = (String) session.getAttribute("userId");
    String role = (String) session.getAttribute("role");
    // 校验用户权限...
} else {
    response.sendRedirect("/login"); // 会话过期,重定向到登录页
}

JWT与Session-Cookie对比

对比维度 Session-Cookie JWT
状态管理 有状态(服务端存储 Session) 无状态(Token 自包含)
扩展性 依赖集中式存储(集群部署需共享 Session) 天然适合分布式系统
安全性 易防御 CSRF(结合 SameSite Cookie) 依赖 HTTPS 和前端安全实践(防 XSS)
性能 每次请求需查询 Session 存储 只需验证签名(无存储查询)
主动失效 服务端可立即删除 Session 依赖 Token 过期时间,无法主动失效
移动端兼容性 依赖 Cookie(部分 Native App 支持不完善) 无 Cookie 依赖,更适合移动端
跨域支持 需处理 CORS 和 Cookie 域限制 无 Cookie,跨域更方便

适合 Session 的场景

  • 单体应用:所有请求由同一服务处理,Session 管理简单。
  • 需要实时权限控制:如银行系统需立即冻结用户账户。
  • 对 CSRF 敏感:依赖 SameSite Cookie 提供内置防护。

适合 JWT 的场景

  • 分布式/微服务架构:服务实例无需共享状态。
  • 第三方认证(OAuth2):如使用 Google/Facebook 登录。
  • 移动端/跨平台应用:避免 Cookie 兼容性问题。
  • 高并发 API 服务:减少数据库/缓存查询压力。

JWT + Spring Security 实践

在使用JWT完成认证过程的实践中,使用SpringBoot框架的情况下引入Spring Security来做技术实现是一个非常好的选择。

Spring Security 的架构

Spring Security 是一个高度可定制的安全框架,其核心基于 过滤器链(Filter Chain)责任链模式,通过一系列组件协作实现认证(Authentication)与授权(Authorization)。

Spring Security 通过 过滤器链(Filter Chain) 实现安全控制,核心流程如下:

关键组件

  • SecurityContextHolder:存储当前线程的安全上下文(如用户身份)。
  • AuthenticationManager:认证入口,委托给 Provider 实现(如 DaoAuthenticationProvider)。
  • UserDetailsService:加载用户数据,从数据库usersauthorities查询用户信息。

JWT 与 Spring Security 的整合实践

在 Web 安全领域,Spring Security 和 JWT 各有其独特优势,但将它们结合使用可以解决传统 Session 架构的痛点,并适应现代分布式系统的需求。

  1. 自定义 JWT 过滤器
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) 
        throws IOException, ServletException {

        // 从 Header 提取 Token
        String token = jwtTokenProvider.resolveToken(request);

        if (token != null && jwtTokenProvider.validateToken(token)) {
            // 解析 Token 获取用户信息
            Authentication auth = jwtTokenProvider.getAuthentication(token);
            // 将 Authentication 存入 SecurityContext
            SecurityContextHolder.getContext().setAuthentication(auth);
        }

        chain.doFilter(request, response);
    }
}
  1. JWT核心工具类

    public class JwtTokenProvider {
    
       private final JwtProperties jwtProperties;
       private SecretKey secretKey;
    
       @PostConstruct
       protected void init() {
           secretKey = Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8));
       }
    
       public String createToken(User user) {
           Claims claims = Jwts.claims().setSubject(String.valueOf(user.getId()));
           claims.put("username", user.getUsername());
           claims.put("permissions", user.getPermissions().stream()
                   .map(Permission::getCode)
                   .collect(Collectors.toSet()));
    
           Date now = new Date();
           Date validity = new Date(now.getTime() + jwtProperties.getValidityInMs());
    
           return Jwts.builder()
                   .setClaims(claims)
                   .setIssuedAt(now)
                   .setExpiration(validity)
                   .signWith(secretKey)
                   .compact();
       }
    
       public JwtTokenInfo validateToken(String token) {
           Claims claims = Jwts.parserBuilder()
                   .setSigningKey(secretKey)
                   .build()
                   .parseClaimsJws(token)
                   .getBody();
    
           return JwtTokenInfo.builder()
                   .userId(Long.parseLong(claims.getSubject()))
                   .username(claims.get("username", String.class))
                   .permissions(claims.get("permissions", List.class))
                   .build();
       }
    }
  2. Spring Security配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()  // 禁用 CSRF(无状态 API 不需要)
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态
            .and()
            .authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()  // 公开登录接口
                .anyRequest().authenticated()  // 其他接口需认证
            .and()
            .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
    }
}

纯 Spring Security 方案的对比

维度 纯 Spring Security (Session) JWT + Spring Security
状态管理 有状态(服务端存储 Session) 无状态(客户端存储 Token)
扩展性 需 Session 共享(如 Redis) 天然支持分布式
跨域支持 依赖 Cookie,需处理 CORS 无 Cookie 限制,更灵活
性能 每次请求查询 Session 存储 仅需验证签名,性能更高
移动端兼容性 需处理 Cookie 兼容性问题 无 Cookie 依赖,更适合移动端
适用场景 单体应用、需要实时权限控制 微服务、高并发 API、第三方登录

总结

通过整合 JWT 与 Spring Security,开发者可以构建一个 无状态、高扩展、跨平台 的现代安全架构,尤其适合分布式系统。

最后修改日期: 2025年4月13日

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。