快手二面:有了Cookie和Session 为什么还要JWT ?你说一下JWT的原理?

开发 前端
JWT(JSON Web Tokens)作为一种轻量级且灵活的身份验证和授权机制,在现代web服务及移动应用中得到了广泛应用。它允许服务器端通过加密签名的方式向客户端发放安全的、自包含的令牌,这些令牌可以携带必要的用户身份信息和权限声明,而且由于其无需持久化存储的特性,非常适合于微服务架构下的无状态通信场景。

引言

在业务系统开发中,用户身份验证是保障系统服务安全性的基石。无论是社交网络、电商平台还是企业级应用,都需要确保用户的访问权限与行为与其真实身份相符。为了达到这一目的,我们通常采用了一系列技术手段来管理用户会话并验证其身份,其中最常见的是Cookie和Session机制。但是,在近年来随着微服务以及API驱动架构的发展,JSON Web Tokens(JWT)作为一种轻量级的身份验证方案得到了广泛的关注和应用。

那么,JWT究竟是什么?它的原理是如何运作的?又为何在已有Cookie和Session的基础上,仍需要引入JWT作为身份验证的一种新方法呢?

Cookie和Session

Cookie是什么?

Cookie是Web开发中一种关键的客户端存储技术,它是由Web服务器在用户访问时生成并发送至用户浏览器的微型数据包,以文本文件的形式储存在用户的设备上。浏览器根据HTTP协议的规定,在之后对同一服务器的所有请求中自动附带这些Cookie信息。

服务器通过在HTTP响应头中设置Set-Cookie字段,来指示浏览器保存具有特定名称、值和其他配置参数(例如有效期、安全选项、路径限制、域名限制等)的Cookie。这样,每一个Cookie实质上就是一对键值对,帮助服务器识别和区分不同的用户及其在网站上的交互历史。

Cookie的一个核心应用是在用户身份验证和会话管理方面,它可存储用户的会话ID,使得用户在不同页面间切换或重新打开网站时,服务器能够基于这个ID重建用户的会话状态,实现无缝登录和个性化体验。此外,Cookie还广泛应用于记录用户喜好、统计分析用户行为、广告定向投放等方面,以提升用户体验和服务质量。但是,由于Cookie可能涉及用户隐私,现代浏览器和相关法规越来越重视对Cookie使用的透明度和用户控制权。

Seeion是什么?

Session作为一种服务器端机制,旨在管理和维护用户在其交互期间的状态信息,这些信息被安全地存储在服务器内存或者持久化存储(如数据库)中,与仅在客户端存储的Cookie形成了鲜明对比。每当用户开始与服务器建立联系时,服务器会初始化一个新的Session对象,并将其状态数据保存在服务器内部资源中。

一旦Session建立,服务器会产生一个独一无二的Session标识符——Session ID,该ID随后会通过HTTP响应中的Cookie(通常命名规则包括但不限于JSESSIONID)下发至客户端浏览器进行存储。此后,用户的每一次请求都将携带着这个Session ID,服务器通过解析请求中的Session ID,能够在自身的Session存储区域内检索到相对应的Session对象,从而实时获取并更新用户会话的具体状态。

Session机制广泛应用在诸如认证授权场景中,用来持久化登录用户的凭证和权限等敏感信息,确保用户在浏览网站的不同页面时仍能维持登录状态和个性化体验。此外,Session还能用于暂存用户会话过程中的临时数据,比如购物车的内容、网页表单填写的中间状态等,直至会话自然终止(如超时)、用户主动登出或清理Session时,这些数据才会失效。

但是虽然Cookie和Session在会话管理中一直使用,但它们存在一些缺点如下:

  • Cookie数量和大小限制:浏览器对Cookie的数量和大小有限制,可能导致用户数据存储受限。
  • 服务器资源占用:Session需要在服务器端存储大量用户会话信息,随着并发用户数的增长,可能会造成服务器内存资源紧张。
  • 跨域问题:Cookie默认遵循同源策略,不便于跨域共享会话状态。
  • 分布式系统中的Session同步:在分布式环境或集群部署的情况下,Session数据需要在各服务器之间同步,增加了系统的复杂性。

为解决上述会话管理中的缺点,以及对无状态服务、更好的扩展性和安全性要求,JWT这样的新型身份验证技术就这样诞生了。JWT通过将用户信息加密打包成Token,让客户端自行携带认证信息,从而实现了服务器的无状态化。

JWT是什么?

JSON Web Tokens (JWT) 是一种开放标准(RFC 7519),定义了一种紧凑、自包含的方式来安全地在各方之间传输信息。JWT主要由三个部分组成,即Header(头部)、Payload(载荷)和Signature(签名),这三部分之间通过.分隔。

JWT的组成JWT的组成

JWT的内容采用Base64编码,可以直接嵌入到HTTP请求头或者URL查询参数中,因其具有可读性、自包含和防篡改的特点而广泛应用在身份验证和授权场景中。

Header

描述了所使用的JWT类型(通常为JWT)以及签名算法(如HS256、RS256等)。

• typ: 表明这是一个JWT(固定为 "JWT")。

• alg: 指定用于签署JWT的算法,如 "HS256"(HMAC SHA-256)或 "RS256"(RSA SHA-256)等。

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

Payload

包含了实际要传递的数据,可以是任意的JSON对象,包含一组称为声明(claims)的数据。一般为用户身份信息(如用户ID、角色、权限等)和其他自定义声明,还包含一个exp(Expiration Time)字段来设置JWT的有效期以及其他元数据。声明分为三种类型:

  1. Registered Claims(注册声明) 这些是预先定义好的标准声明,虽然不是必须的,但在JWT规范中建议使用。它们提供了一套通用的信息,有助于JWT的标准化处理。常见的注册声明包括:

• iss(issuer):签发JWT的实体。

• sub(subject):JWT所面向的用户或主题。

• aud(audience):预期接收JWT的受众。

• exp(expiration time):JWT过期时间,在此时间之后JWT应被视为无效。

• nbf(not before):JWT生效时间之前,不应被接受处理的时间点。

• iat(issued at):JWT的创建时间。

• jti(JWT ID):JWT的唯一标识符,可用于防止重放攻击。

2. Public Claims(公共声明) 公共声明是预留用于行业共识的标准声明,虽然目前并未正式注册到IANA JSON Web Token Registry,但可以在IANA JSON Web Token Registry 查看已注册的声明集。如果没有官方注册,但多个项目间需要共享相同的声明,可以选择将声明名称以特定前缀(如 urn:<namespace>:<name>)进行命名,避免冲突。

3. Private Claims(私有声明) 私有声明是用于应用内部约定的自定义声明,我们可以根据自己业务需求自由定义。例如,可以包含用户ID (userId)、用户角色 (role)、邮箱地址 (email) 等任何想要在JWT中传递的信息。

示例:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022,
  "jti": "1pmysetgujcoden",
  "exp": 1516242622,
  "scope": [
    "read",
    "write"
  ]
}

Payload部分默认是不加密的,一定不要将隐私信息存放在 Payload 当中!!!

Signature

Signature部分用于确保JWT在传输过程中没有被篡改,它是通过对前两部分(Header和Payload编码后的字符串)使用Header中指定的加密算法以及一个共享或私有的密钥进行签名计算得到的。签名确保了只有知道该密钥的实体才能创建有效的JWT,并且任何人都可以验证JWT的完整性和来源的真实性。

生成签名的计算公式如下:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

算出签名以后,把Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,这个字符串就是JWT 。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwianRpIjoiMXBteXNldGd1amNvZGVuIiwiZXhwIjoxNTE2MjQyNjIyLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXX0.eW91cmUtYXV0aG9yaXphdGlvbi1zaWduYXR1cmU=

对于这段字符串,我们在JWT官网中,使用它的解码器进行解码,就可以得到Header、Payload、Signature这三部分。

图片图片

如何基于JWT进行身份验证?

JWT生成

服务器首先构建JWT的Header和Payload,并分别进行Base64编码。使用Header中指定的签名算法(比如HMAC SHA-256或RSA)对编码后的Header和Payload进行签名,生成Signature。将Header、Payload和Signature连接在一起,形成完整的JWT字符串。

JWT发放

用户通过用户名/密码登录后,服务器验证用户身份无误,便生成JWT并将其返回给客户端。客户端可以将JWT存储在浏览器的LocalStorage、SessionStorage中,或者作为Bearer Token放在Authorization请求头中。

这里需要注意,将JWT存储在浏览器的localStorage中,相较于存储在Cookie中,可以降低CSRF(跨站请求伪造)的风险。因为CSRF攻击依赖于浏览器自动附带在请求中的Cookie,而localStorage中的数据不会自动包含在普通的HTTP请求头部。

至于携带JWT的方式,建议将JWT放在HTTP Header的Authorization字段中,采用Bearer Token的形式:

Authorization: Bearer <JWT>

这种做法允许在无状态的RESTful API中方便地进行身份验证,服务器可以根据请求头中的JWT来验证请求发起者的身份和权限。同时,这种方式也便于实现JWT的刷新与撤销。

JWT使用

在后续的请求中,客户端将JWT随HTTP请求一起发送给服务器。服务器接收到JWT后,对其进行解码和验证Signature,如果Signature正确,则说明JWT未被篡改,并且仍处于有效期内。根据Payload中的信息,服务器可以确定用户身份以及相关权限,从而做出相应处理。

JWT过期与刷新

JWT有一定的生命周期,过了指定的exp字段时间后,服务器不再接受该JWT。对于长时间交互的场景,可以设计Refresh Token机制,当JWT即将过期时,客户端使用Refresh Token去服务器申请新的JWT,以延长用户会话的有效期。

图片图片

JWT安全性

JWT如何防止被篡改?

JWT通过签名(Signature)来防止被篡改。数字签名的机制确保了JWT的内容一旦被篡改,服务端就能检测出来,从而拒绝非法的请求。

当客户端带着JWT向服务器发送请求时,服务器会先将接收到的JWT按.分割成Header、Payload和Signature三部分。服务器使用与生成签名时相同的密钥和算法,根据接收到的Header和Payload重新计算签名。如果重新计算出的签名与接收到的Signature一致,那么可以认为JWT在传输过程中未被篡改;如果不一致,则说明JWT已被篡改。

当然,JWT通过这种方式防止被篡改的一个大前提就是,秘钥(Secret Key)是安全的,如果秘钥泄漏,那就可以更改Header、Payload,然后利用这个秘钥生成一个Signature就可以了,那么就不安全了。所以,我们一定要确保服务端使用的密钥是安全的,并且严格保密,不对外泄露。

如何加强JWT的安全性?

作为使用者,防止JWT被篡改的主要措施并不直接体现在客户端的操作上,而是依赖于服务端的安全设计和实施。我们可以通过以下方面考虑,加强JWT的安全性:

1. 服务端安全策略:首先我们必须确保服务端使用的密钥是安全的,并且严格保密,不对外泄露。密钥用于签署和验证JWT,一旦密钥泄露,可能导致JWT被伪造或篡改。并且还要采用安全强度足够高的签名算法,例如HS256、RS256等,这些算法能有效地确保JWT的完整性。

2. 正确的签名流程:确保所有签发的JWT都经过服务器端签名,并且每次验证JWT时都要使用相同的密钥和算法进行验证。

3. 设置合理的Token有效期:设置JWT的有效期(exp claim)限制,避免JWT长时间有效,减少恶意攻击者篡改JWT后继续使用的机会。

4. 严谨的身份验证逻辑:在服务端验证JWT时,除了验证签名之外,还应对Payload中的其他重要声明(如用户ID、权限、过期时间等)进行校验。

5. 客户端存储方式:尽管JWT被篡改的风险主要集中在网络传输阶段,但在客户端存储JWT时,推荐使用localStorage而非Cookie(尤其是考虑到防止CSRF攻击),同时也可以考虑对敏感信息加密存储。

JWT相较于Cookie和Session的优缺点

JWT的优点

JWT是无状态的,这样服务器不需要存储会话状态信息,减轻了服务器端的存储负担,在分布式环境下使用JWT可以在集群内的任何节点验证,无需保持会话状态同步,JWT还可以轻易地在不同域名的服务之间传递,适用于微服务架构和跨域应用。因为它包含了所有必要的用户信息,所以在服务间跳转时无需再次验证用户身份,每个请求都包含了认证所需要的所有信息,减少了服务器查询数据库或存储服务以验证用户状态的次数,从而提高了效率。

JWT的缺点

JWT中的数据默认情况下是Base64编码的,虽然可以加密但并非强制要求,这使得在不加密的情况下,JWT不适合存储敏感信息。相比而言,Session存储在服务器端,安全性更高。如果JWT被盗取,黑客可以在有效期内持续使用,直到token过期或被撤销。另外JWT的体积相对较大,特别是当包含更多信息时。每次HTTP请求都需要携带JWT,可能会增加请求头的大小,尤其是在移动网络环境下,可能会对带宽和流量有较大影响。并且由于JWT自带有效期,当用户登出时,由于JWT无法立即失效,所以只要JWT发放出去,除非在服务器端维护黑名单或使用可撤销的Token机制(如JWT ID Token配合OpenID Connect协议),否则无法立即废弃或更改Token中的权限信息。

JWT应用场景

• 身份验证:JWT通常用于用户登录认证,服务器在验证用户凭据成功后,生成一个带有用户特定信息和过期时间的JWT,客户端收到后,在后续请求中将其作为凭证发送给服务器,服务器仅需验证JWT的签名即可确认用户身份,无需持久化存储会话信息。

• 授权:JWT可以携带权限声明,服务器通过解析JWT中的声明,决定用户是否有权访问特定资源或执行某些操作。

• 单点登录(SSO):由于JWT可以跨域使用,因此在多个子系统间轻松实现SSO功能,用户在一个系统登录后,其JWT可在多个系统间传递,实现无缝登录体验。

在高并发、分布式、跨域及移动端场景下,JWT常因其实现的简洁性和高效性而得到青睐,而在注重数据安全性、复杂会话管理和易控制的场景下,Session可能会是更好的选择。同时,很多现代应用采用了混合策略,结合两者的优势来优化用户体验和系统安全性。

如何使用JWT?

在实际开发中,我们可以使用Spring Boot框架结合JWT来实现用户身份验证。我们来实现一个简单的示例。

我们引入JWT的依赖包:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.12.4</version>
</dependency>

创建JWT工具类

创建一个JWT工具类,用于生成和验证JWT令牌。这个类通常会包含生成JWT、解析JWT以及提取其中载荷信息的方法。

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecureDigestAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;

/**
 * @version 1.0
 * @description: <p></p >
 * @author: 码农Academy
 * @create: 2024/4/18 14:39
 */
@Component
public class JwtTokenUtil {

    @Value("${security.jwt.token.secret-key}")
    private String secretKey;

    /**过期时间*/
    private static final long VALIDITY_IN_MILLISECONDS = 3600000; // 1h

    /**
     * 生成JWT
     * @param authentication 当前登陆用户信息
     * @return JWT
     */
    public String generateToken(Authentication authentication) {
        User user = (User) authentication.getPrincipal();
        Date now = new Date();
        // 设置过期时间
        Date expiryDate = new Date(now.getTime() + VALIDITY_IN_MILLISECONDS);
        //指定加密算法
        SecureDigestAlgorithm<SecretKey, SecretKey> algorithm = Jwts.SIG.HS256;
        //密钥实例
        SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes());
        return Jwts.builder()
                .subject(user.getUsername())
                .signWith(key, algorithm) //设置签名使用的签名算法和签名使用的秘钥
                .expiration(expiryDate) //设置过期时间
                .claim("username", user.getUsername()) //设置自定义负载信息
                .compact();
    }


    /**
     * 验证JWT
     * @param authToken
     * @return
     */
    public boolean validateToken(String authToken) {
        try {
            Jws<Claims> claimsJws = parseJWT(authToken);
            claimsJws.getPayload();
            return true;
        } catch (MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException e) {
            return false;
        }
    }

    /**
     * 解析jwt
     * @param authToken 登陆token
     * @return
     */
    public Jws<Claims> parseJWT(String authToken){
        //密钥实例
        SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes());
        return Jwts.parser()
                .verifyWith(key)  //设置签名的密钥
                .build()
                .parseSignedClaims(authToken); //设置要解析的jwt
    }

    /**
     * 从JWT中获取用户名
     * @param token
     * @return
     */
    public String getUsernameFromToken(String token) {
        Jws<Claims> claimsJws = parseJWT(token);
        return (String) claimsJws.getPayload().get("username", String.class);
    }
}

实现JWT过滤器

创建一个自定义的JWT过滤器,它负责检查每个请求的Authorization header中的Bearer Token,并对其进行验证。验证成功后,创建一个Authentication对象,并将其置于SecurityContext中。

import com.springboot.base.jwt.util.JwtTokenUtil;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @version 1.0
 * @description: <p></p >
 * @author: 码农Academy
 * @create: 2024/4/18 16:15
 */
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final UserDetailsService userDetailsService;
    private final JwtTokenUtil jwtTokenUtil;

    private static final String BEARER_PREFIX = "Bearer ";

     public JwtAuthenticationFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil) {
          this.userDetailsService = userDetailsService;
          this.jwtTokenUtil = jwtTokenUtil;
     }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String jwtToken = getTokenFromRequest(request);
        if (jwtToken != null && jwtTokenUtil.validateToken(jwtToken)) {
            String username = jwtTokenUtil.getUsernameFromToken(jwtToken);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            // 用于权限校验
            List<GrantedAuthority> authorities = userDetails.getAuthorities().stream()
                    .map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
                    .collect(Collectors.toList());

            AbstractAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userDetails, null, authorities
            );
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            SecurityContextHolder.getContext().setAuthentication(authentication);
        }else {
            // 如果出现异常,可能是无效的Token,此处可记录日志或做其他处理
            System.out.println("没有登陆。。。。。。");
        }

        filterChain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        String authHeader = request.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
            return authHeader.replace(BEARER_PREFIX, "");
        }
        return null;
    }
}

配置Spring Security

配置Spring Security以支持JWT身份验证。这通常涉及编写自定义的JWT过滤器,用于拦截请求并在请求头中提取JWT令牌进行验证。过滤器会在验证通过后将用户信息填充到Spring Security的上下文中。

import com.springboot.base.jwt.filter.JwtAuthenticationFilter;
import com.springboot.base.jwt.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @version 1.0
 * @description: <p></p >
 * @author: 码农Academy
 * @create: 2024/4/18 16:11
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests((requests) -> requests
                        .antMatchers("/user/login").permitAll() // 登录接口公开
                        .anyRequest().authenticated() // 其他接口需要验证
                )
                .addFilterBefore(new JwtAuthenticationFilter(userDetailsService, jwtTokenUtil), UsernamePasswordAuthenticationFilter.class) // 添加JWT过滤器
                .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // 关闭Session管理
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

在实现Spring Security的UserDetailsService的接口,用于登陆时查询用户信息,构造Spring Security的User信息。

import com.springboot.base.jwt.login.RoleEntity;
import com.springboot.base.jwt.login.UserEntity;
import com.springboot.base.jwt.repository.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @version 1.0
 * @description: <p></p >
 * @author: 码农Academy
 * @create: 2024/4/18 16:22
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) {
        // 根据用户名查询用户实体
        UserEntity userEntity = userRepository.findByUserName(username);

        // 如果用户不存在,则抛出异常
        if (userEntity == null) {
            throw new UsernameNotFoundException("User not found with username: " + username);
        }

        // 将用户实体转换为Spring Security可识别的UserDetails对象
        User user = new User(userEntity.getUserName(),
                userEntity.getPassword(), // 注意:此处假设password字段是已经经过加密处理过的
                userEntity.getEnabled(),
                true, true, true,
                getAuthorities(userEntity.getRoleList()) // 获取用户的角色并转换为GrantedAuthority列表
        );
        System.out.println(user.getPassword());
        return user;
    }

    private Collection<? extends GrantedAuthority> getAuthorities(List<RoleEntity> roles) {
        return roles.stream()
                .map(role -> new SimpleGrantedAuthority(role.getRoleCode()))
                .collect(Collectors.toList());
    }
}

上述使用Spring Security,如果有疑问的东西可以去查一下Spring Security的工作原理。

登陆接口

我们在定义一个登陆接口,登陆成功后返回jwt生成的token。

import com.springboot.base.jwt.request.LoginRequest;
import com.springboot.base.jwt.response.JwtResponse;
import com.springboot.base.jwt.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @version 1.0
 * @description: <p></p >
 * @author: 码农Academy
 * @create: 2024/4/18 17:31
 */
@RestController
@RequestMapping("/user")
public class LoginController {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private AuthenticationManager authenticationManager;

    @PostMapping("/login")
    public ResponseEntity<?> createAuthenticationToken(@RequestBody LoginRequest loginRequest) throws Exception {
        // 创建基于用户名密码的Authentication请求对象
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                loginRequest.getUserName(),
                loginRequest.getPassword()
        );

        try {
            // 使用AuthenticationManager进行身份验证
            Authentication authentication = authenticationManager.authenticate(authToken);

            // 如果身份验证成功,SecurityContextHolder会存储该Authentication对象
            SecurityContextHolder.getContext().setAuthentication(authentication);

            // 生成JWT Token或其他形式的授权凭证
            String token = jwtTokenUtil.generateToken(authentication);

            // 返回带有Token的响应
            return ResponseEntity.ok(new JwtResponse(token));

        } catch (BadCredentialsException ex) {
            // 处理用户名/密码不正确等错误情况
            System.out.println("密码不正确");
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
        } catch (DisabledException ex) {
            // 处理账户被禁用等情况
            System.out.println("处理账户被禁用");
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Account is disabled");
        } catch (AuthenticationException e) {
            System.out.println("Authentication failed");
            // 其他身份验证异常
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Authentication failed");
        }
    }

}
import com.fasterxml.jackson.annotation.JsonCreator;
import lombok.Data;

/**
 * @version 1.0
 * @description: <p></p >
 * @author: 码农Academy
 * @create: 2024/4/18 18:59
 */
@Data
public class JwtResponse {

    private String token;
    private String type = "Bearer";

    public JwtResponse(String token) {
        this.token = token;
    }

    @JsonCreator
    public static JwtResponse of(String token) {
        return new JwtResponse(token);
    }
}

我们模拟一下登陆发起请求:

登陆成功,返回token。

给一个错误密码,登陆失败:

我们将token设置到Authorization中,发起其他业务请求:

接口请求成功。

我们改变一下token值,在发起请求:

后台打印token校验失败,即未登录。

结论

JWT(JSON Web Tokens)作为一种轻量级且灵活的身份验证和授权机制,在现代web服务及移动应用中得到了广泛应用。它允许服务器端通过加密签名的方式向客户端发放安全的、自包含的令牌,这些令牌可以携带必要的用户身份信息和权限声明,而且由于其无需持久化存储的特性,非常适合于微服务架构下的无状态通信场景。

总结起来,采用JWT进行身份验证具有以下优点:

• 安全性: 通过密钥签名保证令牌的安全性,防止篡改。

• 高效性: 状态less设计减少了服务器端存储负担,提升了系统的可扩展性和响应速度。

• 跨域友好: JWT可轻松应用于多个域名或子系统间的认证需求。

• 自包含性: 令牌自身携带了足够的用户信息,减轻了服务器端查询数据库的频率。

• 有效期可控: 可以在JWT中设置过期时间,从而控制用户的登录会话持续时间。

不过需要注意的是,JWT并非适用于所有场景,尤其是在敏感数据的处理上,因为一旦令牌被截获,黑客们在有效期内可以持续使用。因此,在实施JWT方案时,应充分考虑其适用范围,并配合合适的刷新策略、黑名单机制以及其他安全措施,确保系统的整体安全性和用户体验。

责任编辑:武晓燕 来源: 码农Academy
相关推荐

2021-08-09 08:53:30

HTTP状态化协议

2024-04-03 15:33:04

JWTSession传输信息

2021-03-23 10:45:23

CookieSession前端

2023-12-06 09:10:28

JWT微服务

2024-09-29 09:50:05

2022-06-07 08:39:35

RPCHTTP

2024-04-26 12:45:39

JWTCookieSession

2019-08-05 14:23:43

DockerKubernetes容器

2024-07-11 10:41:07

HTTPSHTTP文本传输协议

2020-11-25 09:36:17

HTTPRPC远程

2021-08-26 06:58:14

CookieSession应用

2023-01-12 09:01:01

MongoDBMySQL

2021-03-29 09:25:58

JWTSpringNimbus

2023-07-07 09:08:21

2023-12-11 12:03:14

Python工具元组

2024-04-07 10:07:52

2015-09-01 09:28:55

calayeruiview区别

2021-12-20 10:30:33

forforEach前端

2022-09-13 08:44:02

IP网络MAC地址

2024-04-16 08:26:18

IP地址MAC地址
点赞
收藏

51CTO技术栈公众号