JWT( JSON Web Token —— JSON Web 令牌 )的学习笔记
一、跨域认证的问题
互联网服务离不开用户认证。一般流程是下面这样:
1、用户向服务器发送用户名和密码。 2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。 3、服务器向用户返回一个 session_id,写入用户的 Cookie。 4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。 5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。
举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。
二、Cookie的概要
cookie 是一个非常具体的东西,指的就是浏览器里面能永久存储的一种数据,仅仅是浏览器实现的一种数据存储功能。
cookie 由服务器生成,发送给浏览器,浏览器把 cookie 以 K-V 形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该cookie发送给服务器。由于cookie是存在客户端上的,所以浏览器加入了一些限制确保 cookie 不会被恶意使用,同时不会占据太多磁盘空间,所以每个域的 cookie 数量是有限的。
三、传统Session
3.1、认证方式
http 协议本身是一种无状态的协议,如果用户向服务器提供了用户名和密码来进行用户认证,下次请求时,用户还要再一次进行用户认证才行。因为根据http协议,服务器并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储─份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为 cookie,以便下次请求时发送给我们的应用,这样应用就能识别请求来自哪个用户。
3.2、暴露的问题
Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
在前后端分离系统中应用解耦后增加了部署的复杂性。通常用户一次请求就要转发多次。如果用session每次携带sessionid到服务器,服务器还要查询用户信息。同时如果用户很多。这些信息存储在服务器内存中,给服务器增加负担。还有就是sessionid就是一个特征值,表达的信息不够丰富。不容易扩展。而且如果你后端应用是多节点部署。那么就需要实现session共享机制。不方便集群应用。
3.3、cookie和session的区别
session 是存储服务器端,cookie 是存储在客户端,所以 session 的安全性比 cookie 高。
获取 session 里的信息是通过存放在会话 cookie 里的 session id 获取的。而 session 是存放在服务器的内存中里,所以 session 里的数据不断增加会造成服务器的负担,所以会把很重要的信息存储在 session 中,而把一些次要东西存储在客户端的 cookie 里。
cookie 确切的说分为两大类:会话 cookie 和持久化 cookie。
会话 cookie 是存放在客户端浏览器的内存中,他的生命周期和浏览器是一致的,当浏览器关闭会话 cookie 也就消失了
持久化 cookie 是存放在客户端硬盘中,持久化 cookie 的生命周期是我们在设置 cookie 时候设置的那个保存时间。
session 的信息是通过 sessionid 获取的,而 sessionid 是存放在会话 cookie 当中的,当浏览器关闭的时候会话 cookie消失,所以sessionid也就消失了,但是session 的信息还存在服务器端,只是查不到所谓的 session,但它并不是不存在。所以 session 在服务器关闭的时候,或者是 session 过期,又或者调用了invalidate(),再或者是 session 中的某一条数据消失调用 session.removeAttribute() 方法,session 在通过调用 session.getsession 来创建的。
四、基于传统token的认证
基于 Token 的身份验证是无状态的,我们不用将用户信息存在服务器或 Session 中。这种概念解决了在服务端存储信息时的许多问题。没有 session 信息意味着你的程序可以根据需要去增减机器,而不用去担心用户是否登录和已经登录到了哪里。
4.1、Token 身份验证的过程
虽然基于Token的身份验证实现的方式很多,但大致过程如下:
用户通过用户名和密码发送请求。
程序验证。
程序返回一个签名的 token 给客户端。
客户端储存 token, 并且每次请求都会附带它。
服务端验证 token 并返回数据。
每一次请求都需要 Token。Token 应该在 HTTP的头部发送从而保证了 Http 请求无状态。我们也需要设置服务器属性
Access-Control-Allow-Origin: *
来让服务器能接受到来自所有域的请求。需要注意的是,在ACAO头部指定 * 时,不得带有像HTTP认证,客户端SSL证书和cookies的证书。
实现思路:
1.用户登录校验,校验成功后就返回Token给客户端。
2.客户端收到数据后保存在客户端
3.客户端每次访问API是携带Token到服务器端。
4.服务器端采用filter过滤器校验。校验成功则返回请求数据,校验失败则返回错误码
当我们在程序中认证了信息并取得 token 之后,我们便能通过这个 token 做许多的事情。我们甚至能基于创建一个基于权限的token传给第三方应用程序,这些第三方程序能够获取到我们的数据(当然只限于该 token 被允许访问的数据)。
4.2、Tokens的优势
(1)无状态、可扩展
在客户端存储的 token 是无状态的,并且能够被扩展。基于这种无状态和不存储Session信息,负载均衡服务器 能够将用户的请求传递到任何一台服务器上,因为服务器与用户信息没有关联。相反在传统方式中,我们必须将请求发送到一台存储了该用户 session 的服务器上(称为Session亲和性),因此当用户量大时,可能会造成 一些拥堵。使用 token 完美解决了此问题。
(2)安全性
请求中发送 token 而不是 cookie,这能够防止 CSRF(跨站请求伪造) 攻击。即使在客户端使用 cookie 存储 token,cookie 也仅仅是一个存储机制而不是用于认证。另外,由于没有 session,让我们少我们不必再进行基于 session 的操作。
Token 是有时效的,一段时间之后用户需要重新验证。我们也不一定需要等到token自动失效,token有撤回的操作,通过 token revocataion可以使一个特定的 token 或是一组有相同认证的 token 无效。
(3)可扩展性
使用 Tokens 能够与其它应用共享权限。例如,能将一个博客帐号和自己的QQ号关联起来。当通过一个 第三方平台登录QQ时,我们可以将一个博客发到QQ平台中。
使用 token,可以给第三方应用程序提供自定义的权限限制。当用户想让一个第三方应用程序访问它们的数据时,我们可以通过建立自己的API,给出具有特殊权限的tokens。
(4)多平台与跨域
我们已经讨论了CORS (跨域资源共享)。当我们的应用和服务不断扩大的时候,我们可能需要通过多种不同平台或其他应用来接入我们的服务。
可以让我们的API只提供数据,我们也可以从CDN提供服务(Having our API just serve data, we can also make the design choice to serve assets from a CDN.)。 在为我们的应用程序做了如下简单的配置之后,就可以消除 CORS 带来的问题。只要用户有一个通过了验证的token,数据和资源就能够在任何域上被请求到。
Access-Control-Allow-Origin: *
(5)基于标准
有几种不同方式来创建 token。最常用的标准就是 JSON Web Tokens。很多语言都支持它。
关于token 的笔记来源:基于Token的身份验证的原理 - valentin - 博客园 (cnblogs.com)
五、JWT 的介绍
5.0、JWT 的认证
认证流程:
前端通过Web表单将自己的用户名和密码发送到后端的接口。该过程一般是HTTP的POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。
后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage(浏览器本地缓存)或sessionStorage(session缓存)上,退出登录时前端删除保存的JWT即可。
前端在每次请求时将JWT放入HTTP的Header中的Authorization位。(解决XSS和XSRF问题)HEADER
后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确﹔检查Token是否过期;检查Token的接收方是否是自己(可选)
验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
5.1、jwt 的概念
1. JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的
2. 是目前流行的跨域认证解决方案,一种基于JSON的、用于在网络上声明某种主张的令牌(token)
3. 该 token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景
4. JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 token 也可直接被用于认证,也可被加密。
5.2、JWT 的原理
JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样:
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2018年7月1日0点0分"
}
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展
5.3、什么场景应该使用 JWT
Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
5.4、JWT 的数据结构
实际的 JWT 大概就像下面这样:
它是一个很长的字符串,中间用点(.
)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。
JWT 的三个部分依次如下:
Header(头部)
Payload(负载)
Signature(签名)
5.4.1、Header 头部的概要
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)
{
"alg": "HS256",
"typ": "JWT"
}
- typ 为声明类型,指定 "JWT"
- alg 为加密的算法,默认是 "HS256"
上面代码中,alg
属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ
属性表示这个令牌(token)的类型(type),JWT 令牌统一写为 JWT
最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。
Base64URL 算法:
5.4.2、Payload 装载的概要
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
5.4.2、Signature 签名的概要
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret(私钥) )
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用 "点"(.
)分隔,就可以返回给用户。
5.5、JWT 的使用方式
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization
字段里面。
Authorization: Bearer <token>
另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。
5.6、JWT 的特点
(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。
(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
jwt 的介绍原文来源:JSON Web Token 入门教程 - 阮一峰的网络日志 (ruanyifeng.com)
六、JJWT (提供端到端的JWT创建和验证的Java库)的具体使用
JJWT的目标是最容易使用和理解用于在JVM上创建和验证JSON Web令牌(JWTs)的库。
JJWT是基于JWT、JWS、JWE、JWK和JWA RFC规范的Java实现。
JJWT还添加了一些不属于规范的便利扩展,比如JWT压缩和索赔强制。
6.1、基本使用 JJWT 生成密钥与 Token 字符串
6.1.1、添加依赖
pom.xml代码如下:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Uncomment this next dependency if you are using JDK 10 or earlier and you also want to use
RSASSA-PSS (PS256, PS384, PS512) algorithms. JDK 11 or later does not require it for those algorithms:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
<scope>runtime</scope>
</dependency>
-->
6.1.2、生成 JWT
CreateJwtTest.java文件代码如下:
package com.example.jjwwdfdfs; import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest; import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
import java.util.UUID; @SpringBootTest
public class crejwtsd {
//过期毫秒时长
public static final long Expiration=24*60*60*1000;
//密钥
private static final String secretString="Zd+kZozTI5OgURtbegh8E6KTPghNNe/tEFwuLxd2UNw=";
//生成安全密钥
private static final SecretKey KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString)); /**生成密钥*/
@Test
public void genKey(){
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String secretString = Encoders.BASE64.encode(key.getEncoded());
System.out.println("密钥:"+secretString);
} @Test
public void creatJWT(){
//创建一个Jwt构造器
JwtBuilder builder = Jwts.builder();
//设置签发时间
builder.setIssuedAt(new Date());
//设置过期时间
builder.setExpiration(new Date(System.currentTimeMillis()+Expiration));
//设置Id
builder.setId(UUID.randomUUID().toString());
//设置主题
builder.setSubject("auth");
//设置自定义信息
builder.claim("username","zhangsan");
builder.claim("role","admin");
//设置签名
builder.signWith(KEY);
//生成token字符串
String token=builder.compact();
System.out.println("token字符串:"+token);
}
}
运行结果:
6.2、完整生成 JJWT 与解析 JJWT
JwtDemoTest.java文件代码如下:
package com.example.jjwwdfdfs; import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest; import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
import java.util.UUID; @SpringBootTest
class JjwwdfdfsApplicationTests { //密钥字符串
@Value("${jwt.token.secret}")
public String KEYSTRING; //签名安全密钥
public SecretKey getKey() {
SecretKey secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(KEYSTRING));
return secretKey;
} /**生成安全密钥,只执行一次*/
@Test
public void genSecretKey(){
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String secretString = Encoders.BASE64.encode(key.getEncoded());
System.out.println("安全密钥:"+secretString);
} //过期毫秒数
public static final long EXPIRETIME=24*60*60*4000; //1天 // public static final long EXPIRETIME=5*1000; //5秒
/**创建JWT*/
@Test
public void createJWT(){
System.out.println("yaml文件里面密钥的值:"+KEYSTRING);
//创建一个JWT构造器
JwtBuilder builder = Jwts.builder();
//header
builder.setHeaderParam("alg","HS256"); //签名加密算法的类型
builder.setHeaderParam("typ","JWT"); //token类型
//payload
builder.setIssuedAt(new Date()); //签发时间
builder.setExpiration(new Date(System.currentTimeMillis()+EXPIRETIME)); //过期时间
builder.setId(UUID.randomUUID().toString()); //JWT ID
builder.setSubject("auth"); //主题
builder.claim("username","admin"); //自定义信息
builder.claim("role","superadmin"); //角色,自定义信息
builder.signWith(getKey()); //设置签名的密钥
//生成签名
String token=builder.compact();
System.out.println("签名:"+token);
//eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoIn0.TYKaWoixlcu8ma27Bf_i_pNujBLvwtkiX8WoXpUpg6I
} @Test
public void parseJWT(){
String jwt ="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2Njg5Njc2OTcsImV4cCI6MTY2OTMxMzI5NywianRpIjoiN2M1OGJjOWEtNjY0Yi00MDllLWFkN2YtYmQwZjg5NDIwNmY1Iiwic3ViIjoiYXV0aCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoic3VwZXJhZG1pbiJ9.JGu5BbXWf1UjeCLSyPD1DqbGegbLFTQ9Q6dnP8ppkic";
// 创建解析器
JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder(); //jwt解析器
jwtParserBuilder.setSigningKey(getKey()); //设置签名的密钥
Jws<Claims> claimsJws = jwtParserBuilder.build().parseClaimsJws(jwt);//解析内容,获得payload System.out.println("头部:"+claimsJws.getHeader());
System.out.println("数据:"+claimsJws.getBody());
System.out.println("签名:"+claimsJws.getSignature()); JwsHeader header = claimsJws.getHeader();
Claims body = claimsJws.getBody(); System.out.println(header.getAlgorithm());
System.out.println(header.get("typ")); System.out.println(body.getExpiration());
System.out.println(body.get("username")); } }
运行结果:
七、用 JWT 中的授权与验证完成简单的登录示例
7.1、授权与验证(前端验证)
7.1.1、前端
7.1.1.1、创建一个Vue3的项目
7.1.1.2、添加路由功能
如果在创建项目的时候没有勾选 “ router ” 选项的话,需要导入依赖 vue-router,在命令行中使用 npm i vue-router@nex t即可
配置路由信息:src / router / index.ts代码如下
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: ()=>import('../views/Home.vue'),
// 验证哪些组件是否需要登录
meta:{
requiredAuthorized:false
}
},
{
path: '/product',
name: 'product',
component: ()=>import('../views/Product.vue'),
// 验证哪些组件是否需要登录
meta:{
requiredAuthorized:false
}
},
{
path: '/main',
name: 'main',
component: ()=>import('../views/Main.vue'),
// 验证哪些组件是否需要登录
meta:{
requiredAuthorized:true
}
},
{
path: '/login',
name: 'login',
component: ()=>import('../views/Login.vue'),
// 验证哪些组件是否需要登录
meta:{
requiredAuthorized:false
}
}
] const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
}) export default router
7.1.1.3、创建组件
组件一:views / Home.vue
<template lang="">
<div>
<p>当前位置:首页</p>
<p>
欢迎光临陶陶商城!
</p>
</div>
</template>
<script lang="ts" setup> </script>
<style lang=""> </style>
组件二:views / Product.vue
<template lang="">
<div>
<p>当前位置:商品展示</p>
<ol>
<li>心相印抽纸卫生纸面巾纸餐巾纸纸巾纸巾抽 ¥8.9</li>
<li>家用陶瓷砂锅大容量耐高温明火陶瓷煲砂锅熬粥炖锅石锅煲汤砂锅 ¥29.99</li>
<li>周淼10.12 20点新品王牌款 可拆卸连帽 拼色90白鸭绒羽绒服 ¥729</li>
<li>家用创意桌面垃圾桶小号迷你茶几办公北欧网红ins床头带盖收纳桶 ¥5</li>
<li>
HEYGIRL黑哥 榛果巧棕!韩系经典正肩挺阔呢子大衣女50羊毛呢外套 ¥436.05
</li>
<li>60大包400张抽纸纸巾整箱家用实惠装卫生纸擦手纸餐巾纸面巾纸批 ¥13.1</li>
<li>10斤全效去渍自然馨香洗衣液家庭实惠装批发促销洗衣凝珠香味持久 ¥5.1</li>
<li>5双篮球袜男女同款中长筒棉袜四季ins潮百搭鲨鱼裤学生运动船袜 ¥8.9</li>
<li>SUN11 磨毛白色衬衫女秋冬加绒2022新款衬衣叠穿内搭打底长袖上衣 ¥249</li>
</ol> </div>
</template>
<script lang="ts" setup> </script>
<style lang=""> </style>
组件三:views / Login.vue
<template lang="">
<div>
<p>当前位置:登录</p>
<fieldset>
<legend>用户信息</legend>
<p>
<label>账号:</label>
<input type="text" v-model="user.username">
</p>
<p>
<label>密码:</label>
<input type="password" v-model="user.password">
</p>
<p>
<button @click.prevent="loginHandle">登录</button>
</p>
</fieldset>
</div>
</template>
组件四:views / Main.vue
<!-- 个人中心页面 -->
<template lang="">
<div>
<p>当前位置:个人中心</p>
<p>
欢迎您:{{username}}
</p>
<hr>
<p>
订单信息:{{order}}
</p>
<p>
订单地址:{{address}}
</p>
</div>
</template>
7.1.1.4、创建路由守卫
// 路由守卫
router.beforeEach((to,from)=>{
// 判断该路由不是登录页,to.meta.requiredAuthorized=假,而且还没有登录
if (to.name!='login' && to.meta.requiredAuthorized && !isAuthorized()) {
// 回到登录页
return {name:"login",query:{returnUrl:to.path}};
} return true;
}); // isAuthorized:判断是否登录,客户端是否有登录信息
function isAuthorized(){
return !!localStorage.getItem("USER");
} export default router
7.1.2、后端
7.1.2.1、工具类
util / JWTUtil.java 代码如下:
package com.example.jwtlogin.util; import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys; import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
import java.util.UUID; public class JWTUtil { // 密钥
public static String KEYSTRING="hh1GG9NI67eAIGzjx5I5TuRB31jUFNpKzRhfLpqe4ZA="; // 签名安全密钥
public static SecretKey getKey() {
SecretKey secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(KEYSTRING));
return secretKey;
} /**生成安全密钥,只执行一次*/
public void genSecretKey(){
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String secretString = Encoders.BASE64.encode(key.getEncoded());
System.out.println(secretString);
} // 过期毫秒数
public static final long EXPIRETIME=24*60*60*1000; //1天 /**创建JWT*/
public static String createJWT(String username){
System.out.println(KEYSTRING);
//创建一个JWT构造器
JwtBuilder builder = Jwts.builder();
//header
builder.setHeaderParam("alg","HS256"); //签名加密算法的类型
builder.setHeaderParam("typ","JWT"); //token类型
//payload
builder.setIssuedAt(new Date()); //签发时间
builder.setExpiration(new Date(System.currentTimeMillis()+EXPIRETIME)); //过期时间
builder.setId(UUID.randomUUID().toString()); //JWT ID
builder.setSubject("auth"); //主题
builder.claim("username",username); //自定义信息 builder.signWith(getKey()); //设置签名的密钥
//生成签名
String token=builder.compact();
System.out.println(token);
return token;
//eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoIn0.TYKaWoixlcu8ma27Bf_i_pNujBLvwtkiX8WoXpUpg6I
} // 解析JWT
public static Jws<Claims> parseJWT(String jwt){
JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder(); //jwt解析器
jwtParserBuilder.setSigningKey(getKey()); //设置签名的密钥
Jws<Claims> claimsJws = jwtParserBuilder.build().parseClaimsJws(jwt);//解析内容 return claimsJws; } // 解析jwt字符
public static void verifyJWT(String jwt) {
Jwts.parserBuilder().setSigningKey(getKey()).build().parseClaimsJws(jwt);
} }
util / R.java 代码如下:
package com.example.jwtlogin.util; import java.util.HashMap;
import java.util.Map; /**
* 返回数据封装
*/
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L; public R() {
put("code", 1);
put("msg", "success");
} //错误时
public static R error() {
return error(500, "未知异常,请联系管理员");
} public static R error(String msg) {
return error(500, msg);
} public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
} //成功时
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
} public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
} public static R ok() {
return new R();
} public static R ok(Object data) {
return new R().put("data",data);
} @Override
public R put(String key, Object value) {
super.put(key, value);
return this;
}
}
7.1.2.2、User 用户实体
package com.example.jwtlogin.entity; import lombok.Data; @Data
public class User {
private String username;
private String password;
}
7.1.2.3、登录控制器
package com.example.jwtlogin.controller; import com.example.jwtlogin.entity.User;
import com.example.jwtlogin.util.JWTUtil;
import com.example.jwtlogin.util.R;
import org.springframework.web.bind.annotation.*; @RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*")
public class LoginController {
@PostMapping("/login")
public R login(@RequestBody User user){
if (user.getUsername().equals("admin")&&user.getPassword().equals("123456")){
// 根据用户名生成jwt令牌
String jwt= JWTUtil.createJWT(user.getUsername());
// 写完客户端
return R.ok().put("jwt",jwt).put("username",user.getUsername());
}else{
return R.error("用户名或密码错误!");
}
}
}
7.1.3、此时回到前端的登录页面,实现登录的功能
<template lang="">
<div>
<p>当前位置:登录</p>
<fieldset>
<legend>用户信息</legend>
<p>
<label>账号:</label>
<input type="text" v-model="user.username">
</p>
<p>
<label>密码:</label>
<input type="password" v-model="user.password">
</p>
<p>
<button @click.prevent="loginHandle">登录</button>
</p>
</fieldset>
</div>
</template> <script lang="ts" setup>
import axios from 'axios';
import { useRoute, useRouter } from "vue-router" const route = useRoute();
const router = useRouter(); // 全局设置后端地址
axios.defaults.baseURL = "http://localhost:8089/api/"
// 创建实例
const $http = axios.create({
// 配置项
baseURL: "http://localhost:8089/api/",
timeout: 90000 // 默认值为0,永远不超时
}); let user: { username?: string, password?: string } = {};
function loginHandle() {
// 首先删除原来有的登录信息,即把原来的token清除
localStorage.removeItem("USER");
$http.post("login", user).then(res => {
console.log(res.data); if (res.data.code === 1) {
// 将登陆成功的令牌保存到客户端
localStorage.setItem('USER', JSON.stringify(res.data));
let url = (route.query.returnUrl + '') || '/main';
router.replace({ path: url });
console.log(route);
} else {
alert(res.data.msg);
} }).catch(err => {
console.log(err); });
} </script>
<style lang=""> </style>
此时的运行效果:
1、首先,F12 检查一下有没有本地存储的 token 存在,如果存在就手动先把它删除
2、然后刷新页面,发现点击个人中心的页面是会自动跳到登录页面的,因为路由守卫规则,还没有登录,不能进入个人中心页面,接下来我们使用账号(admin)密码(123456)进行登录即可跳到个人中心页面
3、然后就是显示个人中心的订单信息与订单地址了
在后端中新建一个 user 控制器定义订单数据:
package com.example.jwtlogin.controller; import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; @RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*")
public class UserController {
@GetMapping("/user/order")
public String getOrder(){
return "用户私人得订单数据,需要验证身份才可以查看私人用户信息!";
} @GetMapping("/user/address")
public String getAddress(){
return "用户私人得订单数据,需要验证身份才可以查看私人用户信息!";
}
}
在前端的个人中心页面也需要发起 axios 请求获取数据
<!-- 个人中心页面 -->
<template lang="">
<div>
<p>当前位置:个人中心</p>
<p>
欢迎您:{{username}}
</p>
<hr>
<p>
订单信息:{{order}}
</p>
<p>
订单地址:{{address}}
</p>
</div>
</template>
<script lang="ts" setup>
import axios from 'axios';
import { ref } from 'vue';
import { useRoute, useRouter } from "vue-router" const route = useRoute();
const router = useRouter(); // 全局设置后端地址
axios.defaults.baseURL = "http://localhost:8089/api/"
// 创建实例
const $http = axios.create({
// 配置项
baseURL: "http://localhost:8089/api/",
timeout: 90000 // 默认值为0,永远不超时
}); // 获得用户信息
let username = "匿名用户";
let USER = localStorage.getItem("USER");
if (USER) {
let userinfo = JSON.parse(USER+"");
username=userinfo.username;
} // 订单信息
let order = ref("")
// 发起请求
$http.get("user/order").then(res=>{
order.value = res.data;
}).catch(err=>alert(err)) // 订单地址
let address = ref("")
// 发起请求
$http.get("user/address").then(res=>{
address.value = res.data;
}).catch(err=>alert(err)) </script>
<style lang=""> </style>
运行结果:
至此,jwt的授权功能就完成了!
但是注意:此时,有一个致命的问题,就是如果用户自定义修改了本地存储的密钥,是可以避开验证进入到个人中心页面的!!!
那怎么办呢???此时我们就要用到 jwt 的 " 后端验证 " 功能了
7.2、授权与验证(后端验证)
7.2.1、后端代码
7.2.1.1、添加hutool依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.10</version>
</dependency>
7.2.1.2、在 JwtloginApplication文件中添加注解
package com.example.jwtlogin; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan; @SpringBootApplication
@ServletComponentScan /** 让容器扫描 servlet */
public class JwtloginApplication { public static void main(String[] args) {
SpringApplication.run(JwtloginApplication.class, args);
} }
7.2.1.3、定义过滤器
config / AuthorizeFilter 文件代码如下:
package com.example.jwtlogin.config; import cn.hutool.json.JSONUtil;
import com.example.jwtlogin.util.JWTUtil;
import com.example.jwtlogin.util.R;
import org.springframework.core.annotation.Order; import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter; // /api/user/*:只要访问到这个路径下的,就会进入到doFilter方法中
@WebFilter(filterName = "AuthorizeFilter",urlPatterns = "/api/user/*")
@Order(99)
public class AuthorizeFilter implements Filter {
public void init(FilterConfig config) throws ServletException {
} public void destroy() {
} @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { // 刷新一下 main.vue 页面,里面有两个私有数据,所以这两个私有数据就都可以访问到了
// System.out.println("开始访问:"+request.getRemoteAddr()); HttpServletRequest httpServletRequest= (HttpServletRequest) request;
HttpServletResponse httpServletResponse= (HttpServletResponse) response; // 单独处理跨域
httpServletResponse.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:8082");// 这里需要填写的是,前端运行的路径+端口号
httpServletResponse.setHeader("Access-Control-Allow-Headers", "Accept, Origin, XRequestedWith, Content-Type, LastModified,token");
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS, HEAD"); // 取出 token
String token=httpServletRequest.getHeader("token"); //如果存在令牌,就证明拿到了token值
if(token==null||token.equals("")){
// 设置未授权或没登录(就是没有token)
renderJson(httpServletResponse, R.error(-1,"未授权"));
}else{
// 校验是否通过jwt
try {
JWTUtil.verifyJWT(token);
// 只要不报错就放行
chain.doFilter(request, response);
}
catch (Exception exp){ renderJson(httpServletResponse, R.error(-2,"验证失败:"+exp.getMessage()));
}
}
} /**
* 返回JSON数据
* @param response
* @param json
*/
private void renderJson(HttpServletResponse response, Object json){
// 设置编码格式
response.setCharacterEncoding("UTF-8");
// 设置响应内容
response.setContentType("application/json");
// response里面向请求值写入的值
try (PrintWriter writer = response.getWriter()){
// 向请求写入你要介绍的对象
writer.print(JSONUtil.toJsonStr(json));
} catch (IOException e) {
// 捕获异常
e.printStackTrace();
}
}
}
7.2.2、前端代码
7.2.2.1、在main.vue把token值带到服务器里面,然后在客户端做未授权处理
7.2.2.2、封装重复的代码(可不写),并定义拦截器
7.2.2.3、定义图形验证码
7.2.2.4、完整的登录示例代码如下
后端:
User.java 文件代码如下:
package com.example.jwtlogin.entity; import lombok.Data; @Data
public class User {
private String username;
private String password;
private String code; // 验证码
}
LoginController.java 文件代码如下:
package com.example.jwtlogin.controller; import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import com.example.jwtlogin.entity.User;
import com.example.jwtlogin.util.JWTUtil;
import com.example.jwtlogin.util.R;
import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; @RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*")
public class LoginController { /** 验证码 */
CircleCaptcha circleCaptcha=null; @GetMapping("/cap")
public void cap(HttpServletResponse response) throws IOException {
// 创建验证码的基本信息
circleCaptcha = CaptchaUtil.createCircleCaptcha(90,45,4,2);
// 响应出去,用HttpServletResponse
circleCaptcha.write(response.getOutputStream()); } /** 登录 */
@PostMapping("/login")
public R login(@RequestBody User user){
// 判断验证码的
if (circleCaptcha.verify(user.getCode())){
// 判断用户名和密码的
if (user.getUsername().equals("admin")&&user.getPassword().equals("123456")){
// 根据用户名生成jwt令牌
String jwt= JWTUtil.createJWT(user.getUsername());
// 写完客户端
return R.ok().put("jwt",jwt).put("username",user.getUsername());
}else{
return R.error("用户名或密码错误!");
}
}else {
return R.error("验证码错误!");
}
}
}
UserController.java 文件代码如下:
package com.example.jwtlogin.controller; import com.example.jwtlogin.util.R;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; @RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*")
public class UserController {
@GetMapping("/user/order")
public R getOrder(){
return R.ok().put("data","用户私人得订单数据,需要验证身份才可以查看私人用户信息!");
} @GetMapping("/user/address")
public R getAddress(){
return R.ok().put("data","用户私人得订单数据,需要验证身份才可以查看私人用户信息!");
}
}
过滤器与 R 类与JWTUtil 工具类在上文有写,没有更改!
前端代码:
main.ts 文件代码如下:
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import axios from "axios"; // 导入axios依赖 const app = createApp(App); // 创建实例
const $http = axios.create({
// 配置项
baseURL: "http://localhost:8089/api/",
timeout: 90000, // 默认值为0,永远不超时
}); // 创建拦截器,否则即使是登录成功了,也不会跳转页面
// 添加请求拦截器
$http.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
let USER = localStorage.getItem("USER");
if (USER) {
let userinfo = JSON.parse(USER + ""); config.headers!.token=userinfo.jwt
} return config;
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
); // 添加响应拦截器
$http.interceptors.response.use(
function (res) {
// 针对未授权做的处理
if (res.data.code===-1) { // 未授权,去登录
router.replace({name:"login"});
}else if(res.data.code===-2){
alert(res.data.msg);
}
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return res;
},
function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
}
); // 全局共享对象方法一:
app.config.globalProperties.$http = $http; app.use(router);
app.mount("#app");
Login.vue 文件代码如下:
<template lang="">
<div>
<p>当前位置:登录</p>
<fieldset>
<legend>用户信息</legend>
<p>
<label>账号:</label>
<input type="text" v-model="user.username">
</p>
<p>
<label>密码:</label>
<input type="password" v-model="user.password">
</p>
<p>
<label>验证码:</label>
<input v-model="user.code">
<p>
<!-- 这个点击事件实现的功能是:点击图片的时候会切换新的一张 -->
<img src="http://localhost:8089/api/cap" onclick="this.src='http://localhost:8089/api/cap?t='+new Date()">
</p>
</p>
<p>
<button @click.prevent="loginHandle">登录</button>
</p>
</fieldset>
</div>
</template> <script lang="ts" setup>
import { getCurrentInstance } from 'vue';
import { useRoute, useRouter } from "vue-router" const route = useRoute();
const router = useRouter(); // 获取全局的 $对象
const globalProperties:any = getCurrentInstance()?.appContext.config.globalProperties;
const $http = globalProperties.$http; let user: { username?: string, password?: string,code?:"" } = {};
function loginHandle() {
// 首先删除原来有的登录信息,即把原来的token清除
localStorage.removeItem("USER");
$http.post("login", user).then(res => {
console.log(res.data);
if (res.data.code === 1) {
// 将登陆成功的令牌保存到客户端
localStorage.setItem('USER', JSON.stringify(res.data));
let url = (route.query.returnUrl) || '/main';
router.replace({ path: url + "" });
console.log(route);
} else {
alert(res.data.msg);
} }).catch(err => {
console.log(err); });
} </script>
<style lang=""> </style>
Main.vue 文件代码如下:
<!-- 个人中心页面 -->
<template lang="">
<div>
<p>当前位置:个人中心</p>
<p>
欢迎您:{{username}}
</p>
<hr>
<p>
订单信息:{{order}}
</p>
<p>
订单地址:{{address}}
</p>
</div>
</template>
<script lang="ts" setup>
import { ref, getCurrentInstance } from 'vue';
import { useRoute, useRouter } from "vue-router" // 获取全局的 $对象
const globalProperties: any = getCurrentInstance()?.appContext.config.globalProperties;
const $http = globalProperties.$http;
let username = "匿名用户"; let USER = localStorage.getItem("USER");
if (USER) {
username = JSON.parse(USER).username;
} const route = useRoute();
const router = useRouter(); // 订单信息
let order = ref("")
// 发起请求
$http.get("user/order").then(res => {
// 正常的,可登录的
if (res.data.code == 1) {
// 直接取里面的数据
order.value = res.data.data;
} }).catch(err => alert(err)) // 订单地址
let address = ref("")
// 发起请求
$http.get("user/address").then(res => {
// 正常的,可登录的
if (res.data.code == 1) {
// 直接取里面的数据
address.value = res.data.data;
} }).catch(err => alert(err)) </script>
<style lang=""> </style>
运行结果:
JWT( JSON Web Token —— JSON Web 令牌 )的学习笔记的更多相关文章
- 利用Redis撤销JSON Web Token产生的令牌
利用Redis撤销JSON Web Token产生的令牌 作者:chszs.版权全部.未经允许,不得转载.博主主页:http://blog.csdn.net/chszs 早先的博文讨论了在Angula ...
- JSON Web Token – 在 Web 应用间安全地传递信息
出处:子回 使用 JWT 令牌和 Spring Security 来实现身份验证 八幅漫画理解使用JSON Web Token设计单点登录系统
- JSON Web Token - 在Web应用间安全地传递信息(zhuan)
来自 http://blog.leapoahead.com/2015/09/06/understanding-jwt/ JSON Web Token(JWT)是一个非常轻巧的规范.这个规范允许我们使用 ...
- JSON Web Token - 在Web应用间安全地传递信息
转载自:http://blog.leapoahead.com/2015/09/06/understanding-jwt/ JSON Web Token(JWT)是一个非常轻巧的规范.这个规范允许我们使 ...
- [转]JSON Web Token - 在Web应用间安全地传递信息
JSON Web Token(JWT)是一个非常轻巧的规范.这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息. 让我们来假想一下一个场景.在A用户关注了B用户的时候,系统发邮件给B用户, ...
- (转)JSON Web Token - 在Web应用间安全地传递信息
JSON Web Token(JWT)是一个非常轻巧的规范.这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息. 让我们来假想一下一个场景.在A用户关注了B用户的时候,系统发邮件给B用户, ...
- HTML5权威指南--Web Storage,本地数据库,本地缓存API,Web Sockets API,Geolocation API(简要学习笔记二)
1.Web Storage HTML5除了Canvas元素之外,还有一个非常重要的功能那就是客户端本地保存数据的Web Storage功能. 以前都是用cookies保存用户名等简单信息. 但是c ...
- IDEA自动部署WEB工程至远程服务器(学习笔记)
一.部署Web工程的几种方式 ①本地打war,上传至远程服务器tomcat容器即可 优点:简单粗暴 缺点:浪费时间 ②IDEA自动部署至远程服务器 优点:节省大量时间 缺点:配置稍多(第一次) 二.I ...
- 《Web接口开发与自动化测试》学习笔记(一)
一.Django的入门 学习思路:先安装Django,然后在建立一个项目,接着运行这个项目,最后修改一下这个项目的数据,学习一下Django的原理之类的. 1.安装Django $pip instal ...
- JAVA Web day02--- Android小白的第二天学习笔记
CSS(美工部分知识,了解) 1. CSS概述 1.1.CSS是什么? * CSS 指层叠样式表 样式表:存储样式的地方 层叠:一层一层叠加 高大富有帅气人 1.2.CSS有什么作用? *CSS就是用 ...
随机推荐
- POJ 1985.Cow Marathon(DFS求树的直径模板题)
两次BFS/DFS求树的直径 我们可以先从任意一点开始DFS,记录下当前点所能到达的最远距离,这个点为P. 在从P开始DFS记录下所能达到的最远点的距离,这个点为Q. \(P , Q\)就是直径的端点 ...
- 航拍倾斜摄影 Web 3D GIS 数字孪生智慧火电厂
前言 7 月份,245 个国家气象站日最高气温突破 7 月历史极值:同时,疫情防控形势向好,企业加快复工达产节奏,电力负荷屡创新高.煤电作为我国最主要的电源,用不足 50% 的装机占比,生产了全国约 ...
- Threejs实现一个园区
一.实现方案 单独贴代码可能容易混乱,所以这里只讲实现思路,代码放在最后汇总了下. 想要实现一个简单的工业园区.主要包含的内容是一个大楼.左右两片停车位.四条道路以及多个可在道路上随机移动的车辆.遇到 ...
- python之十进制、二进制、八进制、十六进制转换
数字处理的时候偶尔会遇到一些进制的转换,以下提供一些进制转换的方法 一.十进制转化成二进制 使用bin()函数 1 x=10 2 print(bin(x)) 二.十进制转化为八进制 使用oct()函数 ...
- 05_删除链表的倒数第N个节点
删除链表的倒数第N个节点 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点. 示例 1: 输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] 示例 2: ...
- 【MCU】浮点数如何判等
[来源]https://mp.weixin.qq.com/s/481H4imm73IIS1yFI7-DNA
- java - 对象装载数据传递到方法中
1. 创建 Phone 类 package class_object; public class Phone { String brand; String color; double price; v ...
- Jquery - 获取所有子节点 ( 并删除 )
1,获取所有子节点 $(".parent").find('.child') 2,获取所有子节点,通过上层 div 的类名 , 获取上层 div 节点 $(".pare ...
- electron打包,使用electron-packager
构建项目可以使用electron-forge构建,但是这个东西打包比较坑,mac运行报错,win下会有缓存机制,也就是热更新无效 所以选择使用electron-packager打包 sudo npm ...
- [转帖]堆表&索引组织表
堆表&索引组织表 https://zhuanlan.zhihu.com/p/487271927 15 人赞同了该文章 很多大佬强调学习一定要看"原版英文材料". 比如再 ...