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

764 阅读9分钟

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

传统Session登录问题

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

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

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

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

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

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

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

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

image.png

  • 传统的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实现

image.png

什么是JWT

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

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

image.png

  • 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)

image.png

  • Header

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

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

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

{
	"userId": "xxxxxx..."
}
  • Signature签名哈希(重要)

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

HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)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版本以后的发生了较大的变化这里不进行赘述

				<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>
  • 对称加密
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();
    }
}
  • 非对称加密
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;
}