深入浅出,JWT单点登录实例+原理

深入浅出,JWT单点登录实例+原理深入浅出,JWT单点登录实例先直接上案例,方便工作中拷贝。后面说原理。代码git链接 案例演示:Controller: 登录授权接口,用户输入名字密码后请求此接口。登录成功后返回jwt 模拟认证中心,真实环境中此接口应该是一个单独的服务,这里方便演示,用一个接口代替。@PostMapping(“/login”)publicObjectlogin(){returnnull;} 主业务服务的主接口,返回主页

大家好,又见面了,我是你们的朋友全栈君。

先直接上案例,方便工作中拷贝。后面说原理。

实例代码git链接点击这里

案例演示:

现在我们的案例流程图。看不懂没关系,后面一下就懂了。
在这里插入图片描述

首先引入jwt相关依赖

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.10.7</version>
        </dependency>

Controller:


		模拟认证中心,真实环境中此接口应该是一个单独的认证服务,这里方便演示,用一个接口代替。
	    登录授权接口,用户输入名字密码后请求此接口。登录成功后将生成地jwt保存在Cookie(真实环境不建议放在Cookie中,会有跨域问题),
	    并请求下面的"/mainData"接口,返回主页面。
    @GetMapping("/login")
    public Object login(HttpServletResponse response){ 
   
        		这里默认登录成功,业务逻辑我就不写了,挑重点写
        String userId = "1";
        String userName = "张三";
        		用用户的id 和 用户名生成jwt  真实环境中这里还会存用户权限等等。。
        String JID = JwtUtils.getJwtToken(userId, userName);
        Cookie cookie = new Cookie("JID",JID);
        cookie.setPath("/" );
        response.addCookie(cookie);
        return “登录成功”;
    }


		主业务服务的主接口,返回主页面。
		自定义注解@Check,用于拦截器拦截对此方法的请求,校验jwt
    @GetMapping("/mainData")
    @Check(module="获取主页面")
    public Object mainData(){ 
   

        return  "主页面";
    }

@Check自定义注解:

@Retention(RetentionPolicy.RUNTIME)  表明该注解在运行时生效
@Target(ElementType.METHOD)    表明该注解只能作用于方法上
public @interface Check { 
   
    //模块
    String module() default "";    表明当前使用该注解的方法是哪个模块,例如上面Controller中的
    							   "@Check(module="获取主页面")",由注解调用处传入该module}

JwtUtils工具类:

@Component
public class JwtUtils { 
   

    设置token过期时间
    public static final long EXPIRE = 1000 * 60 * 60 * 24;  
    密钥,随便写,做加密操作
    public static final String APP_SECRET ="xbrceXUKwYIRoQJndTPFNzAmhDagkLMExbrceXUKwYIRoQJndTPFNzAmhDagkLME";  
    
    生成jwt字符串的方法
    public static String getJwtToken(String id, String nickname){ 
   

        String JwtToken = Jwts.builder()
                //设置头信息,固定
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                //设置过期时间
                .setSubject("guli-user")//名字随便取
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                //设置jwt主体部分
                .claim("id", id)
                .claim("userName", nickname)
                //根据密钥生成字符串
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();

        return JwtToken;
    }

    /** * 判断jwt是否存在与有效 * @param request * @return */
    public static boolean checkToken(HttpServletRequest request) { 
   
        try { 
   
            String jwtToken = request.getHeader("Cookie").split("=")[1];
            if(StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) { 
   
            e.printStackTrace();
            return false;
        }
        return true;
    }
    /** * 根据jwt获取用户信息 * @param request * @return */
    public static Claims getMemberByJwtToken(HttpServletRequest request) { 
   
        String jwtToken = request.getHeader("Cookie").split("=")[1];
        if(StringUtils.isEmpty(jwtToken)) return null;
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return claims;
    }
}

Interceptor拦截器:

@Component
public class loginCheckInterceptor implements HandlerInterceptor { 
   

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 
   
        Check check = null;
        if(handler instanceof  HandlerMethod){ 
   
            HandlerMethod hm  = (HandlerMethod)handler;
             check = hm.getMethodAnnotation(Check.class);
        }
        if(check != null){ 
   
        		如果check不为空,说明此接口被自定义注解@Check注释,需要校验登录状态
            if(JwtUtils.checkToken(request)){ 
   
            			通过此If判断,说明本次请求携带了jwt,并且未过期。 解密jwt获取用户信息
                Claims userInfo = JwtUtils.getMemberByJwtToken(request);
              			将用户信息保存到User工具类或Redis缓存中。方便用户信息跟踪。 
              			我们这里保存到UserUtil工具类中。
                UserUtil.setUserName((String) userInfo.get("userName"));
                UserUtil.setUsId(Long.parseLong((String) userInfo.get("id")));
            }else { 
   
                	jwt为空或过期,返回登录页面让用户登录,重新获取jwt
                return false;
            }
        }
        return true;
    }
}

UserUtil用户工具类

关于ThreadLocal技术这里不做展开。

/** * @Description: 获取/设置本次用户数据 */
   这当然不是一个普通的java实体类。如果是普通的类,其中的属性字段会出现线程覆盖问题,数据错乱。
   想象一下这样的场景。
   1.张三登录了,普通实体类的name字段设置成张三。
   2.李四登录了,name字段变成了李四。
   3.张三完成了一个操作,程序需要调用UserUtil.getUserName()方法获取当前操作人,结果获取的是李四

所以这里采用了ThreadLocal技术。

public class UserUtil { 
   

    public static String getUserName(){ 
   
        return ThreadLocalUtil.get( "USER_NAME" );
    }

    public static Long getUsId(){ 
   
        return ThreadLocalUtil.get( "USER_ID" );
    }

    public static void setUsId( Long user_id ){ 
   
        ThreadLocalUtil.set( "USER_ID", user_id );
    }

    public static void setUserName( String  userName ){ 
   
        ThreadLocalUtil.set( "USER_NAME",userName );
    }

}

ThreadLocalUtil用户工具类

关于ThreadLocal技术这里不做展开。主要就是通过当前线程对象线程集合中获取到该线程下保存的变量,解决多线程下的数据覆盖问题。

public class ThreadLocalUtil { 
   

    private static final ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal() { 
   
        protected Map<String, Object> initialValue() { 
   
            return new HashMap(4);
        }
    };

    public static Map<String, Object> getThreadLocal(){ 
   
        return threadLocal.get();
    }
    public static <T> T get(String key) { 
   
        Map map = threadLocal.get();
        return (T)map.get(key);
    }

    public static <T> T get(String key,T defaultValue) { 
   
        Map map = threadLocal.get();
        return map.get(key) == null ? defaultValue : (T)map.get(key);
    }

    public static void set(String key, Object value) { 
   
        Map map = threadLocal.get();
        map.put(key, value);
    }

    public static void set(Map<String, Object> keyValueMap) { 
   
        Map map = threadLocal.get();
        map.putAll(keyValueMap);
    }

    public static void remove() { 
   
        threadLocal.remove();
    }

    public static <T> Map<String,T> fetchVarsByPrefix(String prefix) { 
   
        Map<String,T> vars = new HashMap<>();
        if( prefix == null ){ 
   
            return vars;
        }
        Map map = threadLocal.get();
        Set<Map.Entry> set = map.entrySet();

        for( Map.Entry entry : set ){ 
   
            Object key = entry.getKey();
            if( key instanceof String ){ 
   
                if( ((String) key).startsWith(prefix) ){ 
   
                    vars.put((String)key,(T)entry.getValue());
                }
            }
        }
        return vars;
    }

    public static <T> T remove(String key) { 
   
        Map map = threadLocal.get();
        return (T)map.remove(key);
    }

    public static void clear(String prefix) { 
   
        if( prefix == null ){ 
   
            return;
        }
        Map map = threadLocal.get();
        Set<Map.Entry> set = map.entrySet();
        List<String> removeKeys = new ArrayList<>();

        for( Map.Entry entry : set ){ 
   
            Object key = entry.getKey();
            if( key instanceof String ){ 
   
                if( ((String) key).startsWith(prefix) ){ 
   
                    removeKeys.add((String)key);
                }
            }
        }
        for( String key : removeKeys ){ 
   
            map.remove(key);
        }
    }
}

好开始测试(请带入角色)!

  1. 现在我们模拟,我是一个从来没上过淘宝购物的小白。今天心血来潮,我要网购了。好,访问淘宝网!
  2. 这个时候我们的请求发到了”/mainData”接口,希望返回淘宝网的主页面,但是由于拦截器的存在,我们的请求先进入拦截器中。
  3. 由于”/mainData”接口被@Check修饰,拦截器会对所有@Check修饰的方法进行登录验证
  4. 由于我们是第一次访问,没有登陆过。这个时候我们的请求头中没有携带Cookie(jwt),所以跳转到了登录页面,让我登录!在这里插入图片描述
  5. 现在我们在登录页,输入了正确的姓名密码。点击登录,访问到了“/login”接口。
  6. 登录成功后,系统给我们分配了一个jwt字符串。并将jwt保存在浏览器的cookie中!
    在这里插入图片描述
  7. 这个时候,登录成功了,拦截器放行,获取淘宝地主页面。我们看看这次地请求。
    在这里插入图片描述

8.后台拿到了Cookie,就拿到了JWT字符串。通过校验,发现JWT没有过期也没有被篡改,解密过后,获取到了我的用户名和我的id,保存到了线程安全的UserUtil中。这样我后续地每一步操作,都可以跟踪到我地用户状态了!

单点效果的体现

那么现在如果我点击淘宝页面右上角的“切换到天猫超市”按钮 我还需要重新登录吗???

总所周知,现在都是微服务体系。那么在我们的这个例子中。我们可以认为。

  • 淘宝是一个服务
  • 认证中心是一个服务(就是登录页+/login接口+jwt验证)
  • 天猫是一个服务

现在淘宝登录成功了。浏览器也保存了jwt。那么我们访问天猫的时候,也将jwt带上,让天猫拿着这个jwt去验证(其实就是拿到认证中心服务去验证)。验证成功就返回天猫的首页。就不需要登录了。

现在再看一次这个图,是不是清晰了不少
在这里插入图片描述

原理:

在这里插入图片描述

首先 JWT 长这个样 : xxxx.xxxx.xxxx(header.payload.signature)
眼睛看仔细一些,你会发现 JWT 里面有两个’.’
数据格式是这样的 header.payload.signature
我们逐个逐个部分去分析,这个部分到底是干嘛的,有什么用

Header:

JWT 的 header 中承载了两部分信息

{ 
   
  "alg": "RS256",  声明加密的算法
  "typ": "JWT"   声明类型
}

对这个头部信息进行 base64,即可得到 header 部分。

Payload:

payload 是主体部分,意为载体,承载着有效的 JWT 数据包,它包含三个部分

  • 标准声明
  • 公共声明
  • 私有声明

标准声明的字段:标准中建议使用这些字段,但不强制。

  iss?: string; // JWT的签发者
  sub?: string; // JWT所面向的用户
  aud?: string; // 接收JWT的一方
  exp?: number; // JWT的过期时间
  nbf?: number; // 在xxx日期之间,该JWT都是可用的
  iat?: number; // 该JWT签发的时间
  jti?: number; //JWT的唯一身份标识

公共声明的字段:公共声明字段可以添加任意信息,但是因为可以被解密出来,所以不要存放敏感信息。

[key: string]: any;

私有声明的字段:私有声明是 JWT 提供者添加的字段,一样可以被解密,所以也不能存放敏感信息。

[key: string]: any;

同样是通过 base64 加密生成第二部分的 payload部分。

Signature:

Signature是签证信息,该签证信息是通过header和payload,加上secret(后台自定义的密钥),通过算法加密生成。
公式: signature = 加密算法(header + “.” + payload, 密钥);

它是如何做身份验证的?

首先,JWT 的 Token 相当是明文,是可以解密的,任何存在 payload 的东西,都没有秘密可言,所以隐私数据不能签发 token。

而服务端,拿到 token 后解密,即可知道用户信息,例如本例中的UserName,id

有了 id,那么你就知道这个用户是谁,是否有权限进行下一步的操作。

Token 的过期时间怎么确定?

payload 中有个标准字段 exp,明确表示了这个 token 的过期时间。例如案例中的:

在这里插入图片描述
服务端可以拿这个时间与服务器时间作对比,过期则拒绝访问。

如何防止 Token 被串改?

此时 signature字段就是关键了,能被解密出明文的,只有header和payload

假如黑客/中间人串改了payload,那么服务器可以通过signature去验证是否被篡改过。

在服务端在执行一次 signature = 加密算法(header + “.” + payload, 密钥);, 然后对比 signature 是否一致,如果一致则说明没有被篡改。

所以为什么说服务器的密钥不能被泄漏。

如果泄漏,将存在以下风险:

  • 客户端可以自行签发 token
  • 黑客/中间人可以肆意篡改 token

如何加强 JWT 的安全性?

  • 缩短 token 有效时间
  • 使用安全系数高的加密算法
  • token 不要放在 Cookie 中,有 CSRF 风险
  • 使用 HTTPS 加密协议
  • 对标准字段 iss、sub、aud、nbf、exp 进行校验
  • 使用成熟的开源库,不要手贱造轮子
  • 特殊场景下可以把用户的 UA、IP 放进 payload 进行校验(不推荐)
好了 基本已经讲完,欢迎大家评论区指出不足,一起学习进步!

大家看完了点个赞,码字不容易啊。。。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/143313.html原文链接:https://javaforall.net

(0)
上一篇 2022年5月21日 下午10:00
下一篇 2022年5月21日 下午10:00


相关推荐

  • soapclient php 扩展,PHP扩展—SOAP[通俗易懂]

    soapclient php 扩展,PHP扩展—SOAP[通俗易懂]一、概述及安装SOAP扩展可以用于编写SOAP服务器和客户端,支持SOAP1.1,SOAP1.2和WSDL1.1规格的子集。此扩展需要libxmlPHP扩展。这表示需要使用–enable-libxml,尽管这将隐式完成因为libxml是缺省开启的。要开启SOAP支持,配置PHP时要加上–enable-soap.二、相关函数is_soap_fault—…

    2025年6月1日
    5
  • pycharm调试远程主机_服务终端

    pycharm调试远程主机_服务终端我们有时为了方便,可能需要用到pycharm中的终端功能进行服务器端调试。在将pycharm配置远程开发后,我们点开Terminal终端功能,默认是本地的终端,如下图。如果要使用远程的终端,非常简单,因为已经配置过远程的解释器,我们点开Tools里的startSSHsession功能,即可选择服务器端的终端功能按图示选择完后,就可以直接使用服务器端的终端功能了,不需要额外的SSH工具显…

    2022年8月28日
    6
  • python浮雕图片_用Python来画浮雕画

    python浮雕图片_用Python来画浮雕画浮雕艺术在世界各地都可以见到,中国古代在唐朝以来就有许多浮雕效果的东西,很多的大型纪念性建筑都有这种作为装饰,常见的有花窗,龙柱等。简单的来说,浮雕就是把所要呈现的图像突起于石头表面,根据凹凸的程度不同从而形成三维的立体感。用Python画一张浮雕画,那就进行类似的原理,通过勾画图像的轮廓,并且降低周围的像素值,那就可以产生一张具有立体感的浮雕效果图片。我们可以采用相邻像素相减的方法来得到轮廓与平…

    2022年6月20日
    32
  • java tair,Tair 简介

    java tair,Tair 简介1Tair 的功能 Tair 是一个 Key Value 结构数据的解决方案 它默认支持基于内存和文件的两种存储方式 分别和我们通常所说的缓存和持久化存储对应 Tair 除了普通 Key Value 系统提供的功能 比如 get put delete 以及批量接口外 还有一些附加的实用功能 使得其有更广的适用场景 包括 Version 支持原子计数器 Item 支持 1 1Version 支持 Tair 中的每个数据都包含

    2026年3月19日
    2
  • “仅三天可见” 的朋友圈有方法破解啦!

    “仅三天可见” 的朋友圈有方法破解啦!点击上方“逆锋起笔”,公众号回复PDF领取大佬们推荐的学习资料之前微博上出现过一个热搜话题:超一亿人朋友圈仅三天可见。微信创始人张小龙在年度演讲里说,这个开关,是微信里使用最多的。很多…

    2022年6月13日
    58
  • 电脑如何防蹭网?使用防蹭网功能杜绝未知设备连接WiFi

    电脑如何防蹭网?使用防蹭网功能杜绝未知设备连接WiFi

    2021年9月21日
    51

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号