Jwt新手入门教程

Jwt的新手入门教程

1.Jwt究竟是什么东东?

​ 先贴官网地址:JSON Web Tokens - jwt.io

image-20210921201459885

​ 再贴官方的定义:

What is JSON Web Token?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

Although JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.

​ 我的理解总结:Jwt全称是Json Web Token,顾名思义就是一种通过Json方式来传输的令牌,并且他最大的特点就是signed tokens,签发者可以使用各种的加密方式对信息进行签名,更重要的是Jwt还能验证token是否被篡改或者token是否正确。当然了,这种方式注定是不安全的。我们很容易地就可以在官网得到这一结论。

image-20210921203154437

​ 可以看到,只要我们获得token字符串,就可以获取到里面的大部分信息,除了签名的密钥。

所以Jwt简单来说,就是简单地存储部分不那么重要的信息,通过Json,对客户端进行验证的一种方式。

2.Jwt的组成

​ 从官网的Debugger界面,我们可以得知,Jwt由三部分组成。

image-20210921203716689

  • 第一部分:Header,Header通常由令牌的类型和加密的算法组成,也就是
{
  "alg": "HS256",
  "typ": "JWT"
}

这个Header的含义就是"alg"--Algorithm(算法) 是HS256。

  • 第二部分: Payload,这部分主要是记录我们所存储的简单且不重要的信息。例如:用户名,过期时间,用户id等等。
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

但是要注意的是,Payload记录的这些信息是完全公开的,所以千万不能把用户或者系统的敏感数据放到Payload中,Payload只是负责记录简单信息,并不具备加密的功能。

  • 第三部分:Signature, 签名里是由三部分组成,Header的Base64编码,Payload的Base64编码,还有secret,然后通过指定的加密方式,例如HS256,进行加密后得出的字符串。
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

可以说这部分是Jwt的重点,因为他承担着两个作用,第一个:验证JwtToken在传输过程中没有受到篡改 第二个:验证签发人的身份

​ 详细说一下这两个作用。

验证JwtToken在传输过程中没有受到篡改,这个原理就比较好理解,因为Signature中有一个字符串,也就是secret,这个secret是由我们来设置的,相当于私钥,只有我们自己才知道,别人是不知道的。那么在后续验证过程中,只要我们自己生产的Token与客户端传来的Token进行比对,如果一致那就证明该Token没有受到篡改,反之则证明客户端传来的Token是非法的。

验证签发人的身份,其实第二个作用是从第一个作用中展现而来的,因为如果能证明Token在传输过程中没有受到篡改,那就更加说明服务端是这个签名的签发者,因为只有签发者才知道私钥

​ 其实secret我们也可以认为是盐(salt),我们知道如果单纯地在数据库中存储明文密码,或者是只经过一重MD5加密的密码,是非常的不安全,因为就算是经过MD5加密,仍然可以通过暴力穷举的方式来进行破解,可是如果在用户密码的基础上,加上我们生成的随机或固定的字符串,然后再进行加密,那么安全程度会大大提升。

​ 如果不知道盐(salt)的童鞋,可以去bind搜索一下相关资料,其实理解起来就是用户密码+我们设置的随机或固定字符串再进行多重加密。

3.编码实现

​ 我们来创建这几个类。

image-20210921215324634

配置类:InterceptorConfig

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/login");
    }

    @Bean
    public JwtInterceptor authenticationInterceptor() {
        return new JwtInterceptor();
    }

}

控制类: UserController

@RestController
public class UserController {


    @PostMapping("/login")
    public Map<String, Object> login(@RequestBody User user) {

        Map<String, Object> map = new HashMap<>();
        String username = user.getUsername();
        String password = user.getPassword();
        // 省略 账号密码验证
        // 验证成功后发送token
        String token = JwtUtil.sign(username, password);
        if (token != null) {
            map.put("code", "200");
            map.put("message", "认证成功");
            map.put("token", token);
            return map;
        }
        map.put("code", "403");
        map.put("message", "认证失败");
        return map;
    }
    
    @GetMapping(value = "/api/test")
    public String get(){
        System.out.println("执行了get请求");
        return "success";
    }
}

服务类:UserService

@Service
public class UserService {

    public String getPassword(){

        return "admin";
    }
}

实体类 User

@Data
public class User {

    private String username;
    private String password;

}

自定义拦截器类 JwtIntercepter

@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从 http 请求头中取出 token
        String token = request.getHeader("token");
        // 如果请求不是映射到方法直接通过
        if(!(handler instanceof HandlerMethod)){
            return true;
        }
        if (token != null){
            String username = JwtUtil.getUserNameByToken(request);
            // 这边拿到的 用户名 应该去数据库查询获得密码,简略,步骤在service直接获取密码
            boolean result = JwtUtil.verify(token,username,userService.getPassword());
            if(result){
                System.out.println("通过拦截器");
                return true;
            }
        }
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

Jwt工具类 JwtUtil

public class JwtUtil {

    // Token一天后过期
    public static final long EXPIRE_TIME = 1000 * 60 * 60 * 24;


    //检验Token是否正确
    public static boolean verify(String token, String username, String secret) {
        try {
            // 设置加密算法
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            // 效验TOKEN
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }


    public static String sign(String username, String secret) {
        //现在系统的时间 + 一天
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        //对密码进行加密
        Algorithm algorithm = Algorithm.HMAC256(secret);
        // 附带username信息
        return JWT.create()
                .withClaim("username", username)
                .withExpiresAt(date)
                .sign(algorithm);
    }
    
    public static String getUserNameByToken(HttpServletRequest request)  {
        String token = request.getHeader("token");
        DecodedJWT jwt = JWT.decode(token);
        return jwt.getClaim("username")
                .asString();
    }
    
}

具体代码,我上传到github上:Alickx/JwtTokenDemo: JwtDemo代码 (github.com)

这里代码的secret则是用户的密码。

我们先通过PostMan向/login接口发送我们的账号密码,得到Jwt根据我们的账号密码生成的token。

{
    "code": "200",
    "message": "认证成功",
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MzIzMTg1NjksInVzZXJuYW1lIjoiYWRtaW4ifQ.c_m3z11UOcFcS_hZN9KNlidzZ2j6y_Ugkb9awHQ3FGY"
}

image-20210921221458979

这里认证成功后,服务器如果不经过我们其他的存储操作,是不会对生成的token进行持久化或其他控制的,所以一旦签发出去,这个token串就会变成无状态了。

​ 接着,我们访问一下我们的api测试接口,因为我们在config里配置了全局拦截器,且重写了HandlerInterceptor类,除了/login路径,其他全路径都需要请求的Header(请求头)中带有token字段,且需验证token成功后才会允许访问,否则进行拦截。

image-20210921222244047

4.管理JwtToken的状态

​ 要做到管理JwtToken的状态,我们可以通过把token存储到Redis数据库中,通过设置key的过期时间,就可以做到对Jwt的过期操作,同时也能够对Token进行续签,失效等等操作。这部分先不去仔细探究,有个思路就可以了。具体的编码实现我相信也不难。

本文中所有的代码均已上传到github上,如有需要请下载。

Alickx/JwtTokenDemo: JwtDemo代码 (github.com)

JwtTokenDemo (gitee.com)

posted @ 2021-09-21 22:39  Alickx  阅读(9095)  评论(0编辑  收藏  举报