一、基于 Session 认证方式
1.1、http 无状态性
http 是一种无状态协议,就是说每次用户进行用户名和密码认证之后,http 不会留下记录,下一次请求还需要进行认证。因为http不知道每次请求是哪一个用户发出的。
为了能识别哪个用户提交的请求,在用户首次登录之后,在服务端存储一份用户登录的信息,这份信息在服务端就是通过 session 保存,然后把这份信息返回给客户端,客户端存储在 cookie 中,下次请求时,带上 cookie 中的信息,服务端就可以判断出是哪一个用户的请求。
1.2、session认证流程
- 客户端向服务器发送用户名、密码等认证信息。
- 服务器通过验证后,创建一个 session ,并将 session 信息保存起来。
- 服务器向客户端返回一个 sessionId ,客户端自动把 sessionId 存储在 cookie 中。
- 后续客户端每一次请求。都会在请求头携带 cookie ,将 sessionId 传给服务器。
- 服务器获得 cookie 之后,通过 cookie 中的 sessionId 解析出用户信息,执行相关业务。
1.3、存在的问题
- 每个用户信息都存储在服务端,随着用户量的增加,服务器的开销会增大。
- session 存储在服务端中,在分布式系统中,这种方式将会失效,为了保证各个服务器中的 session存储的信息一致,需要引入额外的中间件,比如:redis 等
- 对于非浏览器客户端不适用,原因在于 session 依赖 cookie,移动端没有 cookie
- 不安全,session 基于 cookie ,如果 cookie 信息被截获,很容易进行 CSRF(跨域请求伪造攻击)
了解到基于 session 认证方式存在的问题之后,有没有一种认证去解决这些问题?下面我们来看看基于Token认证方式。
二、基于Token认证方式
token 就是验证用户身份的凭证,成为令牌,
2.1、认证流程
- 客户端向服务器发送用户名、密码等认证信息。
- 服务端通过验证后,服务端会返回已经签名的 Token,也就是 JWT。
- 客户端收到 token 后,存储起来,比如放在 cookie 或者 localStorage 中。
- 客户端每次向服务端请求资源时需要带上这个 token。
- 服务端收到请求后去验证这个 token,成功则返回请求数据。
2.2、特点:
- 基于 token 的认证方式是一种服务端无状态的认证方式,服务端不存储 token 数据,适合分布式系统。
- 不依赖cookie机制,适用于浏览器客户端同时也适用移动端。
使用建议
- 建议将 JWT 存放在 localStorage 中,放在 Cookie 中会有 CSRF 风险。
- 请求服务端并携带 JWT 的做法是放在 HTTP 请求头中。
三、JWT
3.1、什么是 JWT
概念:JWT 全称 JSON Web Token,是一种基于 Token 的认证授权机制。可以生成 token,也可以解析验证 token。
官网地址 :https://jwt.io/
先看看官网上 JWT 的具体样式
3.2、JWT 由哪几部分组成
JWT 通常是这样的:xxxxx.yyyyy.zzzzz
。 示例:
erlang
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
通过示例可以看出 JWT 由 Header、Payload、Signature 三部分组成。
第一部分:Header(头)作用:记录令牌类型、签名算法等。
json
{
"alg": "HS256",
"typ": "JWT"
}
此 JSON 经过 Base64 编码,形成 JWT 的第一部分。
第二部分:Payload(有效载荷)作用:携带一些用户信息或者过期时间
json
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
Payload 默认是不加密的,不要将隐私数据放到 Payload 中,此 JSON 经过 Base64 编码,形成 JWT 的第二部分。
第三部分:Signature(签名)作用:防止 Token 被篡改,确保安全性。
json
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
Signature 是对前两部分的签名,这个签名生成需要用到:
- Header + Payload
- 存放在服务端的密钥(不能泄露)
- 签名算法
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用点(.)分隔,这个字符串就是 JWT 。
3.3、SpringBoot整合JWT
首先新建一个 springBoot 项目 jwt ,然后在 pom 中引入所需依赖。
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.duan</groupId>
<artifactId>jwt</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.3</version>
</dependency>
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- Springboot测试方法依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
在 JWTTest 测试类中新建生成 JWT 方法 getJWT,代码如下
java
package com.duan;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author db
* @version 1.0
* @description JWTTest
* @since 2023/12/21
*/
public class JWTTest {
/**
* 生成令牌
*/
@Test
public void getJWT(){
Map<String, Object> map = new HashMap<>();
map.put("id",1);
map.put("username","程序员康康");
String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "cxykk1217") // 设置签名算法和密钥
.setClaims(map) // 设置给定的数据
.setExpiration(new Date(System.currentTimeMillis() + 1000L * 60 * 3))
.compact();
System.out.println(jwt);
}
}
启动测试方法,打印结果如下:
在 JWTTest 测试类中新建解析 JWT 方法 parseJwt,代码如下:
java
package com.duan;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author db
* @version 1.0
* @description JWTTest
* @since 2023/12/21
*/
public class JWTTest {
/**
* 解析令牌
*/
@Test
public void parseJwt(){
Claims body = Jwts.parser()
.setSigningKey("cxykk1217")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNzAzOTkxMjg5LCJ1c2VybmFtZSI6Iueoi-W6j-WRmOW6t-W6tyJ9.Q_xG0pDaG2rkix2rEw2SNS5uBwTS5f0FbzcLFpQ0yJY")
.getBody();
System.out.println(body);
}
}
启动测试方法,打印结果如下:
以上测试可以生成和解析 JWT,在项目实际使用时 JWT 一般都是以工具类的方式出现。下面我们把生成和解析 JWT 的方法封装成工具类
java
package com.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
/**
* @author db
* @version 1.0
* @description JWTUtils
* @since 2023/12/31
*/
public class JWTUtils {
// 密钥
private static String signKey = "cxykk1217";
// 过期时间
private static Long expire = 1000L*60*30; // 30分钟
/**
* 生成JWT
* @param claims JWT第二部分负载payload中存储的内容
* @return
*/
public static String generateJwt(Map<String,Object> claims){
String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, signKey)
.addClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser().setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
代码仓库链接:https://gitee.com/duan138/practice-code/tree/dev/jwt
四、总结
基于 session 和基于 Token 认证方式,本质上没有什么区别,都是对用户身份的认证机制,只是在使用过程中校验的方式不同,各有优缺点,不能说哪个好哪个不好,要根据实际需求选择响应的方式,后续会有文章实现基于 token 的方式去实现用户登录访问控制。
改变你能改变的,接受你不能改变的,关注我,一起成长,共同进步。