深入浅出,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 进行校验(不推荐)
好了 基本已经讲完,欢迎大家评论区指出不足,一起学习进步!

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

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

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

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • JMM简介_英文缩写jmy啥意思

    JMM简介_英文缩写jmy啥意思Java的内存模型JMM(JavaMemoryModel)JMM主要是为了规定了线程和内存之间的一些关系。根据JMM的设计,系统存在一个主内存(MainMemory),Java中所有实例变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(WorkingMemory),工作内存由缓存和堆栈两部分组成,缓存中保存的是主存中变量的拷贝,缓存可能并不总和主存同步,也就是缓存中变量的修改可能没有立刻写到主存中;堆栈中保存的是线程的局部变量,线程之间无法相互直接访问堆栈中的变量。JM

  • weka中文论坛

    weka中文论坛

  • Java菜鸟教程 递归算法与Scanner类「建议收藏」

    Java菜鸟教程 递归算法与Scanner类「建议收藏」最近笔者学习了递归算法与Scanner类的简单应用,在此做一些分享。递归算法:Recursion是一种解决问题的方法,就是把问题逐渐简单化。遵循“自己调用自己”的基本思想。运用递归算法解决问题的时候,要注意定义递归头,即什么时候不调用自身的方法;以及定义递归体:什么时候要调用自身的方法。可以用ifelse语句来控制,形成一个循环。以下是几个例子。staticinta=0;pu

  • set example(buildingexamples)

    //Examplesforusingsocat(andfilan)//”$”meansnormaluser,”#”requiresprivileges,”//”startsacomment/////////////////////////////////////////////////////////////////////////////////si

  • 数据分层之DWD

    数据分层之DWD1DWD是什么?明细粒度事实层以业务过程作为建模驱动,基于每个具体的业务过程特点,构建最细粒度的明细层事实表。可以结合企业的数据使用特点,将明细事实表的某些重要维度属性字段做适当冗余,即宽表化处理.明细粒度事实层(DWD)通常分为三种:事务事实表周期快照事实表累积快照事实表。2DWD中的信息有什么?事实表中一条记录所表达的业务细节程度被称为粒度。通常粒度可以通过两种方式来表述:一种是维度属性组合所表示的细节程度,一种是所表示的具体业务含义。作为度量业务过程的事实,通常为整型或浮点型的十

  • MySQL修改表名注释

    MySQL修改表名注释MySQL修改表名注释altertabletest1comment’修改后的表的注释’;

发表回复

您的电子邮箱地址不会被公开。

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