JWT学习笔录

JWT认证原理和源码实战逻辑

简介和原理

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

传统的session认证

我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。

但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来.

基于session认证所显露的问题

Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。

扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

基于token的鉴权机制

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

流程上是这样的:

用户使用用户名密码来请求服务器

服务器进行验证用户的信息

服务器通过验证发送给用户一个token

客户端存储token,并在每次请求时附送上这个token值

服务端验证token值,并返回数据

这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access- Control-Allow-Origin: *。

代码实现

基于SpringBoot的JWT

  1. 在开始构建前先导入JWT所依赖的jar3包
 <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
 </dependency>

其他开发所需要的jar包

<!--        引入mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.1</version>
        </dependency>
<!--        引入lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

<!--        引入durid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.6</version>
        </dependency>

<!--        引入mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
  
</dependency>
  1. 书写配置文件application.application

    spring.application.name=springbootjwt
    server.port=8081
    
    spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/jwt?characterEncoding=utf-8&useSSL=false
    spring.datasource.username=root
    spring.datasource.password=root
    
    mybatis.type-aliases-package=com.zhao.springbootjwt.entity
    mybatis.mapper-locations=classpath:mapper/*.xml
    
    logging.level.com.zhao.dao=debug
    
    
    
  2. 在引入成功后,进行代码的书写

    public static String getToken(){
        private static final String SIGN = "!@#$%^&*+=";
        Calendar instance = Calendar.getInstance(); //获取时间
        instance.add(Calendar.DATE, 7); //默认七天过期
    //令牌的获取
    String token = JWT.create()
           .withHeader(map)//header
           .withClaim("userid", "zhao") //设置要传递的的信息
           .withClaim("username", "admin")
           .withExpiresAt(instance.getTime())//指定令牌的过期时间  -- 七天
           .sign(Algorithm.HMAC256(SING));//设置签名
            return token;
    }
    

    通过上方方法我们根据传入的userid username salt经过指定的签名算法进行加密后 设置为 token 响应给客户端

    客户端将token保存,在日后的访问中用户在他的header中携带token进行请求我们就可以进行解密判断 用户传入的token是否和客户端的匹配;

  3. 用户再次访问时会携带未过期的token进行请求,实现方法如下:

    /*
    * 验证token信息方法
    * */
    public static DecodedJWT getTokenInfo(String token){
        DecodedJWT verify=JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
           return verify;
       }
    

    通过JWT.require()方法可以获得Verification 证明体 , Verification 证明体 这个接口,接口有一个Bulid()方法,JWTVerifier这个类实现创造 JWTVerifier方法 ,JWTVerifier 调用verify 方法进行验证。

在创建token时每次的token都是不同的但是,因为我们的加密算法和私钥是相同的所以可以保证相对安全

  1. 因为每次的分发和验证都是一次请求,我们可以将这个方法进行封装,封装成一个util包

    整合的工具类代码:

    package com.zhao.springbootjwt.utils;
    
    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTCreator;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.interfaces.DecodedJWT;
    
    import java.util.Calendar;
    import java.util.HashMap;
    
    public class JWTUtils {
        //签名
        private static final String SIGN = "!@#$%^&*+=";
        /* *
         * 生成token   header.payload.signature
        */
    
        public static String getToken(HashMap<String, String> map){
    
            //初始化实例,实例代表着创建时的日期
            Calendar instance = Calendar.getInstance(); //获取时间
            instance.add(Calendar.DATE, 7); //默认七天过期
            //根据日历的规则,将指定的时间量添加或减去给定的日历字段。 例如,要从当前日历的时间减去5天,您可以通过调用以下方法来实现:
            //add(Calendar.DAY_OF_MONTH, -5) 。
    
            //创建builder 遍历填充
            JWTCreator.Builder builder = JWT.create();
    
            //payload 进行装载   被处理过后的前端返回的信息
            //遍历map lamba表达式   -> payload结束
            map.forEach((k,v) -> {
                builder.withClaim(k,v);  //源码里面装入的是payload
            });
    
            //最终设置
            String token = builder.withExpiresAt(instance.getTime()) //添加时间
                    .sign(Algorithm.HMAC256(SIGN)); //sign 签名 设置加密算法
    
            
            return token;
        }
    
    
        /*
        * 验证token
        * */
        public static DecodedJWT verify(String token){
    
            return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
    
        }
        
    }
    
    

    而每次进行api请求时会访问不同的地址我们可以对特定的链接进行过滤验证,我们可以在Config类创建一个类来统一管理

package 
    com.zhao.springbootjwt.Config;

import com.zhao.springbootjwt.interceptors.JWTInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


//在这里进行拦截的相关配置
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JWTInterceptor())
                .addPathPatterns("/user/test") //增加拦截路径
                .excludePathPatterns("/user/login"); //剔
        除拦截路径
    }

}

在JWT的实现中,实际上是对拦截器(Interceptor)的实现,也可以被叫做过滤器

所以我们在进行token请求时,先拦截请求判断客户端请求来的数据和信息满足服务端的什么信息从而根据请求返回数据

  1. 我们现在 通过设置拦截器,拦截客户端请求,获取token 来进行判断,从而返回客户端数据

    代码实现:

    package com.zhao.springbootjwt.interceptors;
    
    import com.auth0.jwt.exceptions.AlgorithmMismatchException;
    import com.auth0.jwt.exceptions.SignatureVerificationException;
    import com.auth0.jwt.exceptions.TokenExpiredException;
    import com.auth0.jwt.interfaces.DecodedJWT;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.zhao.springbootjwt.utils.JWTUtils;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.HashMap;
    
    
    //这个位置是重写了拦截的方法
    //在InterceptorConfig里面建立拦截配置,在这里面重写拦截方法!!!
    //不需要直接调用本类的方法
    //可以注解掉/user/test 里面的验证方法进行测试
    public class JWTInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            HashMap<String, Object> map = new HashMap<>();
            //获取头中的令牌
            String token = request.getHeader("token");
    
            try {
                DecodedJWT verify =  JWTUtils.verify(token); //验证令牌
                System.out.println("拦截成功token为:"+token);
                return true; //验证通过后,可以直接放行
            } catch (SignatureVerificationException e) {
                e.printStackTrace();
                map.put("msg","无效签名");
            }catch (TokenExpiredException e) {
                e.printStackTrace();
                map.put("msg","token过期");
            }catch (AlgorithmMismatchException e) {
                e.printStackTrace();
                map.put("msg","算法不一致");
            }catch (Exception e) {
                e.printStackTrace();
                map.put("msg","token无效");
            }
            map.put("state",false); //设置状态
            //要将状态返回前端 将map转换为map jackson
            String json = new ObjectMapper().writeValueAsString(map);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(json); //服务台验证调试
    
            return false;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        }
    }
    
    
  2. 接下来我们实现Controller

    创建UserController

    实现登录和其他页面请求转换

    package com.zhao.springbootjwt.controller;
    
    import com.auth0.jwt.interfaces.DecodedJWT;
    import com.zhao.springbootjwt.entity.User;
    import com.zhao.springbootjwt.service.UserService;
    import com.zhao.springbootjwt.utils.JWTUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.HashMap;
    import java.util.Map;
    
    @RestController
    @Slf4j
    public class UserController {
        @Autowired
        private UserService userService;
    
        @GetMapping("/user/login")
        public Map<String,Object> login(User user){
            log.info("用户名:[{}]",user.getName());
            log.info("密码:[{}]",user.getPassword());
            Map<String,Object> map = new HashMap<>();
            try {
                User login = userService.login(user);
                HashMap<String, String> payload = new HashMap<>();
                payload.put("id",login.getId());
                payload.put("name",login.getName());
    
                String token = JWTUtils.getToken(payload);
    //            System.out.println(token);
    
                map.put("state",true);
                map.put("msg","认证成功");
                map.put("token",token);
            } catch (Exception e) {
                map.put("state",false);
                map.put("msg",e.getMessage());
            }
    
            return map;
        }
    
    
    
        //可以在这里处理具体的业务逻辑
        //因为是浏览器所以这里post是无法请求到,可以用swagger,apifox等模拟请求
        //所以在这里就要使用@PathVariable
        @PostMapping("/user/test")
        public Map<String,Object> test(HttpServletRequest request, HttpServletResponse response){
    
    
    
            Map<String, Object> map = new HashMap<>();
            String token = request.getHeader("token");
            log.info("当前token为:[{}]",token);
    
            DecodedJWT verify = JWTUtils.verify(token);
            log.info("用户id:[{}]",verify.getClaim("id").asString());
            log.info("用户name:[{}]",verify.getClaim("name").asString());
            map.put("status",true);
            map.put("msg","请求成功");
            return map;
        }
    }
    
    

    通过Apifox 进行模拟用户登录

    Mapper.xml实现login判断

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhao.springbootjwt.dao.UserMapper">
    <select id="login" parameterType="com.zhao.springbootjwt.entity.User" resultType="com.zhao.springbootjwt.entity.User">
        select * from jwt.user where name=#{name} and password = #{password};
    </select>
</mapper>

登录验证

结果

其他页面请求验证

结果

posted @   野生创造侠  阅读(20)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示