JWT使用

什么是JWT?

JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。 从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。

JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。

并且, 使用 JWT 认证可以有效避免 CSRF 攻击,因为 JWT 一般是存在在 localStorage 中,使用 JWT 进行身份验证的过程中是不会涉及到 Cookie 的。

JWT组成

JWT由三部分组成:Header(标头),Payload(有效载荷),Signature(签名),中间用点分开,如:Header.Payload.Signature

Header:

Header(标头)通常由两部分组成:令牌的类型(JWT)和所使用的签名算法(如HMAC SHA256或RSA),被Base64Url编码,形成JWT的第一部分。

Payload:

Payload(有效载荷)包含声明。声明是关于实体和附加数据的声明,实体一般指的就是用户,声明一般存放一些不敏感的信息,比如用户名、权限、角色等等;声明分为三种:Registered claim、Public claims、Private claims

Registered claim(已注册声明):就是官方预先定义好的几个推荐使用的声明

  1. iss (issuer):jwt发行人,指的是谁发行的这个jwt签名

  2. exp (expiration time):jwt的有效期时间,设置这个签名多久后失效,但是这个时间设置的必须要比iat设置的签发时间大

  3. sub (subject):主题

  4. aud (audience):受众,指谁来接受jwt

  5. nbf (Not Before):生效时间,指定在什么时间jwt,才开始有效

  6. iat (Issued At):签发时间

  7. jti (JWT ID):编号,jwt唯一标识

Public claims(公共声明):使用 JWT 的人可以随意定义这些声明,可以声明一些有效的用户信息,比如:用户ID、姓名等,但是不要在次声明敏感的信息,比如密码,还有一点就是为了因为可以随意定义,为了避免冲突,应该在jwt注册表中进行注册

Private claims(私有声明):表示既不是已注册声明,也不是公开声明,而是发行人和受众人一起定义的熟悉的声明

Signature:

Signature(签名)要创建签名部分,您必须获取编码的标头、编码的有效负载、密钥、标头中指定的算法,并对其进行签名。

例如,如果您想使用 HMAC SHA256算法,签名将按以下方式创建:

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

上面的JSON将会通过HMACSHA256算法结合secret进行加盐签名(私钥加密),其中header和payload将通过base64UrlEncode()方法进行base64加密然后通过字符串拼接 "." 生成新字符串,最终生成JWT的第三部分。

注意:因为secret保存在我们的服务器端,jwt也是在服务器端签发的,而jwt的签发和验证都要使用到secret,所以,可以把secret理解为我们的服务器私钥,我们不能任何场景展示我们的私钥。

Spring Boot使用JWT

1.引入依赖

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.17</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.waitforme</groupId><!--根据你自己的来-->
    <artifactId>jwtdemo</artifactId><!--根据你自己的来-->
    <version>1.0</version><!--根据你自己的来-->
    <name>jwtdemo</name><!--根据你自己的来-->
    <properties>
        <java.version>17</java.version><!--根据你自己的来-->
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.49</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
    </dependencies>

2.创建JWT工具类

public class JWTUtil {
  private static SecretKey secretKey = Keys.hmacShaKeyFor("qyPWD9YVe8Q7gduIHJNtsz3Kb8Jui4nf".getBytes());
  //sgin使用的加密密钥
  /**
   * 创建token
   *
   * @param id
   * @param account
   * @return
   */
  public static String createToken(Integer id, String account) {
    String jwt = Jwts.builder()
      .setExpiration(new Date(System.currentTimeMillis() + 3600000))    //设置过期时间,3600000毫秒为1小时
      .setSubject("LOGIN_USER")   //设置主题,可以不加
      .claim("id", id)
      .claim("account", account)
      //claim添加自己自定义的字段
      .signWith(secretKey, SignatureAlgorithm.HS256)     //设置签名;参数1:密钥;参数2:签名算法
      .compact();
    return jwt;
  }
​
  /**
   * 解析token
   *
   * @param token
   */
  public static Claims parseToken(String token) {
    if (token == null) {
      //token为空判断
      throw new CustomException("未登录");
    }
    try {
      JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(secretKey).build();//构建jwt解析器,并设置签名密钥
      return jwtParser.parseClaimsJws(token).getBody();//解析我们传入的token,并将得到的claims对象返回
      //如果有异常就是验证失败
    } catch (ExpiredJwtException e) {
      //过期处理
      throw new CustomException("token已过期");
    } catch (JwtException e) {
      //其他异常统一处理
      throw new CustomException("token非法");
    }
  }
}

3.编写登录service,及其实现

service

public interface IUserService extends IService<User> {
  User getUserInfo(Integer id);//根据用户id获取登录用户信息
​
  String login(String account, String password);//登录方法
}
serviceImpl

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
​
  @Override
  public User getUserInfo(Integer id) {
    return this.baseMapper.selectUserInfoById(id);
  }
​
  @Override
  public String login(String account, String password) {
    User user = this.baseMapper.selectUserByAccount(account);
    if (user == null) {
      //先判断这个账号是否存在
      throw new CustomException("账号不存在");
    }
    if (!user.getPassword().equals(DigestUtils.md5Hex(password))) {
      //如果存在在判断密码是否正确,这里使用了DigestUtils的MD5算法来验证的密码,并且,数据库中保存的密码也要时MD5加密的
      throw new CustomException("用户名或密码错误");
    }
    //调用jwtUtil的createToken方法并传入id、account作为用户的唯一标识,生成jwt返回
    return JWTUtil.createToken(user.getId(), user.getAccount());
  }
}

4.编写Mapper及其xml实现

Mapper

public interface UserMapper extends BaseMapper<User> {
​
  User selectUserByAccount(@Param("account") String account);
​
  User selectUserInfoById(Integer id);
}
Mapper.xml
<select id="selectUserByAccount" resultType="com.waitforme.jwtdemo.pojo.User">
    select id,
           account,
           password,
           name,
           sex,
           age
    from user
    where account = #{account}
</select>
<select id="selectUserInfoById" resultType="com.waitforme.jwtdemo.pojo.User">
    select id,
           account,
           name,
           sex,
           age
    from user
    where id = #{id}
</select>

5.创建LoginUserHolder工具类

主要的作用就是从在拦截器拦截的时候,获取jwt中playload保存的用户唯一标识,保存在ThreadLocal中,供其他方法获取登录的用户信息

首先创建一个LoginUser对象,用来保存登录的用户唯一标识

@Data
public class LoginUser {
  private Integer id;
  private String account;
​
  public LoginUser(Integer id, String account) {
    this.id = id;
    this.account = account;
  }
}

6.在创建LoginUserHolder工具类

public class LoginUserHolder {
  public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();
​
  public static void setThreadLocal(LoginUser loginUser) {
    threadLocal.set(loginUser);//设置线程变量的值
  }
​
  public static LoginUser getLoginUser() {
    return threadLocal.get();//得到线程变量的值
  }
​
  public static void clear() {
    threadLocal.remove();//情况线程变量
  }
}

7.编写自定义的interceptor拦截器

@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String token = request.getHeader("token");
    //从http请求头中获取key为access-token的值
    Claims claims = JWTUtil.parseToken(token);
    //对token进行解析,并得到claims对象
    Integer id = claims.get("id", Integer.class);
    String account = claims.get("account", String.class);
    //得解析创建jwt传入的登录用户的唯一标识
    LoginUserHolder.setThreadLocal(new LoginUser(id, account));
    //把得到的id和userName存到ThreadLocal,供其他方法获取登录的用户信息
    return true;
    //直接返回true是因为,jwt里面对token已经进行了响应异常处理,也就是没有出现异常就是成功了,我们直接返回true
  }
​
  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    LoginUserHolder.clear();
    //在请求处理完后,清理掉登录的用户信息,做这一步的原因是因为mvc用了线程池的技术,线程使用完并不会被销毁,而是会回到线程池等待分配下一次任务
  }
}

8.注册自定义的拦截器

@Configuration
public class WebConfiguration implements WebMvcConfigurer {
  @Autowired
  AuthenticationInterceptor authenticationInterceptor;
​
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    //注册自定义的拦截器
    registry.addInterceptor(this.authenticationInterceptor)
      //拦截请求的范围
      .addPathPatterns("/**")
      //放行请求的范围
      .excludePathPatterns("/login/**", "/logout/**");
  }
}

9.在controller层编写获取登录用户信息的方法

@RestController
public class LoginController {
  private static final Logger log = LogManager.getLogger(LoginController.class);
  @Autowired
  IUserService userService;
​
  @RequestMapping("/login")
  public Result login(String account, String password) {
    String jwt = userService.login(account, password);
    log.info("登录时的线程变量:" + LoginUserHolder.getLoginUser());
    return Result.success("登录成功", jwt);
  }
​
  @RequestMapping("/getUserInfo")
  public Result info() {
    LoginUser loginUser = LoginUserHolder.getLoginUser();
    User userInfo = userService.getUserInfo(loginUser.getId());
    log.info("获取登录用户信息时的线程变量:" + LoginUserHolder.getLoginUser());
    return Result.success("获取成功", userInfo);
  }
​
  @RequestMapping("/logout")
  public Result logout() {
    LoginUserHolder.clear();
    log.info("退出登录时的线程变量:" + LoginUserHolder.getLoginUser());
    return Result.success("成功");
  }
}

测试(使用ApiFox)

1.打开apiFox创建一个项目

2.在这个项目里面新建三个接口:登录,获取登录用户信息,注销

创建登录接口:

登录配置:

配置环境:注:只需配置一次,创建获取登录用户信息的时候,不用配置了,直接选择测试环境即可

发送测试,并复制得到的jwt:

创建获取登录用户信息接口:

发送测试,并在Headers中添加从登录接口获取到的jwt:

创建注销接口:

三个接口调用完之后查看idea控制台的线程变量日志输出


错误案例

此错误表示,我们的密钥太弱了,用于设置签名的密钥必须要是大于等于signWith中设置的算法大小

.signWith(secretKey, SignatureAlgorithm.HS256) //256就是算法的字节大小

如果我们设置的密钥是用英文字母和数字组成的,需要设置32位的密钥,因为一个英文字母是一个字节,一个字节是八位

解决方法:

修改密钥大小位32位或更大

private static SecretKey secretKey = Keys.hmacShaKeyFor("qyPWD9YVe8Q7gduIHJNtsz3Kb8Jui4nf".getBytes());

### JWT 使用教程详解 #### 1. JWT 基础概念 JSON Web Token (JWT) 是一种开放标准 RFC 7519,用于在网络应用环境间安全地传输信息[^1]。它是一个紧凑且自包含的字符串,能够传递声明(Claims),这些声明通常被用来在各方之间传达身份验证和其他用户相关信息。 JWT 的主要组成部分包括三部分:Header、Payload 和 Signature。每一部分都通过 Base64Url 编码并用点号 `.` 进行分隔[^1]。 #### 2. JWT 结构分析 - **Header**: 描述令牌元数据,通常是加密算法的信息。 - **Payload**: 存储实际的数据或声明,可以分为注册声明(Registered Claims)、公共声明(Public Claims)和私有声明(Private Claims)。 - **Signature**: 通过对前两部分进行签名来确保数据未被篡改。 具体生成过程如下: ```plaintext token = base64urlEncode(header) + "." + base64urlEncode(payload) + "." + HMACSHA256(base64urlEncode(header) + "." + base64urlEncode(payload), secretKey) ``` #### 3. 工作原理 当客户端向服务器发送请求时,可以通过 HTTP 头部字段中的 `Authorization` 来携带 JWT。例如: ```http GET /api/resource HTTP/1.1 Host: example.com Authorization: Bearer <token> ``` 服务器接收到此请求后会解析并验证该令牌的有效性和合法性。 #### 4. 实现方式举例 ##### Python 中的实现 Python 可以利用 PyJWT 库轻松完成 JWT 的编码与解码操作。下面展示了一个简单的例子[^4]: ```python import jwt from datetime import datetime, timedelta def create_jwt_token(secret_key="your_secret", algorithm="HS256"): payload = { "sub": "1234567890", "name": "John Doe", "iat": int(datetime.utcnow().timestamp()), "exp": int((datetime.utcnow() + timedelta(minutes=30)).timestamp()) } encoded_jwt = jwt.encode(payload, secret_key, algorithm=algorithm) return encoded_jwt def decode_jwt_token(encoded_jwt, secret_key="your_secret", algorithms=["HS256"]): try: decoded_payload = jwt.decode(encoded_jwt, secret_key, algorithms=algorithms) return decoded_payload except jwt.ExpiredSignatureError as e: return f"Token expired: {e}" except jwt.InvalidTokenError as e: return f"Invalid token: {e}" if __name__ == "__main__": token = create_jwt_token() print(f"Encoded JWT: {token}") decoded_data = decode_jwt_token(token) print(f"Decoded Data: {decoded_data}") ``` ##### Laravel 中的应用 Laravel 框架下集成 Tymon\JWTAuth 扩展包可方便管理基于 JWT 的认证逻辑[^2]。首先需安装依赖库并通过配置文件设置别名以便快速调用相关方法: ```php // 配置 aliases 数组中加入以下内容 'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth', 'JWTFactory' => 'Tymon\JWTAuth\Facades\JWTFactory', // 创建 Token 示例 $user = Auth::user(); $token = JWTAuth::fromUser($user); // 验证 Token 示例 try { $user = JWTAuth::parseToken()->authenticate(); } catch (\Tymon\JWTAuth\Exceptions\TokenExpiredException $e) { // Token过期处理... } ``` #### 5. 安全注意事项 尽管 JWT 提供了一种灵活的身份验证机制,但在实际开发过程中仍需要注意一些潜在的安全隐患。比如避免泄露敏感信息到 Payload 或者 Header 当中;始终保护好 Secret Key 不被暴露给未经授权方等等[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值