JWT应用

本文详细介绍了JWT(JSON Web Token)的实现过程,包括如何使用Spring Boot搭建环境,添加依赖,配置yml,创建模型,服务层,控制器,以及最重要的TokenUtils类进行JWT的构建和解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

以前我在开发App时,后端给我们的权限字符串是一个token,这个token很简单,疑似一个固定字符串经过base64编码,大约32个字符,并不长。每次我们向后端请求接口,都要带着这个字符串。可能是由于那时候没有做分布式吧,这个token只是用来做权限甄别,并不携带其他信息。

最近两年我做java服务端,开始也是用这种方式。这种token在单例部署服务中是可以的,后端生成token,和用户绑定,收到前端的接口请求,把token对比一下就放行。简单起到了权限检查的作用,但是安全性很差。为什么?因为没有签名算法,很容易造假。另外,在分布式系统中,token签发和校验可能不在同一个服务中,这就造成了用户信息丢失的问题。我们在分布式系统中,mysql和redis也有可能是分开的,这就需要我们的前端在获取token的时候,一并带上自己的用户信息,就像身份证一样。于是jwt就起到了这个作用。

jwt的最终结果是生成个字符串,所以它只是一个算法工具。可以不依赖web程序进行测试。不过由于它也只有在web程序中才能体现自己的价值,所以我的demo还是以springboot为基础来搭建的。

看看程序的结构:

可以看出,要测试jwt,只需要很简单几个文件。除了web服务常见的controller、service和model之外,就只有TokenUtils是重点了,jwt的构建和解析都在其中。

一、添加pom依赖

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.5</version>
        </dependency>

        <!-- java-jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

二、配置yml,我习惯改个端口

server:
  port: 6005

三、model文件User.java

这其中放置用户信息,假设是从数据库中查询得到的用户信息

package com.chris.jwt.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author chrischan
 * create on 2019/6/24 9:50
 * use for:
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private int id;
    private String username;
    private String password;
    private String[] permissions;

    public static User create(int id, String name, String password, String... permissions) {
        return new User(id, name, password, permissions);
    }
}

四、service层AccountService.java

package com.chris.jwt.service;

import com.chris.jwt.model.User;
import com.chris.jwt.utils.TokenUtils;
import org.springframework.stereotype.Service;

/**
 * @author chrischan
 * create on 2019/6/24 9:25
 * use for:
 */
@Service
public class AccountService {

    public Object login(String username, String password) {

        User user = User.create(1, username, password, "ROLE_ADMIN", "ROLE_USER");

        String sub = "jwt_test";

        //构建的时候把用户信息中的用户名
        return TokenUtils.build(user, sub, 600000, "username","permissions");
    }

    public String parseToken(String token, String field) {
        return String.valueOf(TokenUtils.parse(token).get(field));
    }

}

此次的目的是测试jwt,所以并没有涉及用户信息检验的逻辑,用户信息要加在jwt中,调用的时候传什么都视为合法,直接加进去。中间缺失的逻辑根据真实业务补充。

五、接口AccountController.java

package com.chris.jwt.api;

import com.chris.jwt.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author chrischan
 * create on 2019/6/24 9:22
 * use for:
 */
@RestController
@RequestMapping("/api")
public class AccountController {
    @Autowired
    AccountService accountService;

    /**
     * 登录 登陆成功则返回一个token
     *
     * @param username
     * @param password
     * @return
     */
    @GetMapping("/login")
    public ResponseEntity<?> login(String username, String password) {
        return ResponseEntity.ok(accountService.login(username, password));
    }

    @PostMapping("/parseToken")
    public ResponseEntity<?> parseToken(String token, String field) {
        return ResponseEntity.ok(accountService.parseToken(token, field));
    }
}

很简单的两个接口,一个用来模拟登陆,获得jwt,一个用来测试对token的解析。

六、最重要的部分,TokenUtils.java

jwt的构建和解析都在其中。

package com.chris.jwt.utils;

import com.chris.jwt.model.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.lang.reflect.Field;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * @author chrischan
 * create on 2019/6/24 9:48
 * use for:
 */
public class TokenUtils {
    /**
     * 指纹 最关键的东西 不可丢失
     */
    private static String secret = "ASFW56868UIIGNJ2356SKFH568DS856876FJK";

    /**
     * 工具初始化
     *
     * @param secret
     */
    public static void init(String secret) {
        //设置一个通用指纹 出入保持一致
        TokenUtils.secret = secret;
    }

    /**
     * 构建token 使用公用的指纹
     *
     * @param obj
     * @param subject
     * @param ttMillis
     * @param fieldNames
     * @param <T>
     * @return
     */
    public static <T> String build(T obj, String subject, long ttMillis, String... fieldNames) {
        return build(obj, TokenUtils.secret, subject, ttMillis, fieldNames);
    }

    /**
     * 构建token
     *
     * @param obj        需要添加到token中的用户对象
     * @param secret     指纹
     * @param subject    主题
     * @param ttMillis   过期时间
     * @param fieldNames 需要添加到token中的字段
     * @param <T>
     * @return
     */
    public static <T> String build(T obj, String secret, String subject, long ttMillis, String... fieldNames) {
        //签名算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //生成token的时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        //私有生命 最有用的部分 可携带信息
        Class<?> objClass = obj.getClass();
        Map<String, Object> claims = new HashMap<>(16);
        for (String fieldName : fieldNames) {
            Field field = null;
            try {
                field = objClass.getDeclaredField(fieldName);
                field.setAccessible(true);
                Object value = field.get(obj);
                field.setAccessible(false);
                claims.put(fieldName, value);
            } catch (Exception e) {
                continue;
            }
        }

        //构建
        JwtBuilder builder = Jwts.builder()
                .setClaims(claims)//刚发现这部分自定义的数据要首先设置,否则此前的设置都会消失
                .setId(UUID.randomUUID().toString())
                .setIssuer("chris") //签发人
                .setIssuedAt(now) //签发时间
                .setSubject(subject) //主题
                .signWith(signatureAlgorithm, secret);
        //添加过期时间
        if (ttMillis > 0) {
            long expMillis = nowMillis + ttMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }

        return builder.compact();
    }

    /**
     * 解析token 使用通用指纹
     *
     * @param tokenJson
     * @return
     */
    public static Claims parse(String tokenJson) {
        return parse(tokenJson, secret);
    }

    /**
     * 解析token 使用自定义指纹
     *
     * @param tokenJson
     * @param secret
     * @return
     */
    public static Claims parse(String tokenJson, String secret) {
        Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(tokenJson).getBody();
        return claims;
    }

    /**
     * token是否有效检查
     * 此处只对user做简单密码匹配校验
     *
     * @param tokenJson
     * @param user
     * @return
     */
    public static boolean isEffective(String tokenJson, User user) {
        String password = user.getPassword();
        Claims claims = parse(tokenJson, secret);
        //todo 检查过期
        Object password1 = claims.get("password");

        if (null != password1 && password.equals(password1)) {
            return true;
        }
        return false;
    }
}

jwt的构建,主要是操作JwtBuilder

        //构建
        JwtBuilder builder = Jwts.builder()
                .setClaims(claims)
                .setId(UUID.randomUUID().toString())
                .setIssuer("chris") //签发人
                .setIssuedAt(now) //签发时间
                .setSubject(subject) //主题
                .signWith(signatureAlgorithm, secret);

 

jwt的解析,主要是操作JwtParser 

Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(tokenJson).getBody();

七、运行测试

http://localhost:6005/api/login?username=zhangsan&password=123456

得到结果:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqd3RfdGVzdCIsInBlcm1pc3Npb25zIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwiaXNzIjoiY2hyaXMiLCJleHAiOjE1NzA2NzkxNDIsImlhdCI6MTU3MDY3ODU0MiwianRpIjoiMjk1MzI2N2QtNTNiZS00N2Y2LWFjNTYtMjI4M2FjMDFkNWQ4IiwidXNlcm5hbWUiOiJ6aGFuZ3NhbiJ9.LT9ngTFW8BARFnBmK-enzz37fjRc39sELFPKUXjDw2g

关于jwt的结构,此处不再赘述。jwt分三段,头部指明了算法,中间部分是有效载荷,放置的是我们有用的信息。我们构建jwt的时候设置的大多数都是这部分,包括用户名和权限列表,签发人、主题和有效时间等等。最后一部分是吧前两部分合起来通过指纹算法生成的,不可逆,可以用来做校验。这也是最关键的,因为使用的是摘要算法,无法解析,也就不可能在中途做假,我们携带的信息就无法被篡改。

三部分之间都是用英文句点来连接的。

1. 现在我们来看一来看头部分,用base64解析一下。

左边是解析之后的部分,起哦门可以看到其中包含算法方式信息。这个信息在解析的时候会很有用,系统会根据这个信息使用相同的算法来验证签名。

2. 我们来看看有效载荷部分解析之后的样子

有效载荷就是第二部分,解析之后我们可以看到我们曾经添加的东西,主题、权限、用户名、签发人,有效时间都在里面。我们可以解析出用户名,在分布式服务中直接使用,权限列表也可以直接在security中进行控制。

3. 我们调用一下解析接口,看看是否成功解析到我们想要的数据。我们把刚才得到的token传进去,获取用户名

http://localhost:6005/api/parseToken?token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqd3RfdGVzdCIsInBlcm1pc3Npb25zIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwiaXNzIjoiY2hyaXMiLCJleHAiOjE1NzA2NzkxNDIsImlhdCI6MTU3MDY3ODU0MiwianRpIjoiMjk1MzI2N2QtNTNiZS00N2Y2LWFjNTYtMjI4M2FjMDFkNWQ4IiwidXNlcm5hbWUiOiJ6aGFuZ3NhbiJ9.LT9ngTFW8BARFnBmK-enzz37fjRc39sELFPKUXjDw2g&field=username

猜猜结果是什么?

{
    "timestamp": "2019-10-10T04:04:42.682+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "JWT expired at 2019-10-10T11:45:42Z. Current time: 2019-10-10T12:04:42Z, a difference of 1140679 milliseconds.  Allowed clock skew: 0 milliseconds.",
    "path": "/api/parseToken"
}

提示这个token已经过期失效了。因为我是边 测试边编辑,10分钟的有效时间已经过了。这也刚好检验了一下我们的jwt对过期检查的逻辑。我们重新请求一个token来检测解析。

http://localhost:6005/api/parseToken?token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqd3RfdGVzdCIsInBlcm1pc3Npb25zIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwiaXNzIjoiY2hyaXMiLCJleHAiOjE1NzA2ODExMDYsImlhdCI6MTU3MDY4MDUwNiwianRpIjoiMTNiNDAxZTktZTNkMC00YTZjLWEyZTItYTc3YWQzMzQyYmIwIiwidXNlcm5hbWUiOiJ6aGFuZ3NhbiJ9.XqDfNclsygF4nxp5pHToIEGOAx5rQhcl8oBXFvvVQc4&field=username

看看PostMan请求的结果:

 我们成功解析获取到我们的用户名: zhangsan。

八、说明

jwt自身携带用户信息,在分布式服务中可以直接验证和提取用户信息,省去了再到用户账号中心进行检验的麻烦。加上合理的设置有效时间,也能基本做到和用户信息的同步。不过由于解析的时候要用到构建时的签名,构建和解析时需要使用相同的指纹secret,而这个指纹也是jwt安全的关键所在,一旦丢失,jwt就可以在前端被伪造出来。所以一定要保密,或者要以非常安全的方式来管理secret。

本例中构建和解析都在一起。分布式中构建则存在于签发服务中,解析在业务服务中,是分开的。只需要把TokenUtils.java中关于解析和过期检验的逻辑搬过去即可,切记解析使用的类(如果有,如User)和指纹secret要一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值