登录令牌JWT — JSON WEB TOKEN 原创

java厂长
发布于 2022-1-18 13:49
浏览
1收藏

登录令牌JWT — JSON WEB TOKEN

春节不停更,此文正在参加「星光计划-春节更帖活动」

关于作者

  • 作者介绍

🍓 博客主页:作者主页

🍓 简介:JAVA领域优质创作者🥇、一名在校大三学生🎓、在校期间参加各种省赛、国赛,斩获一系列荣誉🏆。

🍓 关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿👨‍💻。


JWT简介

1、概述

传统的Web应用中,使用session来存在用户的信息,每次用户认证通过以后,服务器需要创建一条记录 保存用户信息,通常是在内存中。

  • 随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大
  • 由于Session是在内存中的,这就带来一些扩展性的问题
  • 当我们想要扩展我们的应用,让我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题
  • 需要客户端(浏览器)中使用cookie存储session的ID值,但是移动端设备没有cookie

登录令牌JWT — JSON WEB TOKEN-鸿蒙开发者社区

2、什么是JWT?

JWT是是目前最流行的跨域认证解决方案,本文介绍它的原理和用法。并且是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。

JWT就是token的一种具体实现方式,其全称是JSON Web Token

官网地址:https://jwt.io/

先来说一下基本的流程:

  1. 跨域是一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域,要想跨域肯定要实现不同的端到端的通信。
  2. 客户端使用用户名和密码进行登录
  3. 服务端收到请求,验证客户端的用户名和密码
  4. 验证成功后,服务端会签发一个类似于钥匙一样的私钥token,再把这个token返回给客户端
  5. 客户端收到token后可以把它存储到cookie、session、redis等
  6. 客户端每次向服务端请求资源时需要携带服务端签发的token,可以在cookie、header中携带
  7. 服务端接收到请求,然后验证客户端请求里面是否携带着token,如果验证成功,就向客户端返回请求数据

与传统的认证方式相比有哪些好处

  • json的通用性非常好,JWT支持多种语言实现,如JAVA,JavaScript,JS,PHP等很多语言都可以使用。
  • 因为有了payload部分,可以携带非敏感信息。
  • 方便信息传递,jwt的组成简单,占用字节小。
  • 易于应用的扩展,不需要在服务端保存会话信息。

原理图

登录令牌JWT — JSON WEB TOKEN-鸿蒙开发者社区

1、前端也就是客户端,通过表单提交用户名和密码信息发送到后端(服务的)

2、后端(服务端)验证该用户的用户名和密码是否正确,验证通过通过代码规定生成相对应的token令牌,token令牌将包含用户的数据i西信息作为Payload,与JWT Header分别进行Base64编码拼接后签名,生产类似于zzz.sss.rrr的字符串

3、后端(服务端)将生成的token作为判断用户登录成功的依据

4、前端拿到后端发来的token令牌后存储起来,等下一次用户需要再次请求服务器时,该用户将携带token(未过期的)请求服务器端以获取数据

5、后端拦截该用户的请求,判断token是否过期,未过期则执行业务逻辑,返回用户需要的数据

6、后端拦截该用户的请求,判断token是否过期,token令牌过期则返回错误的登录信息,这是需要后端再次生成token令牌,此时又会回到步骤1。

<font color=“red”>注意,session和JWT的主要区别就是保存的位置,session是保存在服务端的,而JWT是保存在客户 端的,JWT就是一个固定格式的字符串</font>

3、结构

JWT固定各种的字符串,由三部分组成:

  • Header,头部
  • Payload,载荷
  • Signature,签名

<font color=“red”>注意,把这三部分使用点(.)连接起来,就是一个JWT字符串</font>

登录令牌JWT — JSON WEB TOKEN-鸿蒙开发者社区

1)头部

header一般的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。 JWT里验证和签名使用的算法列表如下:

JWS 算法名称
HS256 HMAC256
HS384 HMAC384
HS512 HMAC512
RS256 RSA256
RS384 RSA384
RS512 RSA512
ES256 ECDSA256
ES384 ECDSA384
ES512 ECDSA512

例如,

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

2)载荷

payload主要用来包含声明(claims ),这个声明一般是关于实体(通常是用户)和其他数据的声明。 声明有三种类型:

  • registered
  • public
  • private

具体如下:

Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。

iss: jwt签发者

sub: jwt所面向的用户

aud: 接收jwt的一方

exp: jwt的过期时间,这个过期时间必须要大于签发时间

nbf: 定义在什么时间之前,该jwt都是不可用的

iat: jwt的签发时间

jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

Public claims : 可以随意定义

  • 自定义数据:存放在token中存放的key-value值

Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明

例如:

{
"iss": "sxau",
"iat": 1446593502,
"exp": 1446594722,
"aud": "sxau.edu.com",
"sub": "1016942589@qq.com",
"username": "admin"
}

<font color=“red”>注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的</font>

把头部和载荷分别进行Base64编码之后得到两个字符串,然后再将这两个编码后的字符串用英文句号 . 连接在一起(头部在前),形成新的字符串:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MmI2OWNlZC02YWNlLTRmYzAtOTk5MS00Y.WUwMjIxODQ0OTciLCJleHAiOjE2MDYwNTQzNjl9

3)签名

最后,将上面拼接完的字符串用HS256算法进行加密,在加密的时候,还需要提供一个密钥(secret)。 加密后的内容也是一个字符串,这个字符串就是签名

把这个签名拼接在刚才的字符串后面就能得到完整的JWT字符串。 header部分和payload部分如果被篡改,由于篡改者不知道密钥是什么,也无法生成新的signature部分, 服务端也就无法通过。 在JWT中,消息体是透明的,使用签名可以保证消息不被篡改。 例如,使用HMACSHA256加密算法,配合秘钥,将前俩部进行加密,生成签名

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

例如,将Header、Payload、Signature三部分使用点(.)连接起来

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MmI2OWNlZC02YWNlLTRmYzAtOTk5MS00Y WUwMjIxODQ0OTciLCJleHAiOjE2MDYwNTQzNjl9.DNVhr36j66JpQBfcYoo64IRp84dKiQeaq7axHTBcP9 E

例如,使用官网提供的工具,可以对该JWT进行验证和解析

登录令牌JWT — JSON WEB TOKEN-鸿蒙开发者社区

<font color=“red”>注意,在代码中,我们使用JWT封装的工具类,也可以完成此操作</font>

4、使用

在springboot中可以很容易的使用JWT,只需要引入相关依赖,封装一个JWT的工具类,并且编写 Controller的拦截器,对指定路径进行拦截验证token即可。

1)新建项目springboot-jwt

登录令牌JWT — JSON WEB TOKEN-鸿蒙开发者社区

2)pom文件中,引入操作jwt相关依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zmz</groupId>
    <artifactId>springboot-jjwt</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springboot-jjwt</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </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>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3)Controller层

package com.zmz.springbootjjwt.api;

import com.alibaba.fastjson.JSONObject;
import com.zmz.springbootjjwt.annotation.UserLoginToken;
import com.zmz.springbootjjwt.entity.User;
import com.zmz.springbootjjwt.service.TokenService;
import com.zmz.springbootjjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;

/**
 * @author zhangshengrui
 * @date 2022-01-06 20:45
 */
@Controller
@RequestMapping("api")
public class UserApi {
    @Autowired
    UserService userService;
    @Autowired
    TokenService tokenService;

    @GetMapping("/loginto")
    public String loginto(String username,String password ,ModelMap map){
        map.addAttribute("name","ceshi");
        System.out.println(username+" "+password);
        return "login";
    }
    //登录
    @ResponseBody
    @PostMapping("/login")
    public Object login(@RequestBody User user){
        System.out.println(user);
        JSONObject jsonObject=new JSONObject();
        User userForBase=userService.findByUsername(user);
        if(userForBase==null){
            jsonObject.put("message","登录失败,用户不存在");
            return jsonObject;
        }else {
            if (!userForBase.getPassword().equals(user.getPassword())){
                jsonObject.put("message","登录失败,密码错误");
                return jsonObject;
            }else {
                String token = tokenService.getToken(userForBase);
                jsonObject.put("token", token);
                jsonObject.put("user", userForBase);
                return jsonObject;
            }
        }
    }

    @ResponseBody
    @UserLoginToken
    @GetMapping("/getMessage")
    public String getMessage(){
        return "你已通过验证";
    }
}
  1. 拦截器
package com.zmz.springbootjjwt.interceptor;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.zmz.springbootjjwt.annotation.PassToken;
import com.zmz.springbootjjwt.annotation.UserLoginToken;
import com.zmz.springbootjjwt.entity.User;
import com.zmz.springbootjjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;


/**
 * @author zhangshengrui
 * @date 2022-01-06 20:41
 */
public class AuthenticationInterceptor implements HandlerInterceptor {
    @Autowired
    UserService userService;
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
        // 如果不是映射到方法直接通过
        if(!(object instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod=(HandlerMethod)object;
        Method method=handlerMethod.getMethod();
        //检查是否有passtoken注释,有则跳过认证
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }
        //检查有没有需要用户权限的注解
        if (method.isAnnotationPresent(UserLoginToken.class)) {
            UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
            if (userLoginToken.required()) {
                // 执行认证
                if (token == null) {
                    throw new RuntimeException("无token,请重新登录");
                }
                // 获取 token 中的 user id
                String userId;
                try {
                    userId = JWT.decode(token).getAudience().get(0);
                } catch (JWTDecodeException j) {
                    throw new RuntimeException("401");
                }
                User user = userService.findUserById(userId);
                if (user == null) {
                    throw new RuntimeException("用户不存在,请重新登录");
                }
                // 验证 token
                JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
                try {
                    jwtVerifier.verify(token);
                } catch (JWTVerificationException e) {
                    throw new RuntimeException("401");
                }
                return true;
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }
}

5)全局异常处理GloablExceptionHandler.java

package com.zmz.springbootjjwt.interceptor;

import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author zhangshengrui
 * @date 2022-01-06 22:37
 */
@ControllerAdvice
public class GloablExceptionHandler {
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public Object handleException(Exception e) {
        String msg = e.getMessage();
        if (msg == null || msg.equals("")) {
            msg = "服务器出错";
        }
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("message", msg);
        return jsonObject;
    }
}

6)javaconfig配置类

package com.zmz.springbootjjwt.config;

import com.zmz.springbootjjwt.interceptor.AuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author zhangshengrui
 * @date 2022-01-06 22:33
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**")// 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
                .excludePathPatterns("/loginto","/static/**");
    }
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
}

7)启动项目,直接访问http://localhost:8888/api/login

登录令牌JWT — JSON WEB TOKEN-鸿蒙开发者社区

8)登录成功拿到Token之后访问http://localhost:8888/api/getMessage

登录令牌JWT — JSON WEB TOKEN-鸿蒙开发者社区

不携带token

登录令牌JWT — JSON WEB TOKEN-鸿蒙开发者社区

注意:这里的key一定不能错,因为在拦截器中是取关键字token的值String token = httpServletRequest.getHeader("token");加上token之后就可以顺利通过验证和进行接口访问了

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2022-1-21 10:08:18修改
收藏 1
回复
举报
回复
    相关推荐