Session?单点登录?JWT? 一篇看懂

Session?单点登录?JWT? 一篇看懂

传统Session登录问题

  • 要讲 单点登录 和 JWT 首先得知道在没有 JWT 之前是怎么完成登录用户校验的

    在没有JWT之前,我们是通过传统的Session认证

    我们知道**HTTP协议本身是一种无状态的协议,**这一点很重要,这意味着如果用户像我们的应用提供了用户名和密码进行用户认证,

    认证通过后HTTP协议是不会记录认证后的状态的,name下一次用户再发请求的时候,就需要用户再一次进行认证。因为我们服务

    端不知道到底是哪个用户发的请求,所以为了能够让我们的应用识别出到底是那个用户发出的请求,我们只能在用户首次登陆成功

    后,在服务器存储一份用户登录的信息,(这里很多人刚入门是不知道什么是服务器的这个概念的,比如Java中Tomcat)

    这份登录信息会在响应时传递给浏览器,告诉浏览器让其保存为cookie,以便下一次请求时发送给我们的应用

    这样我们的应用就能识别请求到底来自哪一个用户了,这就是传统的基于session的认证过程了

  • 传统的Session认证有什么问题?不是能解决登录问题吗?
    1. 每个用户的登录信息都保存在服务器的session中,随着用户的增多,服务器开销会明显上升
    2. Session是存储在服务器的物理内存中的,所以在分布式系统中,这种方式也会导致登录失败 ,什么意思呢?举个例子 ,比如一个SpringBoot 项目对应的是一个jvm虚拟机,现在有一个SpringCloud项目(就是多个SpringBoot),然你只在一个SpringBoot的jvm中进行了登录,并且在这一个jvm的tomcat进行了session认证,但是其他的jvm是没有这个session,即没有你的登录信息,
    3. 对于非浏览器的客户端、手机移动端等不适用,因为session依赖于Cookie,而移动端经常没有Cookie
    4. Session认证基于Cookie,若Cookie被截获了,用户很容易受到跨站请求伪造攻击,如果浏览器禁用Cookie,这种方式也会失效
    5. Session基于Cookie,而Cookie无法跨域 ,所以Session认证也是无法跨域的,单点登录不适用(除非采用redis进行session共享)
    6. 前后端分离系统中更加不适用,后端部署复杂,前端的请求往往经过多个中间件到达后端,Cookie中的Session信息需转发多次

什么是单点登录

单点登录 (Single Sign On) ,简称 SSO,SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有互相信任的应用系统

拿上面那个例子SpringBoot和SpringCloud举例子,此时我们的一个SpringCloud有三个启动了三个SpringBoot的项目,即此时有三

个JVM在运行,所谓的单点登录就是在一个JVM上面登录过后,其他的两个JVM能够感知得到这个用户已经登录了

  • 单点登录的技术实现机制
  1. 使用Redis之类的实现Session共享
  2. 使用 jwt 生成token实现

什么是JWT

最简单点的理解,就是一个用来生成token的工具

  • 先来看一下利用token进行用户身份验证的流程
  1. 客户端使用用户名和密码请求登录
  2. 服务端收到请求,验证用户名和密码
  3. 验证成功后,服务端会签发一个token,再把这个token返回给客户端
  4. 客户端收到token后可以把它存储起来,比如放到cookie中
  5. 客户端每次向服务端请求资源时需要携带服务端签发的token,可以在cookie或者header中携带
  6. 服务端收到请求,然后去验证客户端请求里面带着的token,如果验证成功,就向客户端返回请求数据
  • token认证的优势

    这种基于token的认证方式与传统的session认证方式更加节约服务器资源,并且对移动端和分布式都友好,其优点如下:

  1. 支持跨域访问 :COokie是无法实现跨域的,而token并没有用到Cookie(前提是将token放到请求头里面去)
  2. 无状态 :token机制在服务端不需要存储session信息,因为token自身包含了所有登录用户的信息,所以可以减轻服务器压力
  3. 适用移动端:当客户端是非浏览器平台时,Cookie谁不被支持的,此时采用token认证的方式会简单很多
  4. 无需考虑CSRF :token不依赖于Cookie,所以采用token的认证方式不用担心Cookie被截获,无需考虑CSRF
  • JWT的组成结构

    JWT由三部分组成:头部(Header)、负载(PayLoad)和签名(Signature),在传输的时候,会将JWT的三部分分别进行Base64编码进行字符串拼接

    JWT 生成的 token 为:

    token = Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)

  • Header

    JWT头部是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存

js 复制代码
{
  "alg": "HS256",
  "typ": "JWT"
}
  • PayLoad

    有效载荷 部分,是JWT的主体内容部分,也是一个JSON对象 ,包含需要传递的数据。一般会把包含用户信息的数据放到payload中

js 复制代码
{
	"userId": "xxxxxx..."
}
  • Signature签名哈希(重要)

    签名哈希 部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> H M A C S H A 256 ( b a s e 64 U r l E n c o d e ( h e a d e r ) + " . " + b a s e 64 U r l E n c o d e ( p a y l o a d ) , s e c r e t ) HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret) </math>HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)

​ 是怎么确保数据不会被窜改的,首先是在生成token的时候,就把Header和PayLoad记录下来放在token里面,无法被破解

​ 若是Header和PayLoad有任何一个被破解了,那么在将Header和PayLoad与签名进行比较的时候,如果发现不一致,就说明

​ 数据被篡改了

  • 注意JWT每部分的作用,在服务端接收到客户端发送过来的JWT token之后

    • header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据

    • signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secretKey对header、payload进行加密,比对加密后的数据和客户端发送过来的是否一致。

Java中使用JWT

  • 对称加密和非对称加密的区别

    对称加密和非对称加密是两种加密算法,它们的区别在于加密和解密所使用的密钥不同。

    **对称加密使用相同的密钥既用于加密数据,也用于解密数据。**这意味着发送方使用相同的密钥对数据进行加密,接收方也使用相同的密钥对数据进行解密。对称加密算法速度快,效率高,适用于加密大量的数据。但是,对称加密算法存在一个密钥分发的问题,即如何确保密钥在发送方和接收方之间安全地传输。 非对称加密使用两个不同的密钥,分别为公钥和私钥。公钥用于加密数据,私钥用于解密数据。发送方使用接收方的公钥对数据进行加密,接收方使用自己的私钥对数据进行解密。

    非对称加密算法相对于对称加密算法速度较慢,适用于加密少量的数据,例如数字签名和密钥的传输。非对称加密算法可以解决密钥分发的问题,但是存在私钥的保护问题,即如何确保私钥不会被未经授权的第三方获取。

  • 引入依赖

    注意这里引入的依赖是0.9.x的,0.10.x版本以后的发生了较大的变化这里不进行赘述

xml 复制代码
				<dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.5.1</version>
        </dependency>				
			  <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.33</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
  • 对称加密
java 复制代码
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

/**
 * JWT工具类
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 60 * 60 * 1000 * 24 *10000L;// 60 * 60 * 1000 * 24 *10000  一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "qx";

    public static String getUUID(){
        String token = UUID.randomUUID().toString().replaceAll("-", "");//token用UUID来代替
        return token;
    }

    /**
        id      : 可以不用
        subject : 我们想要加密存储的数据
        ttl     : 我们想要设置的过期时间
     */


    /**
     * 生成token  jwt加密
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成token  jwt加密
     * @param subject token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }


    /**
     * 创建token jwt加密
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("sg")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }



    public static void main(String[] args) throws Exception {
        //jwt加密
        String jwt = createJWT("123456");
        System.out.println(jwt);

        //jwt解密
        Claims claims = parseJWT(jwt);
        String subject = claims.getSubject();
        System.out.println(subject);
        System.out.println(jwt);
    }

    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    /**
     * jwt解密
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}
  • 非对称加密
java 复制代码
private static final String RSA_PRIVATE_KEY = "...";
private static final String RSA_PUBLIC_KEY = "...";

/**
     * 根据用户id和昵称生成token
     * @param id  用户id
     * @param nickname 用户昵称
     * @return JWT规则生成的token
     */
public static String getJwtTokenRsa(String id, String nickname){
    // 利用hutool创建RSA
    RSA rsa = new RSA(RSA_PRIVATE_KEY, null);
    RSAPrivateKey privateKey = (RSAPrivateKey) rsa.getPrivateKey();
    String JwtToken = Jwts.builder()
        .setSubject("baobao-user")
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
        .claim("id", id)
        .claim("nickname", nickname)
        // 签名指定私钥
        .signWith(privateKey, SignatureAlgorithm.RS256)
        .compact();
    return JwtToken;
}

/**
     * 判断token是否存在与有效
     * @param jwtToken token字符串
     * @return 如果token有效返回true,否则返回false
     */
public static Jws<Claims> decodeRsa(String jwtToken) {
    RSA rsa = new RSA(null, RSA_PUBLIC_KEY);
    RSAPublicKey publicKey = (RSAPublicKey) rsa.getPublicKey();
    // 验签指定公钥
    Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(jwtToken);
    return claimsJws;
}
相关推荐
AI人工智能+电脑小能手16 分钟前
【大白话说Java面试题】【Java基础篇】第30题:JDK动态代理和CGLIB动态代理有什么区别
java·开发语言·后端·面试·代理模式
言萧凡_CookieBoty18 分钟前
AI 编程省 Token 实战:从 Spec、上下文工程到模型分层的降本策略
前端·ai编程
swipe27 分钟前
别再把 AI 聊天做成纯文本:从 agui 这个前后端项目,拆解“可感知工具调用”的流式 AI UI
后端·langchain·llm
GetcharZp28 分钟前
GitHub 爆火!纯 Go 编写的文件同步神器 Syncthing,凭什么成为程序员的标配?
后端
hERS EOUS32 分钟前
SpringBoot 使用 spring.profiles.active 来区分不同环境配置
spring boot·后端·spring
DFT计算杂谈37 分钟前
wannier90 参数详解大全
java·前端·css·html·css3
LucianaiB43 分钟前
我用飞书多维表做了一个 AI 活动推荐智能体:每天自动催我别错过截止日期!
后端
铁皮饭盒2 小时前
第2课:5分钟!用 Trae AI 生成你的第一个后端服务(Bunjs + Elysia)
前端·后端·全栈
金銀銅鐵2 小时前
[git] 浅解 git reset 命令
git·后端