1.1 jwt和token
1.1.1 token介绍
令牌(Token):在计算机领域,令牌是一种代表某种访问权限或身份认证信息的令牌。它可以是一串随机生成的字符或数字,用于验证用户的身份或授权用户对特定资源的访问。普通的令牌可能以各种形式出现,如访问令牌、身份令牌、刷新令牌等。
简单理解 : 每个用户生成的唯一字符串标识 , 可以进行用户识别和校验
优势: token验证标识无法直接识别用户的信息,盗取token后也无法`登录`程序! 相对安全!
1.1.2 jwt介绍
- Token是一项规范和标准(接口)
- JWT(JSON Web Token)是具体可以生成,校验,解析等动作Token的技术(实现类)
- JWT只通过算法实现对Token合法性的验证,不依赖数据库存储系统,因此可以做到跨服务器验证,只要密钥和算法相同,不同服务器程序生成的Token可以互相验证。
- JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数
- JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑(JWT的登出问题)。
思考 : 正常情况下 修改了密码后就会跳转到登录页面 :修改成功后清空浏览器保存的token了
后端怎么玩? 因为服务端不保留token 我用之前的token 还是可以继续访问的
JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
为了减少盗用,JWT 不应该使用 HTTP 80 协议明码传输,要使用 HTTPS 443 协议传输。
1.1.3 JWT解决服务器集群/跨域认证问题
互联网服务离不开用户认证。一般流程是下面这样。
- 用户向服务器发送用户名和密码。
- 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
- 服务器向用户返回一个 jsession_id,写入用户的 Cookie。
- 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
- 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
这种模式的问题在于,扩展性(scaling)不好。
单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,要求每台服务器都能够读取 session。
举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。 服务器不存数据,客户端存,服务器解析就行了
1.1.4 jwt工作流程
- 用户提供其凭据(通常是用户名和密码)进行身份验证。
- 服务器对这些凭据进行验证,并在验证成功后创建一个JWT。
- 服务器将JWT发送给客户端,并客户端在后续的请求中将JWT附加在请求头或参数中。
- 服务器接收到请求后,验证JWT的签名和有效性,并根据JWT中的声明进行身份验证和授权操作
-
1.1.5 jwt数据组成和包含信息
一个JWT由三部分组成,各部分以点分隔:
Header(头部)-----base64Url编码的Json字符串
Payload(载荷)—base64url编码的Json字符串
Signature(签名)—使用指定算法,通过Header和Playload加盐计算的字符串
一个JWT看起来像下面这样:
xxxxx.yyyyy.zzzzz
下面这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
jwt可以携带很多信息 一般情况,需要加入:有效时间,签名秘钥,其他用户标识信息!
有效时间的作用 : 保证token的时效性,过期可以重新登录获取!
签名秘钥的作用 : 防止其他人随意解析和校验token数据!
用户信息的作用 : 系统解析的时候,分辨Token对应的具体用户!
1.1.5.1 Header
此部分有两部分组成:
- 一部分是token的类型,目前只能是JWT
- 另一部分是签名算法,比如HMAC 、 SHA256 、 RSA
示例:
{
“alg”:“HS256”,
“typ”:“JWT”
}
base64编码命令:
echo -n ‘{“alg”:“HS256”,“typ”:“JWT”}’ | base64
1.1.5.2 Payload
token的第二部分是payload(有效负载),其中包含claims(声明)。Claims是关于一个实体(通常是用户)和其他数据类型的声明。
1.1.5.3 Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + “.” +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
示例:
HMACSHA256(
base64UrlEncode(header) + “.” +
base64UrlEncode(payload),
secret)
1.1.6 jwt使用和测试
1.1.6.1 jjwt包实现JWT
- 导入依赖
<!--jjwt只是实现JSON的一个包,还有其他的例如Java-jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
- 编写配置application.yaml
#jwt配置
jwt:
token:
tokenExpiration: 120 #有效时间,单位分钟
tokenSignKey: headline123456 #当前程序签名秘钥 自定义
- 封装jwt技术工具类
分析 : 从逻辑上讲,该工具类必须有如下功能
a. 根据传入的用户id生成token并返回 ,以便用户免密登录
b. 根据传入的token判断用户id , 以便知道登录的是哪个用户
c. 判断传入的token是否还在有效期
package com.sunsplanter.utils;
import com.alibaba.druid.util.StringUtils;
import io.jsonwebtoken.*;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.util.Date;
@Data
@Component
//读取配置文件中所有前缀为jwt.token的属性,只要最后后缀一样就自动注入
@ConfigurationProperties(prefix = "jwt.token")
public class JwtHelper {
private long tokenExpiration; //有效时间,单位毫秒 1000毫秒 == 1秒
private String tokenSignKey; //当前程序签名秘钥
//生成token字符串
public String createToken(Long userId) {
System.out.println("tokenExpiration = " + tokenExpiration);
System.out.println("tokenSignKey = " + tokenSignKey);
String token = Jwts.builder()
.setSubject("YYGH-USER")
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration*1000*60)) //单位分钟
.claim("userId", userId)
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
//从token字符串获取userid
public Long getUserId(String token) {
if(StringUtils.isEmpty(token)) return null;
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
Integer userId = (Integer)claims.get("userId");
return userId.longValue();
}
//判断token是否有效
public boolean isExpiration(String token){
try {
boolean isExpire = Jwts.parser()
.setSigningKey(tokenSignKey)
.parseClaimsJws(token)
.getBody()
.getExpiration().before(new Date());
//没有过期,有效,返回false
return isExpire;
}catch(Exception e) {
//过期出现异常,返回true
return true;
}
}
}
4. 使用和测试
package com.sunsplanter.test;
import com.sunsplanter.utils.JwtHelper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class SBTest {
@Autowired
private JwtHelper jwtHelper;
@Test
public void test(){
//生成 传入用户标识
String token = jwtHelper.createToken(1L);
System.out.println("token = " + token);
//解析用户标识
int userId = jwtHelper.getUserId(token).intValue();
System.out.println("userId = " + userId);
//校验是否到期! false 未到期 true到期
boolean expiration = jwtHelper.isExpiration(token);
System.out.println("expiration = " + expiration);
}
}
1.1.6.2 java-jwt包实现JWT
- 导入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
- 封装jwt技术工具类
package com.sunsplanter.utils;
/**
* 本类用于生成和解析JWT
*/
@Configuration
public class JWTUtil {
/**
* 声明一个秘钥
*/
private static final String SECRET = "woshiyigemiyao ";
/**
* 生成JWT
*
* @param userId 用户编号
* @param username 用户名
* @param auth 用户权限
*/
public String createToken(Integer userId, String username, List<String> auth) {
//得到当前的系统时间
Date currentDate = new Date();
//根据当前时间计算出过期时间 定死为5分钟
Date expTime = new Date(currentDate.getTime() + (1000 * 60 * 5));
//头数据就是算法alg和类型typ,其中type必须是JWT(用JWT生成token)
Map<String, Object> header = new HashMap<>();
header.put("alg", "HS256");
header.put("typ", "JWT");
return JWT.create()
.withHeader(header) //头
.withClaim("userId", userId) //自定义数据
.withClaim("username", username) //自定义数据
.withClaim("auth", auth) //自定义数据
.withIssuedAt(currentDate) //创建时间
.withExpiresAt(expTime)//过期时间
.sign(Algorithm.HMAC256(SECRET));
}
/**
* 验证JWT并解析
*
* @param token 要验证的jwt的字符串
*/
public static Boolean verifyToken(String token) {
try{
// 使用秘钥创建一个解析对象
JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
//验证JWT
DecodedJWT decodedJWT = jwtVerifier.verify(token);
System.out.println(decodedJWT);
String payload = decodedJWT.getPayload();
System.out.println("payload = " + payload);
return true;
}catch (TokenExpiredException e){
e.printStackTrace();
}
return false;
}
/**
* 从JWT里的获取用户编号
*/
public Integer getUserId(String token){
try{
// 使用秘钥创建一个解析对象
JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
//验证JWT
DecodedJWT decodedJWT = jwtVerifier.verify(token);
Claim userId = decodedJWT.getClaim("userId");
return userId.asInt();
}catch (TokenExpiredException e){
e.printStackTrace();
}
return null;
}
/**
* 从JWT里的获取用户名
*/
public static String getUsername(String token){
try{
// 使用秘钥创建一个解析对象
JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
//验证JWT
DecodedJWT decodedJWT = jwtVerifier.verify(token);
Claim username = decodedJWT.getClaim("username");
return username.asString();
}catch (TokenExpiredException e){
e.printStackTrace();
}
return null;
}
/**
* 从JWT里的获取用户权限
*/
public List<String> getAuth(String token){
try{
// 使用秘钥创建一个解析对象
JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
//验证JWT
DecodedJWT decodedJWT = jwtVerifier.verify(token);
Claim auth = decodedJWT.getClaim("auth");
return auth.asList(String.class);
}catch (TokenExpiredException e){
e.printStackTrace();
}
return null;
}
}
单元测试
@Autowired
private JWTUtil jwtUtil;
@Test
void testjwt(){
List<String> authList = Arrays.asList("student:query", "student:add");
String obamaToken = jwtUtil.createToken(19, "obama", authList);
jwtUtil.verifyToken(obamaToken);
}
2. base64
2.1 什么是Base64
- 所谓Base64,就是说选出64个字符:小写字母a-z、大写字母A-Z、数字0-9、符号"+“、”/“(再加上可能的”=",实际上是使用65个字符),作为一个基本字符集。然后,其他所有符号都转换成这个字符集中的字符。
- base64是一个编码方案. 并非加密方案
- base64的编码方案是:每遇到三个字节就按照特定规则编码为4个字节, 因此最后文件体积一定是变大了
- 既然每三个字节才编码为一次为4个字节, 那么最后可能会剩下1/2个字节没法编码, 此时用1/2个"=表示"
2.2 linux base64命令
2.2.1 Linux下用base64命令编解码字符串
#编码:
#echo 命令自带换行符, 相当于用Hello World加上一个换行符进行base64编码
echo 'Hello World' | base64
SGVsbG8gV29ybGQK
#-n参数指定去掉换行符,这样才是对Hello World本身进行编码
echo -n 'Hello World' | base64
SGVsbG8gV29ybGQ=
#解码:同理也要去掉换行符
echo -n 'SGVsbG8gV29ybGQ=' | base64 -d
Hello World
备注:
echo -n ‘{“alg”:“HS256”,“typ”:“JWT”}’ | base64
2.2.1 Linux下用base64命令编解码文件
#base64编码
# base64 待编码的文件名 > 编码后的文件名
base64 1.mp3 > mymp3
#base64 -d 待解码的文件名 >解码后的文件名
base64 -d mymp3>88.mp3
2.3 在网络中使用baseUrl
为了能在http传输中也使用base64的基本编码规则, 对其进行一定改造:
1、明文使用BASE64进行编码
2、在Base64编码的基础上进行以下的处理:
1)去除尾部的"="
2)把"+“替换成”-"
3)斜线"/“替换成下划线”_"
这就是baseUrl