JWT技术分享(1)
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门,即可获取!
iss:jwt签发者
sub:jwt所面向的用户
aud:接收jwt的一方
exp:jwt的过期时间,这个过期时间必须大于签发时间
nbf:定义在什么时间之前,该jwt都是不可用的
iat:jwt的签发时间
jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
公共的声明:
公共的声明可以添加任何的信息,一般添加用户的相关信息或其它业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密;
私有的声明
私有的声明是提供者和消费者功能定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为名文信息。
定义一个payload
{ “sub”: “1234567890”, “name”: “John Doe”, “admin”: true }
然后将其base64加密,得到jwt的一部分
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
Signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header(base64后的)
payload(base64后的)
secred
这个部分需要base64加密后的header和base64加密后的payload使用“.”连接组成的字符串,然后通过header中声明的加密方式进行加secret组合加密,然后就构成了jwt的第三部分
var encodedString = base64UrlEncode(header) + ‘.’ + base64UrlEncode(payload); var signature = HMACSHA256(encodedString, ‘secret’); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用“.”连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发也是在服务端的,secret就是用来进行jwt的签发和jwt的验证,所以它就是你服务端的私钥,在任何场景都不应该流露出去,一旦客户端得知这个secret,那就意味着客户端可以自我签发jwt了
应用
一般是在请求头里加入Authorization,并加上Bearer标注:
fetch(‘api/user/1’, {
headers: { ‘Authorization’: 'Bearer ’ + token
}
})
四、有关JWT值得讨论的一些事
JWT有什么不好的地方?
不好的地方就是JWT等于说是泼出去的水,发出去的令牌,不可更改,只要有效期内就可以永使用
JWT安全性如何?
这也是JWT最重要的一点,它的出现就是为了免登陆或者权限控制,所以它的安全性至关重要。所以它虽然是一直加密的,但是安全性总是建立在相对性之上的。所以只能说一定程度上是安全的。
JWT如何存放?
JWT不需要存放,或者说存放在客户端那里。服务端只需要有解密算法就可以判断当前的JWT是不是有效的。
JWT如何验证?
当接收方接收到一个JWT的时候,首先要对这个JWT的完整性进行验证,这个就是签名认证。它验证的方法其实很简单,只要把header使用base64url解码以后就知道签发者用什么算法做的签名,然后用这个算法对header和payload加密一下,再比较这个签名是否和JWT本身包含的signature是否完全相同,只要不同就可以认为这个JWT是被篡改过的。所以接收方生成签名的时候必须使用跟JWT发送方相同的密钥。
再次就是验证payload里的部分,比如是否过期,还有自定义的信息是否相同,加上已经前面验证的signature,这样就保证了这个签名的有效性
互斥登录/修改密码后登录的问题
场景:用户在多个设备或者其它浏览器/IP登录以后或者修改密码以后,那其它的设备就需要自动退出登录
用户修改密码之后签发一个新的token来覆盖之前的。但是之前的token只要在失效之前仍然是可以验证并且登录的
用户手动退出的时候也是需要token失效的,这就需要退出的时候直接清除或者覆盖客户端保存的token。同上,之前的token仍然有效,所以token的时间不能太长
设置黑名单,在签发新token的时候把之前未过期的token加入黑名单使之失效。但是这样就需要储存token,而jwt的优点就是不需要储存空间。
续签
场景:需要用户长期登录的应用,比如用你开发需要用户保持登录的应用,在使用token的时候保证用户体验不需要频繁登录操作,又要保证用户在一段时间之后登录过期重新登录,主要是一个用户体验和用户信息安全的问题。这个时候又要求token不能过长,只能通过续签来保持登录状态,又或者说用户登录的时候你需要获取用户在社交网站的信息,就需要获取社交网站授权的token,这也需要续签,不过这是向社交网站续签。
何时续签:
1.每次请求刷新 每次请求直接覆盖签发新的token
2.记录时间定时刷新 设置一个定时器,记录签发时间,然后到过期的时候签发新的
3.临期时刷新 访问的时候对比时间,如果即将过期就立即刷新。
4.过期后续签,这样设置过期时间的意义就在于如果token被盗取后黑客登录也需要续签,这样用户再次登录客户端就会发现用户登录异常。而且每次获取的token都是新的,旧的只能>获取一次续签的机会,也就说如果用户续签以后之前的token就不能再续签了。
5.jwt-autorefresh(JWT自动刷新)(有限制条件,在客户端定时刷新)。
JWT的优点
因为json的通用性,所以JWT是可以跨语言支持的,像C#,JavaScript,NodeJS,PHP等许多语言都可以使用
因为由了payload部分,所以JWT可以在自身存储一些其它业务逻辑所必要的非敏感信息
便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的
它不需要在服务端保存会话信息,所以它易于应用的扩展
JWT 安全相关
不应该在jwt的payload部分存储敏感信息,因为该部分是客户端可解密的部分
保护好secret私钥。该私钥非常重要
如果可以,请使用https协议
JWT生成token的安全性分析
从JWT生成的token组成上来看,要想避免token被伪造,主要就得看签名部分了,而签名部分又有三部分组成,其中头部和载荷的base64编码,几乎是透明的,毫无安全性可言,那么最终守护token安全的重担就落在了加入的盐上面了!试想:如果生成token所用的盐与解析token时加入的盐是一样的。岂不是类似于中国人民银行把人民币防伪技术公开了?大家可以用这个盐来解析token,就能用来伪造token。这时,我们就需要对盐采用非对称加密的方式进行加密,以达到生成token与校验token方所用的盐不一致的安全效果!
非对称加密RSA介绍
基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端私钥加密,持有私钥或公钥才可以解密公钥加密,持有私钥才可解密 优点:安全,难以破解 缺点:算法比较耗时,为了安全,可以接受 历史:三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA。
JWT流程图
五、JWT认证的代码实现
5.1 创建工程
环境:idea、maven、jdk8、springboot2.x
maven结构图
pom依赖
xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd”> 4.0.0 org.springframework.boot spring-boot-starter-parent 2.5.3 com.uncle jwt-demo 0.0.1-SNAPSHOT jwt-demo Demo project for Spring Boot com.auth0 java-jwt 3.4.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test com.alibaba fastjson 1.2.48 org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok 5.2 自定义注解 package com.uncle.jwtdemo.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** @program: jwt-demo @description: 跳过验证 @author: 步尔斯特 @create: 2021-08-07 16:18 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface PassToken { boolean required() default true; } package com.uncle.jwtdemo.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** @program: jwt-demo @description: 需要登录才可以进行操作 @author: 步尔斯特 @create: 2021-08-07 16:19 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface UserLoginToken { boolean required() default true; } 5.3 用户实体类 package com.uncle.jwtdemo.bean; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** @program: jwt-demo @description: @author: 步尔斯特 @create: 2021-08-07 16:21 */ @Data @AllArgsConstructor @NoArgsConstructor public class User { String Id; String username; String password; //获取token的方法 public String getToken(User user) { String token=“”; token= JWT.create().withAudience(user.getId()) .sign(Algorithm.HMAC256(user.getPassword())); return token; } } 5.4 创建拦截器 package com.uncle.jwtdemo.config; import com.uncle.jwtdemo.interceptor.AuthenticationInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** @program: jwt-demo @description: @author: 步尔斯特 @create: 2021-08-07 16:25 */ @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns(“/**”); } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); } } 5.5 认证的拦截器 package com.uncle.jwtdemo.interceptor; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.JWTVerificationException; import com.uncle.jwtdemo.annotations.PassToken; import com.uncle.jwtdemo.annotations.UserLoginToken; import com.uncle.jwtdemo.bean.User; import com.uncle.jwtdemo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; /** @program: jwt-demo @description: @author: 步尔斯特 @create: 2021-08-07 16:22 */ public class AuthenticationInterceptor implements HandlerInterceptor { @Autowired UserService userService; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { String token = httpServletRequest.getHeader(“token”);// 从 http 请求头中取出 token // 如果不是映射到方法直接通过 if (!(object instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) object; Method method = handlerMethod.getMethod(); //检查是否有passtoken注释,有则跳过认证 if (method.isAnnotationPresent(PassToken.class)) { PassToken passToken = method.getAnnotation(PassToken.class); if (passToken.required()) { return true; } } //检查有没有需要用户权限的注解 if (method.isAnnotationPresent(UserLoginToken.class)) { UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class); if (userLoginToken.required()) { // 执行认证 if (token == null) { throw new RuntimeException(“无token,请重新登录”); } // 获取 token 中的 user id String userId; try { userId = JWT.decode(token).getAudience().get(0); } catch (JWTDecodeException j) { throw new RuntimeException(“401”); } User user = userService.findUserById(userId); if (user == null) { throw new RuntimeException(“用户不存在,请重新登录”); } // 验证 token JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build(); try { jwtVerifier.verify(token); } catch (JWTVerificationException e) { throw new RuntimeException(“401”); } return true; } } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } } 5.6 用户业务 package com.uncle.jwtdemo.service; import com.uncle.jwtdemo.bean.User; /** @program: jwt-demo @description: @author: 步尔斯特 @create: 2021-08-07 16:30 */ public interface UserService { User findByUsername(User user); User findUserById(String id); } package com.uncle.jwtdemo.service.impl; import com.uncle.jwtdemo.bean.User; import com.uncle.jwtdemo.service.UserService; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; /** @program: jwt-demo @description: @author: 步尔斯特 @create: 2021-08-07 16:48 */ @Service public class UserServiceImpl implements UserService { private static Map @Override public User findByUsername(User user) { return userMap.get(user.getUsername()); } @Override public User findUserById(String id) { return userMap.get(id); } static { userMap.put(“1”, new User(“1”, “zhangsan”, “123”)); userMap.put(“zhangsan”,new User(“1”,“zhangsan”,“123”)); } } 5.7 token业务 package com.uncle.jwtdemo.service; import com.uncle.jwtdemo.bean.User; /** @program: jwt-demo @description: @author: 步尔斯特 @create: 2021-08-07 16:37 */ public interface TokenService { String getToken(User userForBase); } package com.uncle.jwtdemo.service.impl; import com.uncle.jwtdemo.bean.User; import com.uncle.jwtdemo.service.TokenService; import org.springframework.stereotype.Service; /** @program: jwt-demo @description: @author: 步尔斯特 @create: 2021-08-07 17:26 */ @Service public class TokenServiceImpl implements TokenService { @Override public String getToken(User userForBase) { return userForBase.getToken(userForBase); } } 5.8 测试接口 总结 其他的内容都可以按照路线图里面整理出来的知识点逐一去熟悉,学习,消化,不建议你去看书学习,最好是多看一些视频,把不懂地方反复看,学习了一节视频内容第二天一定要去复习,并总结成思维导图,形成树状知识网络结构,方便日后复习。 这里还有一份很不错的《Java基础核心总结笔记》,特意跟大家分享出来 目录: 部分内容截图: 《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门,即可获取! jwt-demo @description: @author: 步尔斯特 @create: 2021-08-07 16:37 */ public interface TokenService { String getToken(User userForBase); } package com.uncle.jwtdemo.service.impl; import com.uncle.jwtdemo.bean.User; import com.uncle.jwtdemo.service.TokenService; import org.springframework.stereotype.Service; /** @program: jwt-demo @description: @author: 步尔斯特 @create: 2021-08-07 17:26 */ @Service public class TokenServiceImpl implements TokenService { @Override public String getToken(User userForBase) { return userForBase.getToken(userForBase); } } 5.8 测试接口 总结 其他的内容都可以按照路线图里面整理出来的知识点逐一去熟悉,学习,消化,不建议你去看书学习,最好是多看一些视频,把不懂地方反复看,学习了一节视频内容第二天一定要去复习,并总结成思维导图,形成树状知识网络结构,方便日后复习。 这里还有一份很不错的《Java基础核心总结笔记》,特意跟大家分享出来 目录: [外链图片转存中…(img-FNimPxeo-1714699910359)] 部分内容截图: [外链图片转存中…(img-NbXGvuXx-1714699910360)] [外链图片转存中…(img-9mgYipHr-1714699910360)] 《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门,即可获取!