Session?单点登录?JWT? 一篇看懂
传统Session登录问题
-
要讲 单点登录 和 JWT 首先得知道在没有 JWT 之前是怎么完成登录用户校验的
在没有JWT之前,我们是通过传统的Session认证
我们知道**HTTP协议本身是一种无状态的协议,**这一点很重要,这意味着如果用户像我们的应用提供了用户名和密码进行用户认证,
认证通过后HTTP协议是不会记录认证后的状态的,name下一次用户再发请求的时候,就需要用户再一次进行认证。因为我们服务
端不知道到底是哪个用户发的请求,所以为了能够让我们的应用识别出到底是那个用户发出的请求,我们只能在用户首次登陆成功
后,在服务器存储一份用户登录的信息,(这里很多人刚入门是不知道什么是服务器的这个概念的,比如Java中Tomcat)
这份登录信息会在响应时传递给浏览器,告诉浏览器让其保存为cookie,以便下一次请求时发送给我们的应用
这样我们的应用就能识别请求到底来自哪一个用户了,这就是传统的基于session的认证过程了
- 传统的Session认证有什么问题?不是能解决登录问题吗?
- 每个用户的登录信息都保存在服务器的session中,随着用户的增多,服务器开销会明显上升
- Session是存储在服务器的物理内存中的,所以在分布式系统中,这种方式也会导致登录失败,什么意思呢?举个例子,比如一个SpringBoot项目对应的是一个jvm虚拟机,现在有一个SpringCloud项目(就是多个SpringBoot),然你只在一个SpringBoot的jvm中进行了登录,并且在这一个jvm的tomcat进行了session认证,但是其他的jvm是没有这个session,即没有你的登录信息,
- 对于非浏览器的客户端、手机移动端等不适用,因为session依赖于Cookie,而移动端经常没有Cookie
- Session认证基于Cookie,若Cookie被截获了,用户很容易受到跨站请求伪造攻击,如果浏览器禁用Cookie,这种方式也会失效
- Session基于Cookie,而Cookie无法跨域,所以Session认证也是无法跨域的,单点登录不适用(除非采用redis进行session共享)
- 前后端分离系统中更加不适用,后端部署复杂,前端的请求往往经过多个中间件到达后端,Cookie中的Session信息需转发多次
什么是单点登录
单点登录 (Single Sign On) ,简称 SSO,SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有互相信任的应用系统
拿上面那个例子SpringBoot和SpringCloud举例子,此时我们的一个SpringCloud有三个启动了三个SpringBoot的项目,即此时有三
个JVM在运行,所谓的单点登录就是在一个JVM上面登录过后,其他的两个JVM能够感知得到这个用户已经登录了
- 单点登录的技术实现机制
- 使用Redis之类的实现Session共享
- 使用 jwt 生成token实现
什么是JWT
最简单点的理解,就是一个用来生成token的工具
- 先来看一下利用token进行用户身份验证的流程
- 客户端使用用户名和密码请求登录
- 服务端收到请求,验证用户名和密码
- 验证成功后,服务端会签发一个token,再把这个token返回给客户端
- 客户端收到token后可以把它存储起来,比如放到cookie中
- 客户端每次向服务端请求资源时需要携带服务端签发的token,可以在cookie或者header中携带
- 服务端收到请求,然后去验证客户端请求里面带着的token,如果验证成功,就向客户端返回请求数据
-
token认证的优势
这种基于token的认证方式与传统的session认证方式更加节约服务器资源,并且对移动端和分布式都友好,其优点如下:
- 支持跨域访问:COokie是无法实现跨域的,而token并没有用到Cookie(前提是将token放到请求头里面去)
- 无状态:token机制在服务端不需要存储session信息,因为token自身包含了所有登录用户的信息,所以可以减轻服务器压力
- 适用移动端:当客户端是非浏览器平台时,Cookie谁不被支持的,此时采用token认证的方式会简单很多
- 无需考虑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对象转换为字符串保存
{
"alg": "HS256",
"typ": "JWT"
}
-
PayLoad
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。一般会把包含用户信息的数据放到payload中
{
"userId": "xxxxxx..."
}
-
Signature签名哈希(重要)
签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改
是怎么确保数据不会被窜改的,首先是在生成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;
}