SpringBoot学习笔记(八)——JWT、(Vue3、Axios、Vue-Router、TypeScript实现授权与验证示例)
一、JWT概要
1.0、认证方式
1.0.1、基于Session的认证
基于session认证所显露的问题:
Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
1.0.2、基于传统token的认证
传统的Token,例如:用户登录成功生成对应的令牌,key为令牌 value:userid,隐藏了数据真实性 ,同时将该token存放到redis中,返回对应的真实令牌给客户端存放。
客户端每次访问后端请求的时候,会传递该token在请求中,服务器端接收到该token之后,从redis中查询如果存在的情况下,则说明在有效期内,如果在Redis中不存在的情况下,则说明过期或者token错误。
1.1. JWT是什么
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
是目前流行的跨域认证解决方案,一种基于JSON的、用于在网络上声明某种主张的令牌(token)。
该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
原理:jwt验证方式是将用户信息通过加密生成token,每次请求服务端只需要使用保存的密钥验证token的正确性,不用再保存任何session数据了,进而服务端变得无状态,容易实现拓展。
1.2. 什么时候你应该用JWT
下列场景中使用JSON Web Token是很有用的:
- Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
- Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
1.3. JWT的结构是什么样的
JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:
- Header
- Payload
- Signature
因此,一个典型的JWT看起来是这个样子的:
xxxxx.yyyyy.zzzzz
接下来,具体看一下每一部分:
1.3.1、Header头部
header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。
例如:
然后,用Base64对这个JSON编码就得到JWT的第一部分
- typ 为声明类型,指定 "JWT"
- alg 为加密的算法,默认是 "HS256"
也可以是下列中的算法:
JWS | 算法名称 | 描述 |
---|---|---|
HS256 | HMAC256 | HMAC with SHA-256 |
HS384 | HMAC384 | HMAC with SHA-384 |
HS512 | HMAC512 | HMAC with SHA-512 |
RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 |
RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 |
RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 |
ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 |
ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 |
ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 |
1.3.2、Payload装载
JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。
- Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
- Public claims : 可以随意定义。
- Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。
下面是一个例子:
对payload进行Base64编码就得到JWT的第二部分
注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的。
1.3.3、Signature签名
为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可。
例如:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
看一张官网的图就明白了:
1.4. JWT是如何工作的
在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token将会被返回。此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。
无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,通常放在Authorization header中,用Bearer schema。
header应该看起来是这样的:
Authorization: Bearer <token>
服务器上的受保护的路由将会检查Authorization header中的JWT是否有效,如果有效,则用户可以访问受保护的资源。如果JWT包含足够多的必需的数据,那么就可以减少对某些操作的数据库查询的需要,尽管可能并不总是如此。
如果token是在授权头(Authorization header)中发送的,那么跨源资源共享(CORS)将不会成为问题,因为它不使用cookie。
下面这张图显示了如何获取JWT以及使用它来访问APIs或者资源:
- 应用(或者客户端)想授权服务器请求授权。例如,如果用授权码流程的话,就是/oauth/authorize
- 当授权被许可以后,授权服务器返回一个access token给应用
- 应用使用access token访问受保护的资源(比如:API)
1.5. 基于Token的身份认证 与 基于服务器的身份认证
1.5.1. 基于服务器的身份认证
在讨论基于Token的身份认证是如何工作的以及它的好处之前,我们先来看一下以前我们是怎么做的:
HTTP协议是无状态的,也就是说,如果我们已经认证了一个用户,那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证
传统的做法是将已经认证过的用户信息存储在服务器上,比如Session。用户下次请求的时候带着Session ID,然后服务器以此检查用户是否认证过。
这种基于服务器的身份认证方式存在一些问题:
- Sessions : 每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大。
- Scalability : 由于Session是在内存中的,这就带来一些扩展性的问题。
- CORS : 当我们想要扩展我们的应用,让我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题。当使用AJAX调用从另一个域名下获取资源时,我们可能会遇到禁止请求的问题。
- CSRF : 用户很容易受到CSRF攻击。
1.5.2. JWT与Session的差异
相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。
Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。
而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。
Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。
1.5.3. 基于Token的身份认证是如何工作的
基于Token的身份认证是无状态的,服务器或者Session中不会存储任何用户信息。
没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。
虽然这一实现可能会有所不同,但其主要流程如下:
- 用户携带用户名和密码请求访问
- 服务器校验用户凭据
- 应用提供一个token给客户端
- 客户端存储token,并且在随后的每一次请求中都带着它
- 服务器校验token并返回数据
注意:
- 每一次请求都需要token
- Token应该放在请求header中
- 我们还需要将服务器设置为接受来自所有域的请求,用Access-Control-Allow-Origin: *
1.5.4. 用Token的好处
- 无状态和可扩展性:Tokens存储在客户端。完全无状态,可扩展。我们的负载均衡器可以将用户传递到任意服务器,因为在任何地方都没有状态或会话信息。
- 安全:Token不是Cookie。(The token, not a cookie.)每次请求的时候Token都会被发送。而且,由于没有Cookie被发送,还有助于防止CSRF攻击。即使在你的实现中将token存储到客户端的Cookie中,这个Cookie也只是一种存储机制,而非身份认证机制。没有基于会话的信息可以操作,因为我们没有会话!
还有一点,token在一段时间以后会过期,这个时候用户需要重新登录。这有助于我们保持安全。还有一个概念叫token撤销,它允许我们根据相同的授权许可使特定的token甚至一组token无效。
1.5.5. JWT与OAuth的区别
- OAuth2是一种授权框架 ,JWT是一种认证协议
- 无论使用哪种方式切记用HTTPS来保证数据的安全性
- OAuth2用在使用第三方账号登录的情况(比如使用weibo, qq, github登录某个app),而JWT是用在前后端分离, 需要简单的对后台API进行保护时使用。
1.5.6. 关于OAuth可以参考下面几篇
1.6. 参考
https://tools.ietf.org/html/rfc7519#section-3
http://blog.leapoahead.com/2015/09/06/understanding-jwt/
https://cnodejs.org/topic/557844a8e3cc2f192486a8ff
http://blog.leapoahead.com/2015/09/07/user-authentication-with-jwt/
二、JJWT实现
2.1、概要
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
- JJWT的目标是最容易使用和理解用于在JVM上创建和验证JSON Web令牌(JWTs)的库。
- JJWT是基于JWT、JWS、JWE、JWK和JWA RFC规范的Java实现。
- JJWT还添加了一些不属于规范的便利扩展,比如JWT压缩和索赔强制。
源码:https://github.com/jwtk/jjwt
2.2、规范兼容
- 创建和解析明文压缩JWTs
- 创建、解析和验证所有标准JWS算法的数字签名紧凑JWTs(又称JWSs):
- HS256: HMAC using SHA-256
- HS384: HMAC using SHA-384
- HS512: HMAC using SHA-512
- RS256: RSASSA-PKCS-v1_5 using SHA-256
- RS384: RSASSA-PKCS-v1_5 using SHA-384
- RS512: RSASSA-PKCS-v1_5 using SHA-512
- PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
- PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384
- PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512
- ES256: ECDSA using P-256 and SHA-256
- ES384: ECDSA using P-384 and SHA-384
- ES512: ECDSA using P-521 and SHA-512
2.3、添加依赖
<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> -->
如果您使用的是JDK 10或更早版本,并且还希望使用RSASSA-PSS(PS256、PS384、PS512)算法。JDK 11或更高版本不需要这些算法,需要依赖bcprov-jdk15on。
2.4、生成JWT
package com.myproject.studentmis4; 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.boot.test.context.SpringBootTest; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.Date; import java.util.UUID; @SpringBootTest public class JWTTest { //过期毫秒时长 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); } }
运行结果:
eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2Njg2NDQ0MjksImV4cCI6MTY2ODczMDgyOSwianRpIjoiYTQ4NjJiNWYtZTg3NS00ZGQ5LTg1M2ItZTJmZjAyY2Y1NDViIiwic3ViIjoiYXV0aCIsInVzZXJuYW1lIjoiemhhbmdzYW4iLCJyb2xlIjoiYWRtaW4ifQ.VlEr3LZNc941vugUU8Cvxh5DX7h6rL1T3WSVZA81080
自定义密钥
您的密钥字符串是 Base64 编码的吗?如果是这样,请执行以下操作:
@Value("${jwt.token.secret}")
private String secret;
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(this.secret);
return Keys.hmacShaKeyFor(keyBytes);
}
JwtToken.builder().value(Jwts.builder()
.setClaims(createClaims(account))
.setSubject(subject.toString())
.setIssuedAt(Date.from(createdDateTime))
.setExpiration(Date.from(expirationDateTime))
.signWith(getSigningKey())
.compact()).expiration(expirationDateTime.toString()).build()
如果您的密钥不是 base64 编码的(它可能应该是,因为例如,如果您使用原始密码,您的密钥可能不正确或格式不正确),您可以通过以下方式执行此操作:
private Key getSigningKey() {
byte[] keyBytes = this.secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
但是,通常不建议使用第二个示例,因为这可能意味着您的密钥格式不佳。格式良好的安全随机密钥不是人类可读的,因此要将其存储为字符串,密钥字节通常首先进行 base64 编码。
从文档https://github.com/jwtk/jjwt#jws-key-create:
如果要生成足够强的 SecretKey 以用于 JWT HMAC-SHA 算法,请使用
Keys.secretKeyFor(SignatureAlgorithm)
辅助方法:
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //or HS384 or HS512
在幕后,JJWT 使用 JCA 提供程序的 KeyGenerator 为给定算法创建具有正确最小长度的安全随机密钥。
如果您有现有的 HMAC SHA SecretKey 的编码字节数组,则可以使用
Keys.hmacShaKeyFor
辅助方法。例如:
byte[] keyBytes = getSigningKeyFromApplicationConfiguration(); SecretKey key = Keys.hmacShaKeyFor(keyBytes);
2.5、解析JWT
package com.myproject.studentmis4; 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.boot.test.context.SpringBootTest; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.Date; import java.util.UUID; @SpringBootTest public class JWTTest { //过期毫秒时长 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 parseJWT(){ String token="eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2Njg2NDQwNjMsImV4cCI6MTY2ODczMDQ2MywianRpIjoiODI3N2FhMjgtMGJmOC00YjY0LWE3M2ItMjk3YWIyY2JhNDZmIiwic3ViIjoiYXV0aCIsInVzZXJuYW1lIjoiemhhbmdzYW4iLCJyb2xlIjoiYWRtaW4ifQ.oUX0iRjKMANNdFUmJdHgq3BJ_d4q54928p_leBx_JU0"; //创建解析器 JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder(); //设置签名密钥 jwtParserBuilder.setSigningKey(KEY); //解析token获得payload Jws<Claims> claimsJws = jwtParserBuilder.build().parseClaimsJws(token); System.out.println(claimsJws.getHeader()); System.out.println(claimsJws.getBody()); System.out.println(claimsJws.getSignature()); } }
生成结果:
JWT示例代码:
package com.zhangguo.jwtdemo; 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 public class JWTTest { //密钥字符串 @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*1000; //1天 public static final long EXPIRETIME=5*1000; //5秒 /**创建JWT*/ @Test public void createJWT(){ 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","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.eyJpYXQiOjE2Njg3Mzg4MDMsImV4cCI6MTY2ODczODgwOCwianRpIjoiOTNmNWJhODctMGQ0OC00MzhhLTg3MTAtMmMyNDc2OGRlZWY1Iiwic3ViIjoiYXV0aCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoic3VwZXJhZG1pbiJ9.d8kW9bwTsAOTEQrPhbzk8-1J5gZceXqXrfVYSr8mZ2M"; JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder(); //jwt解析器 jwtParserBuilder.setSigningKey(getKey()); //设置签名的密钥 Jws<Claims> claimsJws = jwtParserBuilder.build().parseClaimsJws(jwt);//解析内容 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")); } }
application.yaml文件:
jwt:
token:
secret: "hh1GG9NI67eAIGzjx5I5TuRB31jUFNpKzRhfLpqe4ZA="
三、Hutool工具类实现JWT
3.1、由来
从5.7.0开始,Hutool提供了零依赖的JWT(JSON Web Token)实现。
3.2、JWT介绍
相关资料网络上非常多,可以自行搜索,简单点说JWT就是一种网络身份认证和信息交换格式。
3.2.1、结构
- Header 头部信息,主要声明了JWT的签名算法等信息
- Payload 载荷信息,主要承载了各种声明并传递明文数据
- Signature 签名,拥有该部分的JWT被称为JWS,也就是签了名的JWS,用于校验数据
整体结构是:
header.payload.signature
3.3、使用
JWT模块的核心主要是两个类:
JWT
类用于链式生成、解析或验证JWT信息。JWTUtil
类主要是JWT的一些工具封装,提供更加简洁的JWT生成、解析和验证工作
3.3.1、JWT生成
- HS265(HmacSHA256)算法
// 密钥
byte[] key = "1234567890".getBytes();
String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setKey(key)
.sign();
生成的内容为:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9.536690902d931d857d2f47d337ec81048ee09a8e71866bcc8404edbbcbf4cc40
- 其他算法
// 密钥
byte[] key = "1234567890".getBytes();
// SHA256withRSA
String id = "rs256";
JWTSigner signer = JWTSignerUtil.createSigner(id,
// 随机生成密钥对,此处用户可自行读取`KeyPair`、公钥或私钥生成`JWTSigner`
KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));
String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setSigner(signer)
.sign();
- 不签名JWT
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9.
String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setSigner(JWTSignerUtil.none())
.sign()
示例代码:
package com.myproject.studentmis4; import cn.hutool.jwt.JWT; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import java.util.Date; import java.util.UUID; @SpringBootTest public class HutoolJWTTest { byte[] key="+6P1SPDJKQltJdSJq2W4IqPRLon/gQg2Z+dAMoqfYtU=".getBytes(); @Test public void createJWT(){ String token=JWT.create() .setHeader("alg","HS256") //加密算法 .setHeader("typ","JWT") //类型 .setIssuedAt(new Date()) //签发日期 .setExpiresAt(new Date(System.currentTimeMillis()+1000*60*60*24)) //过期时间 .setKey(key) //密钥 .setJWTId(UUID.randomUUID().toString()) //JWT编号 .setSubject("auth") //主题 .setPayload("username","zhangsan123") //自定义信息 .setPayload("role","admin") //自定义信息 .sign(); //生成token System.out.println(token); } }
运行结果:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2Njg2NTY1MTYsImV4cCI6MTY2ODc0MjkxNiwianRpIjoiNjhkZmI2MDAtMGE2Yy00NjllLTg3MzYtNDViMDY0MGNkODQyIiwic3ViIjoiYXV0aCIsInVzZXJuYW1lIjoiemhhbmdzYW4xMjMiLCJyb2xlIjoiYWRtaW4ifQ.Ar2u1pZfLrj8cpvzCsgPp8u6gxA0I97jUp-uHorU2d0
3.3.2、JWT解析
String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9." +
"536690902d931d857d2f47d337ec81048ee09a8e71866bcc8404edbbcbf4cc40";
JWT jwt = JWT.of(rightToken);
// JWT
jwt.getHeader(JWTHeader.TYPE);
// HS256
jwt.getHeader(JWTHeader.ALGORITHM);
// 1234567890
jwt.getPayload("sub");
// looly
jwt.getPayload("name");
// true
jwt.getPayload("admin");
@Test public void parseJWTInfo() { String token="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2Njg2NTY1MTYsImV4cCI6MTY2ODc0MjkxNiwianRpIjoiNjhkZmI2MDAtMGE2Yy00NjllLTg3MzYtNDViMDY0MGNkODQyIiwic3ViIjoiYXV0aCIsInVzZXJuYW1lIjoiemhhbmdzYW4xMjMiLCJyb2xlIjoiYWRtaW4ifQ.Ar2u1pZfLrj8cpvzCsgPp8u6gxA0I97jUp-uHorU2d0"; JWT jwt = JWT.of(token); //解析jwt //getAlgorithm:HS256 System.out.println("getAlgorithm:"+jwt.getAlgorithm()); //算法 //typ:JWT System.out.println("typ:"+jwt.getHeader("typ")); //类型 //{"iat":1668656516,"exp":1668742916,"jti":"68dfb600-0a6c-469e-8736-45b0640cd842","sub":"auth","username":"zhangsan123","role":"admin"} System.out.println(jwt.getPayloads()); }
3.3.3、JWT验证
- 验证签名
String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9." +
"536690902d931d857d2f47d337ec81048ee09a8e71866bcc8404edbbcbf4cc40";
// 密钥
byte[] key = "1234567890".getBytes();
// 默认验证HS265的算法
JWT.of(rightToken).setKey(key).verify()
package com.myproject.studentmis4; import cn.hutool.jwt.JWT; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import java.util.Date; import java.util.UUID; @SpringBootTest public class HutoolJWTTest { byte[] key="+6P1SPDJKQltJdSJq2W4IqPRLon/gQg2Z+dAMoqfYtU=".getBytes(); @Test public void parseJWTVerify() { String token="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2Njg2NTY1MTYsImV4cCI6MTY2ODc0MjkxNiwianRpIjoiNjhkZmI2MDAtMGE2Yy00NjllLTg3MzYtNDViMDY0MGNkODQyIiwic3ViIjoiYXV0aCIsInVzZXJuYW1lIjoiemhhbmdzYW4xMjMiLCJyb2xlIjoiYWRtaW4ifQ.Ar2u1pZfLrj8cpvzCsgPp8u6gxA0I97jUp-uHorU2d0"; JWT.of(token).setKey(key).verify(); //验证是否正确 } }
- 详细验证
除了验证签名,Hutool提供了更加详细的验证:validate
,主要包括:
- Token是否正确
- 生效时间不能晚于当前时间
- 失效时间不能早于当前时间
- 签发时间不能晚于当前时间
使用方式如下:
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJNb0xpIiwiZXhwIjoxNjI0OTU4MDk0NTI4LCJpYXQiOjE2MjQ5NTgwMzQ1MjAsInVzZXIiOiJ1c2VyIn0.L0uB38p9sZrivbmP0VlDe--j_11YUXTu3TfHhfQhRKc";
byte[] key = "1234567890".getBytes();
boolean validate = JWT.of(token).setKey(key).validate(0);
四、登录示例
这里使用Vue3+TypeScript+VueRouter+Spring Boot+JWT实现一个简单的用户登录功能。
4.1、前端
4.1.1、创建一个Vue3的项目
创建一个Vue3项目,使用vue-cli,选择TypeScript,Babel
4.1.2、添加路由功能
依赖vue-router,在命令行中使用npm i vue-router@next即可
src/router/index.ts
import {createRouter,createWebHashHistory,RouteRecordRaw} from 'vue-router' const routes:RouteRecordRaw[]=[{ name:"home", path:"/", component:()=>import("../views/Home.vue"), meta:{ requiredAuth:false } },{ name:"product", path:"/product", component:()=>import("../views/Product.vue"), meta:{ requiredAuth:false } },{ name:"main", path:"/main", component:()=>import("../views/Main.vue"), meta:{ requiredAuth:true } },{ name:"login", path:"/login", component:()=>import("../views/Login.vue"), meta:{ requiredAuth:false } }]; const router = createRouter({ history:createWebHashHistory(), routes }) router.beforeEach((to,from)=>{ if(to.name!=="login"&&to.meta.requiredAuth&&!isAuthorizated()){ return {name:"login",query:{return:to.path}}; } return true; }); function isAuthorizated () { let USER=localStorage.getItem("USER"); return !!USER; } export default router;
4.1.3、创建组件
views/Home.vue
<template> <div> <h3>当前位置:首页</h3> <p>欢迎您光临天狗商城</p> </div> </template> <script lang="ts" setup></script> <style></style>
views/Product.vue
<template> <div> <h3>当前位置:商品列表</h3> <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></style>
views/Login.vue
<template> <div> <h3>当前位置:用户登录</h3> <fieldset> <legend>用户信息</legend> <p> <label>帐号:</label> <input v-model="user.username" /> </p> <p> <label>密码:</label> <input v-model="user.password" type="password" /> </p> <p> <button @click.prevent="login">登录</button> </p> </fieldset> </div> </template> <script setup lang="ts"> import {inject} from "vue" import { useRoute, useRouter } from "vue-router"; const axios: any = inject("axios"); let route = useRoute(); let router = useRouter(); let user = { username: "", password: "" }; function login() { localStorage.removeItem("USER"); axios.post("login", user).then((response) => { let result = response.data; console.log(result); if (result.code == 1 && result.jwt) { localStorage.setItem("USER", JSON.stringify(result)); //获取上一个请求的路径,返回路径,如果没有则直接进入main let returnUrl = route.query.returnUrl || "/main"; router.replace({ path: returnUrl + "" }); } else { alert("登录失败,请重试!"); } }); } </script> <style scoped></style>
views/Main.vue
<template> <div> <h3>当前位置:个人中心(后台)</h3> <p>欢迎您:{{ username }}</p> <p>我的订单:{{ orders }}</p> <p>收货地址:{{ address }}</p> </div> </template> <script lang="ts" setup> import { ref, getCurrentInstance, inject } from "vue"; let ctx = getCurrentInstance(); const axios: any = inject("axios"); let USER = localStorage.getItem("USER"); let username = ref(""); let orders = ref(""); let address = ref(""); if (USER) { let userinfo = JSON.parse(USER); username.value = userinfo.username; } let $http = ctx?.appContext.config.globalProperties.$http; $http .get("user/order") .then((response: any) => { orders.value = response.data.orders; }) .catch((err) => {}); axios .get("user/address") .then((response: any) => { address.value = response.data.address; }) .catch((err) => {}); </script> <style></style>
App.vue
<template> <div id="container"> <h2>天狗商城</h2> <div> <router-link :to="{ name: 'home' }">首页</router-link> | <router-link :to="{ name: 'product' }">商品</router-link> | <router-link :to="{ name: 'main' }">我的</router-link> | <router-link :to="{ name: 'login' }">登录</router-link> </div> </div> <div> <router-view></router-view> </div> </template> <script lang="ts"> import { defineComponent } from "vue"; export default defineComponent({ name: "App", }); </script> <style></style>
ts.config.js
4.1.4、程序主文件
import { createApp } from 'vue' import App from './App.vue' import router from './router/index' import axios from 'axios'; axios.defaults.baseURL="http://127.0.0.1:8089/api/"; //response拦截器 axios.interceptors.response.use( function (response) { if (response.data.code === 1) { //正常 return response } if (response.data.code === -1) { //没有token router.replace({ name: 'login' }); } // 错误信息的提示框 alert("错误:"+response.data.msg) // 将未处理的异常往外抛 // 注:如果没有return Promise.reject(response),或者是直接return; 的话,当前请求的响应仍然会进行then中,只是它的response是undefined的。 // 所以直接返回Promise.reject,在catch方法哪里就直接吃掉。 return Promise.reject(response) }, function (error) { // todo: 做一些其他日志记录处理 //alert('服务器错误,' + error); } ) // request拦截器 可对请求进行相应的处理 axios.interceptors.request.use( function (config) { config.withCredentials = true // 默认永不超时 0为永不超时 config.timeout = 0 // 设置请求的token let USER = localStorage.getItem('USER') let token=""; if(USER){ let userinfo=JSON.parse(USER); token=userinfo.jwt; } if (token) { config.headers = {token} } return config }, function (error) { // 对请求错误做些什么 return Promise.reject(error) } ) const app=createApp(App); app.config.globalProperties.$http=axios; app.provide("axios",axios); app.use(router); app.mount('#app')
4.1.5、帮助文档
axios帮助:https://www.axios-http.cn/docs/intro
4.2、后台
4.2.1、工具类
util.jwtUtil
package com.myproject.studentmis4.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.HashMap; import java.util.UUID; public class JWTUtil { //过期毫秒时长 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)); /**生成密钥*/ public static void genKey(){ Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); String secretString = Encoders.BASE64.encode(key.getEncoded()); System.out.println(secretString); } /**创建JWT*/ public static String creatJWT(HashMap<String,Object> payload){ //创建一个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.addClaims(payload); //设置签名 builder.signWith(KEY); //生成token字符串 String token=builder.compact(); System.out.println(token); return token; } /**解析JWT*/ public static Jws<Claims> parseJWT(String token){ //String token="eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2Njg2NDQwNjMsImV4cCI6MTY2ODczMDQ2MywianRpIjoiODI3N2FhMjgtMGJmOC00YjY0LWE3M2ItMjk3YWIyY2JhNDZmIiwic3ViIjoiYXV0aCIsInVzZXJuYW1lIjoiemhhbmdzYW4iLCJyb2xlIjoiYWRtaW4ifQ.oUX0iRjKMANNdFUmJdHgq3BJ_d4q54928p_leBx_JU0"; //创建解析器 JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder(); //设置签名密钥 jwtParserBuilder.setSigningKey(KEY); //解析token获得payload Jws<Claims> claimsJws = jwtParserBuilder.build().parseClaimsJws(token); System.out.println(claimsJws.getHeader()); System.out.println(claimsJws.getBody()); System.out.println(claimsJws.getSignature()); return claimsJws; } /**校验JWT*/ public static boolean veryfiJWT(String token){ //解析 try { Jwts.parserBuilder().setSigningKey(KEY).build().parseClaimsJws(token); } catch (Exception exp){ exp.printStackTrace(); return false; } return true; } }
R.java 封装返回数据对象
package com.myproject.studentmis4.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; } }
4.2.2、实体类
User 用户实体
package com.myproject.studentmis4.entity; /**用户实体*/ public class User { private String username; private String password; private String code; public User(String username, String password, String code) { this.username = username; this.password = password; this.code = code; } public User() { } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } }
UserIfno用户信息实体
package com.myproject.studentmis4.entity; import java.util.Date; /**用户信息*/ public class UserInfo { private String username; private Date expireTime; private String jwt; public UserInfo(String username, Date expireTime, String jwt) { this.username = username; this.expireTime = expireTime; this.jwt = jwt; } public UserInfo() { } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Date getExpireTime() { return expireTime; } public void setExpireTime(Date expireTime) { this.expireTime = expireTime; } public String getJwt() { return jwt; } public void setJwt(String jwt) { this.jwt = jwt; } }
4.2.3、全局过滤器
CORS跨域
package com.myproject.studentmis4.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @Configuration public class GlobalCorsConfig { @Bean public CorsFilter corsFilter() { //1. 添加 CORS配置信息 CorsConfiguration config = new CorsConfiguration(); //放行哪些原始域 config.addAllowedOrigin("http://localhost:8080/"); //是否发送 Cookie config.setAllowCredentials(true); //放行哪些请求方式 config.addAllowedMethod("*"); //放行哪些原始请求头部信息 config.addAllowedHeader("*"); //暴露哪些头部信息 config.addExposedHeader("*"); //2. 添加映射路径 UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource(); corsConfigurationSource.registerCorsConfiguration("/**",config); //3. 返回新的CorsFilter return new CorsFilter(corsConfigurationSource); } }
身份验证
package com.myproject.studentmis4.config; import cn.hutool.json.JSON; import cn.hutool.json.JSONConverter; import cn.hutool.json.JSONUtil; import com.myproject.studentmis4.util.JWTUtil; import com.myproject.studentmis4.util.R; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.web.filter.CorsFilter; import javax.servlet.*; import javax.servlet.annotation.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.swing.*; import java.io.IOException; import java.io.PrintWriter; @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 { HttpServletRequest httpServletRequest= (HttpServletRequest) request; HttpServletResponse httpServletResponse= (HttpServletResponse) response; httpServletResponse.setHeader("Access-Control-Allow-Origin", "http://localhost:8080"); 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"); String token=httpServletRequest.getHeader("token"); //如果存在令牌 if(token==null||token.equals("")){ renderJson(httpServletResponse, R.error(-1,"未授权")); }else{ try { JWTUtil.veryfiJWT(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"); try (PrintWriter writer = response.getWriter()){ writer.print(JSONUtil.toJsonStr(json)); } catch (IOException e) { e.printStackTrace(); } } }
4.2.4、控制器
登录控制器
package com.myproject.studentmis4.controller; import cn.hutool.core.lang.hash.Hash; import com.myproject.studentmis4.entity.User; import com.myproject.studentmis4.entity.UserInfo; import com.myproject.studentmis4.util.JWTUtil; import com.myproject.studentmis4.util.R; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api") public class LoginController { @PostMapping("/login") public R login(@RequestBody User user){ if(user.getUsername().equals("admin")&&user.getPassword().equals("123456")){ HashMap<String,Object> map=new HashMap<>(); map.put("username",user.getUsername()); return R.ok("登录成功") .put("username",user.getUsername()) .put("jwt", JWTUtil.creatJWT(map)); } else{ return R.error("登录失败!"); } } @GetMapping("/user/order") public R getOrders(){ return R.ok("请求成功").put("orders","这是当前用户的订单信息,从后台加载,模拟数据,需要权限才可以获取"); } @GetMapping("/user/address") public R getAddress(){ return R.ok("请求成功").put("address","这是当前用户的收货地址信息,从后台加载,模拟数据,需要权限才可以获取"); } }
4.2.5、Application设置
package com.myproject.studentmis4; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletComponentScan; @SpringBootApplication @ServletComponentScan public class Studentmis4Application { public static void main(String[] args) { SpringApplication.run(Studentmis4Application.class, args); } }
4.2.6、运行效果
未登录时:
登录后:
伪造假的jwt
五、Shiro
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
5.1、主要功能
三个核心组件:Subject,SecurityManager 和 Realms
1、Subject:
即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
2、SecurityManager:
它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
3、Realm:
Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。
5.2、基本功能点
Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web 支持,可以非常容易的集成到 Web 环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
5.3、特点
1、易于理解的 Java Security API
2、简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等)
3、对角色的简单的签权(访问控制),支持细粒度的签权
4、支持一级缓存,以提升应用程序的性能
5、内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境
6、异构客户端会话访问
7、非常简单的加密 API
8、不跟任何的框架或者容器捆绑,可以独立运行
六、Spring Security
6.1、Spring Security
Spring Security是一个灵活和强大的身份验证和访问控制框架,以确保基于Spring的Java Web应用程序的安全。
Spring Security是一个轻量级的安全框架,它确保基于Spring的应用程序提供身份验证和授权支持。它与Spring MVC有很好地集成,并配备了流行的安全算法实现捆绑在一起。
Spring Security 主要实现了Authentication(认证,解决who are you? ) 和 Access Control(访问控制,也就是what are you allowed to do?,也称为Authorization)。Spring Security在架构上将认证与授权分离,并提供了扩展点。“认证”是为用户建立一个其声明的角色的过程,这个角色可以一个用户、一个设备或者一个系统。“验证”指的是一个用户在你的应用中能够执行某个操作。在到达授权判断之前,角色已经在身份认证过程中建立了。
特点
Shiro能实现的,Spring Security 基本都能实现,依赖于Spring体系,但是好处是Spring全家桶的一员,集成上更加契合,在使用上,比Shiro略功能强大(但是一般Shiro够用)
6.2、区别
1、Shiro比Spring Security更容易使用,也就是实现上简单一些,同时基本的授权认证Shiro也基本够用
2、Spring Security社区支持度更高(但是安装Spring Security很难),Spring社区支持力度和更新维护上有优势,同时和Spring这一套的结合较好
3、Shiro 功能强大、且 简单、灵活。是Apache 下的项目比较可靠,且不跟任何的框架或者容器绑定,可以独立运行
个人理解
Shiro 首选 ,上手快 ,也足够用,自由度高,Spring Security中有的,Shiro也基本都有(项目没有使用Spring这一套,不用考虑,直接Shiro)
如果开发项目使用Spring这一套,用Spring Security可能更合适一些;虽然Spring Security 比较复杂,但与Spring 家族结合能力更强,是一个可以放心选择的框架结构
https://blog.csdn.net/MinggeQingchun/article/details/126414384
七、示例
八、视频
【Vue3 下 + Vuex + Pinia + TypeScript + Router】 https://www.bilibili.com/video/BV1C44y1X7Ud/?share_source=copy_web&vd_source=475a31f3c5d6353a782007cd4c638a8a
【Vue3 上 + Vuex + Pinia + TypeScript + Router】 https://www.bilibili.com/video/BV1at4y1F75D/?share_source=copy_web&vd_source=475a31f3c5d6353a782007cd4c638a8a