文章目录
1.会话技术
- 前因:HTTP是无状态协议,每次请求都是独立的,服务器不会自动保存任何前一次请求的上下文信息,无法满足需要“记住”用户状态的场景(如登录),需要通过会话技术(Cookie、Session、JWT)实现状态管理。
1.1 Cookie
cookie是客户端会话跟踪技术,它是存储在客户端浏览器的,我们使用 cookie 来跟踪会话,我们就可以在浏览器第一次发起请求来请求服务器的时候,我们在服务器端来设置一个cookie。
比如第一次请求了登录接口,登录接口执行完成之后,我们就可以设置一个cookie,在 cookie 当中我们就可以来存储用户相关的一些数据信息。比如我可以在 cookie 当中来存储当前登录用户的用户名,用户的ID。
服务器端在给客户端在响应数据的时候,会自动的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中,都会将浏览器本地所存储的 cookie 自动地携带到服务端。
刚才在介绍流程的时候,用了 3 个自动:
服务器会 自动 的将 cookie 响应给浏览器。
浏览器接收到响应回来的数据之后,会 自动 的将 cookie 存储在浏览器本地。
在后续的请求当中,浏览器会 自动 的将 cookie 携带到服务器端。
为什么这一切都是自动化进行的?
是因为 cookie 它是 HTP 协议当中所支持的技术,而各大浏览器厂商都支持了这一标准。在 HTTP 协议官方给我们提供了一个响应头和请求头:
- 响应头 Set-Cookie :设置Cookie数据的
- 请求头 Cookie:携带Cookie数据的
代码实现:
@Slf4j
@RestController
public class SessionController {
//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
return Result.success();
}
//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
}
}
return Result.success();
}
}
A. 访问c1接口,设置Cookie,http://localhost:8080/c1
我们可以看到,设置的cookie,通过响应头Set-Cookie响应给浏览器,并且浏览器会将Cookie,存储在浏览器端。
B. 访问c2接口 http://localhost:8080/c2,此时浏览器会自动的将Cookie携带到服务端,是通过请求头Cookie,携带的。
优缺点
- 优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的)。
- 缺点:
- 移动端APP(Android、IOS)中无法使用Cookie。
- 不安全,用户可以自己禁用Cookie。
- Cookie不能跨域。
跨域介绍:
- 现在的项目,大部分都是前后端分离的,前后端最终也会分开部署,前端部署在服务器 192.168.150.200 上,端口 80,后端部署在 192.168.150.100上,端口 8080
- 我们打开浏览器直接访问前端工程,访问url:http://192.168.150.200/login.html
- 然后在该页面发起请求到服务端,而服务端所在地址不再是localhost,而是服务器的IP地址192.168.150.100,假设访问接口地址为:http://192.168.150.100:8080/login
- 那此时就存在跨域操作了,因为我们是在 http://192.168.150.200/login.html 这个页面上访问了http://192.168.150.100:8080/login 接口
- 此时如果服务器设置了一个Cookie,这个Cookie是不能使用的,因为Cookie无法跨域
区分跨域的维度:
- 协议
- IP/协议
- 端口
只要上述的三个维度有任何一个维度不同,那就是跨域操作
举例:
http://192.168.150.200/login.html ----------> https://192.168.150.200/login [协议不同,跨域]
http://192.168.150.200/login.html ----------> http://192.168.150.100/login [IP不同,跨域]
http://192.168.150.200/login.html ----------> http://192.168.150.200:8080/login [端口不同,跨域]
http://192.168.150.200/login.html ----------> http://192.168.150.200/login [不跨域]
1.2 Session
Session是服务端会话技术,所以它是存储在服务器端的。而 Session 的底层其实就是基于我们刚才所介绍的 Cookie 来实现的。
创建Session:如果是第一次请求Session ,会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session 。而每一个会话对象Session ,它都有一个ID(示意图中Session后面括号中的1,就表示ID),我们称之为 Session 的ID。
响应Cookie (JSESSIONID):接下来,服务器端在给浏览器响应数据的时候,它会将 Session 的 ID 通过 Cookie 响应给浏览器。其实在响应头当中增加了一个 Set-Cookie 响应头。这个 Set-Cookie 响应头对应的值是不是cookie? cookie 的名字是固定的 JSESSIONID 代表的服务器端会话对象 Session 的 ID。浏览器会自动识别这个响应头,然后自动将Cookie存储在浏览器本地。
查找Session:接下来,在后续的每一次请求当中,都会将 Cookie 的数据获取出来,并且携带到服务端。接下来服务器拿到JSESSIONID这个 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就会从众多的 Session 当中来找到当前请求对应的会话对象Session。
代码实现:
@Slf4j @RestController public class SessionController { @GetMapping("/s1") public Result session1(HttpSession session){ log.info("HttpSession-s1: {}", session.hashCode()); session.setAttribute("loginUser", "tom"); //往session中存储数据 return Result.success(); } @GetMapping("/s2") public Result session2(HttpServletRequest request){ HttpSession session = request.getSession(); log.info("HttpSession-s2: {}", session.hashCode()); Object loginUser = session.getAttribute("loginUser"); //从session中获取数据 log.info("loginUser: {}", loginUser); return Result.success(loginUser); } }
优缺点
优点:Session是存储在服务端的,安全
缺点:
- 每个用户的登录信息都会保存到服务端器中,当用户增多将会增加服务端压力。
- 服务器集群环境下无法直接使用Session
- 对于非浏览器端包括移动端APP(Android、IOS)中无法使用Cookie
- 用户可以自己禁用Cookie,Cookie如果被截获也有安全风险
- Cookie不能跨域
PS:Session 底层是基于Cookie实现的会话跟踪,如果Cookie不可用,则该方案,也就失效了。所以大多数情况使用下述介绍的JWT进行用户认证。
1.3 JWT
1.3.1 JWT简介
定义:JWT(Json Web Token)通过数字签名的方式,以Json为载体,定义了一种简洁的、自包含的,用于在通信双方安全传输信息的技术。
工作流程:
- 用户登录:客户端使用用户名和密码请求登录,服务端收到请求,验证用户名和密码。
- 生成JWT:登录成功,服务端生成保护用户关键信息的JWT返回给前端。
- 保存JWT:客户端接收到JWT后将JWT存储起来,后续客户端每次向服务端请求资源时需要携带服务端签发的JWT,可以在cookie或者header中携带。退出登录时删除保存的JWT。
- 校验JWT:服务端统一拦截请求,校验JWT(有效性、时效性、方法签名等)。
- 解析JWT:后端解析出JWT中包含的用户信息,根据用户信息进行权限校验、数据处理等相关操作,返回结果给前端。
优势:
- 支持跨域访问,解决了cookie无法跨域的问题,跨域后不会存在信息丢失问题,适用于分布式微服务。
- 减轻服务端压力,JWT本身包含了登录用户的信息,服务端无需刻意存储JWT。
- 适用性广,当客户端是非浏览器平台时,cookie是不被支持的,而JWT支持大多数web应用。
- 安全性高,通过非对称加密算法和数字签名技术,防止JWT被篡改。
- JWT基于Json,方便解析和拓展,可以在令牌中轻松的添加自定义内容。
劣势:
- JWT不支持会话管理,一旦签发无法撤回或修改,也不能主动使令牌失效,除非到了过期时间。
- JWT中的信息可以在客户端解码,因此敏感信息不应该存储在JWT中。
1.3.2 JWT结构
JWT由
Header
(标头),PayLoad
(负载),Signature
(签名)三个部分组成,通过Base64编码方式分别转换成字符串,三个字符串之间使用.
的点来分割。JWTString=Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
Header(标头):包含令牌类型和签名算法,是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。
{ "alg": "HS256", "typ": "JWT" }
PayLoad(负载):有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择,除了默认字段外可以自定义私有字段(一般是包含用户信息的字段)。
iss:发行人 exp:到期时间 sub:主题 aud:用户 nbf:在此之前不可用 iat:发布时间 jti:JWT ID用于标识该JWT
{ "sub": "1234567890", "name": "Helen", "admin": true }
Signature(签名):防止JWT被篡改、确保安全性,需要使用Base64编码后的header和payload数据,通过指定的算法生成哈希。首先,需要指定一个密钥(secret)。该密钥仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名:
HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用.分隔,就构成整个JWT对象
JWT各个部分的作用:
在服务端接收到客户端发送过来的JWT token之后:
- header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据。
- signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secretKey对header、payload进行加密,比对加密后的数据和客户端发送过来的是否一致。注意secretKey只能保存在服务端,而且对于不同的加密算法其含义有所不同,一般对于MD5类型的摘要加密算法,secretKey实际上代表的是盐值。
JWT是如何将原始的JSON格式数据,转变为字符串的呢?
其实在生成JWT令牌时,会对JSON格式的数据进行一次编码:进行base64编码
Base64:是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码,那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号
需要注意的是Base64是编码方式,而不是加密方式。
到目前为止,jwt的签名算法有三种:
- HMAC【哈希消息验证码(对称)】:HS256/HS384/HS512
- RSASSA【RSA签名算法(非对称)】(RS256/RS384/RS512)
- ECDSA【椭圆曲线数据签名算法(非对称)】(ES256/ES384/ES512)
1.3.3 使用JWT
引入依赖:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
JWT工具类:
public class JwtUtil { /** * 生成jwt * 使用Hs256算法, 私匙使用固定秘钥,可将密钥存储在服务端 * * @param secretKey jwt秘钥 * @param ttlMillis jwt过期时间(毫秒) * @param claims 负载 * @return */ public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) { // 指定签名的时候使用的签名算法,也就是header那部分 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 生成JWT的时间 long expMillis = System.currentTimeMillis() + ttlMillis; Date exp = new Date(expMillis); // 设置jwt的body String jwt = Jwts.builder() // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的 .setClaims(claims) // 设置签名使用的签名算法和签名使用的秘钥 .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8)) // 设置过期时间 .setExpiration(exp) .compact(); return jwt; } /** * Token解密 * * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则signature就可以被伪造, 如果对接多个客户端建议改造成多个 * @param token 加密后的token * @return */ public static Claims parseJWT(String secretKey, String token) { // 得到DefaultJwtParser Claims claims = Jwts.parser() // 设置签名的秘钥 .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)) // 设置需要解析的jwt .parseClaimsJws(token) .getBody(); return claims; } }
2.拦截技术
如果需要在服务端统一拦截指定请求从而实现一些特殊的功能,就需要用到拦截技术,其中主要包含过滤器(Filter)和拦截器(Interceptor)。
常见场景:登录校验、统一编码处理、敏感字符处理等。
拦截器和过滤器之间的区别:
- 归属不同:Filter属于Servlet技术,Interceptor属于SpringMVC技术。
- 拦截范围不同:Filter对所有访问进行增强,Interceptor仅针对SpringMVC的访问进行增强。
- 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
2.1 过滤器(Filter)
Filter是 JavaWeb三大组件(Servlet、Filter、Listener)之一。
2.1.1 快速上手
下面我们通过Filter快速入门程序掌握过滤器的基本使用操作:
定义过滤器:定义一个类,实现 Filter 接口,并重写其所有方法,并在Filter类上加
@WebFilter
注解,同时配置拦截资源的路径。@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /*) public class DemoFilter implements Filter { //初始化方法, 只调用一次 @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("DemoFilter.init初始化方法执行了"); } //拦截到请求之后调用, 调用多次 @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("DemoFilter执行放行前逻辑"); chain.doFilter(request,response); //放行操作,如果不放行无法访问后续资源 System.out.println("DemoFilter执行放行后逻辑"); } //销毁方法, 只调用一次 @Override public void destroy() { System.out.println("DemoFilter.destroy销毁方法执行了"); } }
SpringBoot启动类上加上
@ServletComponentScan
开启Servlet组件支持。@ServletComponentScan @SpringBootApplication public class TliasWebManagementApplication { public static void main(String[] args) { SpringApplication.run(TliasWebManagementApplication.class, args); } }
登录校验过滤器案例:
@Slf4j @WebFilter(urlPatterns = "/*") //拦截所有请求 public class LoginCheckFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { //前置:强制转换为http协议的请求对象、响应对象 (转换原因:要使用子类中特有方法) HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; //1.获取请求url String url = request.getRequestURL().toString(); log.info("请求路径:{}", url); //请求路径:http://localhost:8080/login //2.判断请求url中是否包含login,如果包含,说明是登录操作,放行 if(url.contains("/login")){ chain.doFilter(request, response);//放行请求 return;//结束当前方法的执行 } //3.获取请求头中的令牌(token) String token = request.getHeader("token"); log.info("从请求头中获取的令牌:{}",token); //4.判断令牌是否存在,如果不存在,返回错误结果(未登录) if(!StringUtils.hasLength(token)){ log.info("Token不存在"); Result responseResult = Result.error("NOT_LOGIN"); //把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类) String json = JSONObject.toJSONString(responseResult); response.setContentType("application/json;charset=utf-8"); //响应 response.getWriter().write(json); return; } //5.解析token,如果解析失败,返回错误结果(未登录) try { JwtUtils.parseJWT(token); }catch (Exception e){ log.info("令牌解析失败!"); Result responseResult = Result.error("NOT_LOGIN"); //把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类) String json = JSONObject.toJSONString(responseResult); response.setContentType("application/json;charset=utf-8"); //响应 response.getWriter().write(json); return; } //6.放行 chain.doFilter(request, response); } }
2.1.2 执行流程
首先我们先来看下过滤器的执行流程:
过滤器当中我们拦截到了请求之后,如果希望继续访问后面的web资源,就要执行放行操作,放行就是调用 FilterChain对象当中的doFilter()方法,在调用doFilter()这个方法之前所编写的代码属于放行之前的逻辑。
在放行后访问完 web 资源之后还会回到过滤器当中,回到过滤器之后如有需求还可以执行放行之后的逻辑,放行之后的逻辑我们写在doFilter()这行代码之后。
2.1.3 拦截路径
Filter可以根据需求,配置不同的拦截资源路径:
拦截路径 | urlPatterns值 | 含义 |
---|---|---|
拦截具体路径 | /login | 只有访问 /login 路径时,才会被拦截 |
目录拦截 | /emps/* | 访问/emps下的所有资源,都会被拦截 |
拦截所有 | /* | 访问所有资源,都会被拦截 |
2.1.4 过滤器链
在一个web应用程序当中,可以配置多个过滤器,多个过滤器就形成了一个过滤器链。以注解方式配置的Filter过滤器,它的执行优先级是按时过滤器类名的自动排序确定的,类名排名越靠前,优先级越高。
2.2 拦截器(Interceptor)
- 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行,在指定方法调用前后,根据业务需要执行预先设定的代码。
2.2.1 快速上手
下面我们通过快速入门程序,来学习下拦截器的基本使用。拦截器的使用步骤和过滤器类似,也分为两步:
定义拦截器:实现
HandlerInterceptor
接口,并重写其所有方法。//自定义拦截器 @Component public class LoginCheckInterceptor implements HandlerInterceptor { //目标资源方法执行前执行。 返回true:放行 返回false:不放行 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("LoginCheckInterceptor.preHandle .... "); return true; //true表示放行 } //目标资源方法执行后执行 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("LoginCheckInterceptor.postHandle ... "); } //视图渲染完毕后执行,最后执行 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("LoginCheckInterceptor.afterCompletion .... "); } }
注册配置拦截器:实现
WebMvcConfigurer
接口,并重写addInterceptors
方法。@Configuration public class WebConfig implements WebMvcConfigurer { //自定义的拦截器对象 @Autowired private LoginCheckInterceptor loginCheckInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //注册自定义拦截器对象 registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求) } }
2.2.2 执行流程
介绍完拦截路径的配置之后,接下来我们再来介绍拦截器的执行流程。通过执行流程,大家就能够清晰的知道过滤器与拦截器的执行时机。
当我们打开浏览器来访问部署在web服务器当中的web应用时,此时我们所定义的过滤器会拦截到这次请求。拦截到这次请求之后,它会先执行放行前的逻辑,然后再执行放行操作。而由于我们当前是基于springboot开发的,所以放行之后是进入到了spring的环境当中,也就是要来访问我们所定义的controller当中的接口方法。
Tomcat并不识别所编写的Controller程序,但是它识别Servlet程序,所以在Spring的Web环境中提供了一个非常核心的Servlet:DispatcherServlet(前端控制器),所有请求都会先进行到DispatcherServlet,再将请求转给Controller。
当我们定义了拦截器后,会在执行Controller的方法之前,请求被拦截器拦截住。执行
preHandle()
方法,这个方法执行完成后需要返回一个布尔类型的值,如果返回true,就表示放行本次操作,才会继续访问controller中的方法;如果返回false,则不会放行(controller中的方法也不会执行)。在controller当中的方法执行完毕之后,再回过来执行
postHandle()
这个方法以及afterCompletion()
方法,然后再返回给DispatcherServlet,最终再来执行过滤器当中放行后的这一部分逻辑的逻辑。执行完毕之后,最终给浏览器响应数据。
2.2.3 拦截路径
通过
addPathPatterns("要拦截路径")
方法,就可以指定要拦截哪些资源,调用excludePathPatterns("不拦截路径")
方法,指定哪些资源不需要拦截。在拦截器中除了可以设置
/**
拦截所有资源外,还有一些常见拦截路径设置:拦截路径 含义 举例 /* 一级路径 能匹配/depts,/emps,/login,不能匹配 /depts/1 /** 任意级路径 能匹配/depts,/depts/1,/depts/1/2 /depts/* /depts下的一级路径 能匹配/depts/1,不能匹配/depts/1/2,/depts /depts/** /depts下的任意级路径 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1
2.2.4 拦截器链
在一个Spring应用程序当中,可以配置多个拦截器,多个拦截器就形成了一个拦截器链,拦截器链的运行顺序参照拦截器添加顺序为准。
preHandle:与配置顺序相同,必定运行
postHandle:与配置顺序相反,可能不运行
afterCompletion:与配置顺序相反,可能不运行
3.登入认证(案例)
在实际的SpringBoot项目中,一般我们可以用如下流程做登录:
在登录验证通过后,给用户生成一个对应的随机token(注意这个token不是指jwt,可以用uuid等算法生成),然后将这个token作为key的一部分,用户信息作为value存入Redis,并设置过期时间,这个过期时间就是登录失效的时间。
将第1步中生成的随机token作为JWT的payload生成JWT字符串返回给前端。
前端之后每次请求都在请求头中的Authorization字段中携带JWT字符串。
后端定义一个拦截器,每次收到前端请求时,都先从请求头中的Authorization字段中取出JWT字符串并进行验证,验证通过后解析出payload中的随机token,然后再用这个随机token得到key,从Redis中获取用户信息,如果能获取到就说明用户已经登录。
下述代码未使用redis暂存用户信息,只做简单示范:
@Component @Slf4j public class JwtTokenPatientInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断当前拦截到的是Controller的方法还是其他资源 if (!(handler instanceof HandlerMethod)) { //当前拦截到的不是动态方法,直接放行 return true; } // 1.从请求头中获取令牌 String token = request.getHeader(jwtProperties.getUserTokenName()); // 2.判断令牌是否存在,如果不存在,返回错误结果(未登录) if (StringUtils.isNull(token)) { log.info("请求头token为空,返回未登录的信息"); response.setStatus(401); return false; } // 3.校验令牌 try { log.info("jwt校验:{}", token); Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token); Long patientId = Long.valueOf(claims.get(JwtClaimsConstant.PATIENT_ID).toString()); log.info("当前用户的id:" + patientId); BaseContext.setCurrentId(patientId); //3、通过,放行 return true; } catch (Exception ex) { //4、不通过,响应401状态码 response.setStatus(401); return false; } } }
@Component @ConfigurationProperties(prefix = "jwt") @Data public class JwtProperties { private String userSecretKey; //密钥 private long userTtl; //过期时间 private String userTokenName; //token名称 }
在实际开发中需要用下列手段来增加JWT的安全性:
- 因为JWT是在请求头中传递的,所以为了避免网络劫持,推荐使用HTTPS来传输,更加安全。
- JWT的哈希签名的密钥是存放在服务端的,所以只要服务器不被攻破,理论上JWT是安全的,因此要保证服务器的安全。
- JWT可以使用暴力穷举来破解,所以为了应对这种破解方式,可以定期更换服务端的哈希签名密钥(相当于盐值)。这样可以保证等破解结果出来了,你的密钥也已经换了。
参考博客