欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JWT的简单了解与使用

程序员文章站 2022-06-17 12:56:52
...

一、JWT简介

JWT(Json Web Token)的作用和优势在网络上有大量的资料,这里不再赘述,只简单介绍JWT的形成和使用。

JWT是由三段信息构成的(以.进行分割):

eyJhbGciOiJIUzI1NiJ9.

eyJzdWIiOiIxMjM0NTY3IiwiaWF0IjoxNTk4OTczNjUwLCJleHAiOjE1OTkyMzI4NTB9.

D2ZhxY0c6azChqiQ7m7I6wje7DIf6Cg2LnYZM8p9bMQ

  • header(头部)
    • 声明类型:固定是JWT
    • 声明加密算法:通常使用HMAC SHA256(HS256

类似:{‘typ’: ‘JWT’, ‘alg’: ‘HS256’}

将头部单独Base64编码,得到例子中的eyJhbGciOiJIUzI1NiJ9

  • playload(载荷):存放有效信息的地方(JWT一般不存放敏感信息
    • 标准中注册的声明(常用)
      • iss(issuer):表示JWT的签发者
      • sub(subject):表示JWT的所有者
      • aud(audience):表示JWT的接受者
      • exp(expiration):一个时间戳,表示JWT的过期时间
      • nbf(not before):一个时间戳,表示该JWT的生效时间,在这个时间之前验证JWT是会失败的
      • iat(issued at):一个时间戳,表示这个JWT的签发时间
      • jti(jwt id):JWT的唯一标识(也常用于redis的键)
    • 公共的声明(可以存放任何信息,通常用Map保存键值对,然后再加入其中)
    • 私有的声明(可以存放签发者和接收者共同定义的信息,不常用)

类似:{“sub”: “1234567890”, “name”: “John Doe”, “admin”: true}

将载荷单独Base64编码,得到例子中的eyJzdWIiOiIxMjM0NTY3IiwiaWF0IjoxNTk4OTczNjUwLCJleHAiOjE1OTkyMzI4NTB9

  • signature(签证):这个部分需要上面两个Base64编码后的结果,用.连接组成字符串,然后通过header中声明的加密方式(HS256)进行加盐secret组合加密(不可逆加密),然后就构成了jwt的第三部分。

加密得到:D2ZhxY0c6azChqiQ7m7I6wje7DIf6Cg2LnYZM8p9bMQ

secret是保存在服务器的,不能泄露,否则任何人都能够伪造合法的JWT。

JWT签发后发完客户端,客户端通常在请求头上加入Authorization字段,并加上Bearer(与JWT之间留有一个空格),并在之后的请求中,都带上此信息作为验证,如图:

JWT的简单了解与使用

二、Spring boot中的简单使用

在本文中,采用的是最新的Java JWT(jjwt)作为生成和处理JWT的工具类,GitHub官网

网上大多资料都是其旧版本的,有些方法已经不适用。

1、解决依赖

首先,引入依赖:

<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>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

2、创建工具类JwtUtil

public class JwtUtil {
    public static final long HOUR = 60 * 60 * 1000L;

    /**
     * jwt签证
     *
     * @param username    用户名(学号)
     * @param secret      **
     * @param expiredTime 过期时间
     * @return jwt
     */
    public static String encode(String username, String secret, int expiredTime) {
        long timeMillis = System.currentTimeMillis();
        // 生成**
        SecretKey key = new SecretKeySpec(secret.getBytes(), SignatureAlgorithm.HS256.getJcaName());
        return Jwts.builder()
                // 唯一用户名(学号),同时做redis的键
                .setSubject(username)
                // 签发时间
                .setIssuedAt(new Date(timeMillis))
                // 过期时间
                .setExpiration(new Date(timeMillis + expiredTime * HOUR))
                .signWith(key)
                .compact();
    }

    /**
     * 检验和解析jwt
     *
     * @param jwt    jwt
     * @param secret **
     * @return 数据实体
     * @throws JwtException token不合法
     */
    public static Claims parse(String jwt, String secret) throws JwtException {
        // 生成**
        SecretKey key = new SecretKeySpec(secret.getBytes(), SignatureAlgorithm.HS256.getJcaName());
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(jwt)
                .getBody();
    }

    /**
     * 获取用户名(学号)
     */
    public static String getSubject(String jwt, String secret) throws JwtException {
        return parse(jwt, secret).getSubject();
    }
}

在parse()方法中,如果JWT不合法,则会抛出JwtException,省去了JWT检验的方法,请看官方例子:

try {
    Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compactJws);
    //OK, we can trust this JWT
} catch (JwtException e) {
    //don't trust the JWT!
}

而在getSubject()方法中的getSubject()方法,如果JWT已过期,则抛出ExpiredJwtException(JwtException的子类),这样也不必检验JWT是否过期。

对于上述抛出的两个异常,我们可以在全局异常中心CenterExceptionHandler进行处理:

@RestControllerAdvice
public class CenterExceptionHandler {
    /**
     * 令牌过期异常
     */
    @ExceptionHandler(ExpiredJwtException.class)
    SkyResponse expiredJwtException(ExpiredJwtException e) {
        return SkyResponse.fail(HttpStatus.INTERNAL_SERVER_ERROR, "令牌过期");
    }

    /**
     * 令牌无效
     */
    @ExceptionHandler(JwtException.class)
    SkyResponse signatureException(JwtException e) {
        return SkyResponse.fail(HttpStatus.INTERNAL_SERVER_ERROR, "令牌无效");
    }
    
    // 省略其他异常处理
    // .....
}

3、定义JwtService

为了更规范的使用(自我感觉规范…),我增加了一层JwtService,进行其他业务操作:

@Component
public class JwtService {
    @Value("${token.secret}")
    private String secret;

    @Value("${token.expiredTime}")
    private int expiredTime;

    @Resource
    RedisUtils redisUtils;

    DurianUserService userService;

    @Autowired
    public void setUserService(DurianUserService userService) {
        this.userService = userService;
    }

    /**
     * 生成token,并将相关的用户信息存入redis
     *
     * @param username username
     * @return token
     */
    public String createToken(String username) {
        //检查redis中是否已存在该用户的登录信息,解决重复登陆的问题
        if (redisUtils.getObject(username) == null) {
            // 从数据库中得到用户
            DurianUserDO loginUser = userService.getUserByUsername(username);
            // 存入redis
            redisUtils.setObject(loginUser.getUsername(), loginUser, expiredTime, TimeUnit.HOURS);
        }
        return JwtUtil.encode(username, secret, expiredTime);
    }


    /**
     * 刷新token和redis中的过期时间
     *
     * @param token 旧token
     * @return 新token
     */
    public String refreshToken(String token) throws JwtException {
        Claims claims = JwtUtil.parse(token, secret);
        long expiration = claims.getExpiration().getTime();
        long timeMillis = System.currentTimeMillis();
        // 过期时间小于1小时则刷新
        if (expiration - timeMillis < JwtUtil.HOUR) {
            String username = claims.getSubject();
            DurianUserDO user = redisUtils.getObject(username);
            redisUtils.deleteObjects(username);
            redisUtils.setObject(username, user, expiredTime, TimeUnit.HOURS);
            return JwtUtil.encode(username, secret, expiredTime);
        }
        return token;
    }

    /**
     * 刷新redis中的用户信息,并返回最新的token
     *
     * @param token token
     * @param user  最新的用户信息
     * @return 最新的token
     */
    public String refreshUser(String token, DurianUserDO user) throws JwtException {
        String username = JwtUtil.getSubject(token, secret);
        redisUtils.deleteObjects(username);
        redisUtils.setObject(username, user, expiredTime, TimeUnit.HOURS);
        return JwtUtil.encode(username, secret, expiredTime);
    }

    /**
     * 从redis中删除用户信息
     *
     * @param token token
     */
    public void deleteUser(String token) throws JwtException {
        String username = JwtUtil.getSubject(token, secret);
        redisUtils.deleteObjects(username);
    }

    /**
     * 从redis中获取token中用户的信息
     *
     * @param token token
     * @return 用户实体
     */
    public DurianUserDO getUserFromRedis(String token) throws JwtException {
        String username = JwtUtil.getSubject(token, secret);
        return redisUtils.getObject(username);
    }

    /**
     * 从token中获取用户名
     *
     * @param token token
     * @return 用户名
     */
    public String getUsername(String token) throws JwtException {
        return JwtUtil.getSubject(token, secret);
    }
}

其中,将secretexpiredTime的值都放到yml配置文件中,后续也更便于更改

4、登陆接口LoginController

最终,是我们对外的登陆接口定义了:

@RestController
public class LoginController {
    @Resource
    private JwtService jwtService;

    private DurianUserService userService;

    @Autowired
    public void setUserService(DurianUserService userService) {
        this.userService = userService;
    }

    /**
     * 登陆接口,登陆成功返回jwt
     */
    @PostMapping("/login")
    public SkyResponse login(String username, String password) {
        DurianUserDO loginUser = userService.getUserByUsername(username);
        if (loginUser == null) {
            throw new RuntimeException("用户不存在,请重新输入");
        }
        if (!new BCryptPasswordEncoder().matches(password, loginUser.getPassword())) {
            throw new RuntimeException("密码不正确,请重新输入");
        }
        String newToken = jwtService.createToken(username);
        return SkyResponse.success("登陆成功", 1)
                .put("token", newToken);
    }

    /**
     * 注销登陆接口
     */
    @PostMapping("/logout")
    public SkyResponse logout(HttpServletRequest request) {
        String token = TokenUtils.getToken(request);
        if (StringUtils.isNotEmpty(token)) {
            jwtService.deleteUser(token);
            return SkyResponse.success("登出成功");
        }
        return SkyResponse.fail(HttpStatus.NOT_ACCEPTABLE, "当前还未登陆");
    }
}

到这里,就完成了JWT签发和处理的所有流程了,后续加上Spring Security和单点登陆,就更完善了。