当前位置: 首页 > news >正文

设计网站考虑哪些因素wordpress关于我们插件

设计网站考虑哪些因素,wordpress关于我们插件,去年做那个网站致富,京东网上商城首页开放标准#xff1a;JSON Web Token 前言基本使用整合Shiro登录自定义JWT认证过滤器配置Config自定义凭证匹配规则接口验证权限控制禁用session缓存的使用登录退出单用户登录Token刷新双Token方案单Token方案 前言 JSON Web Token #xff08;JWT#xff09; 是一种开放标准… 开放标准JSON Web Token 前言基本使用整合Shiro登录自定义JWT认证过滤器配置Config自定义凭证匹配规则接口验证权限控制禁用session缓存的使用登录退出单用户登录Token刷新双Token方案单Token方案 前言 JSON Web Token JWT 是一种开放标准 RFC 7519它定义了一种紧凑且自包含的方式用于将信息作为 JSON 对象在各方之间安全地传输。此信息是经过数字签名的因此可以验证和信任。可以使用密钥使用 HMAC 算法或使用 RSA 或 ECDSA 的公钥/私钥对对 JWT 进行签名。 以下是 JSON Web Token 有用的一些情况 授权这是使用 JWT 的最常见场景。用户登录后每个后续请求都将包含 JWT允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能因为它的开销小并且能够轻松地跨不同域使用。信息交换JSON Web Token 是在各方之间安全地传输信息的好方法。由于 JWT 可以签名例如使用公钥/私钥对因此您可以确保发件人是他们所声称的身份。此外由于签名是使用标头和有效负载计算的因此您还可以验证内容是否未被篡改。 JSON Web Token由三个部分组成由点 . 分隔它们是 Header 页眉标头通常由两部分组成令牌的类型JWT和签名算法HMAC SHA256 或 RSA。 {alg: HS256,typ: JWT }用Base64对这个JSON编码就得到JWT的第一部分。 Payload 有效载荷令牌的第二部分是有效负载其中包含声明。声明是关于实体通常是用户和其他数据的声明。 有三种类型的声明registered public 和 private。 Registered claims这些是一组预定义的声明不是强制性的但建议使用以提供一组有用的、可互操作的声明。比如iss签发者、exp过期时间、sub主题、aud接收者、iat签发时间、nbf在此之前不可用等。 Public claims这些声明可以由用户随意定义。但不建议添加敏感信息因为该部分在客户端可解密。 Private claims提供者和消费者所共同定义的声明一般不建议存放敏感信息 一个示例有效Payload如下并不需要三个声明都设置 {sub: 1234567890,name: John Doe,admin: true }对Payload进行Base64编码就得到JWT的第二部分。 不要将机密信息放在 JWT 的 payload 或 header 元素中除非它们是加密的。 Signature 签名要创建签名部分您必须获取编码的Header、编码的Payload、密钥、标头中指定的算法并对其进行签名。 例如如果您想使用 HMAC SHA256 算法将按以下方式创建签名 HMACSHA256(base64UrlEncode(header) . base64UrlEncode(payload),secret)Header页眉eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Payload 有效载荷eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ通过HMAC SHA256 算法得到SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c最后我们将上述的 3 个部分的字符串通过 “.” 进行拼接得到完整JWT。 每当用户想要访问受保护的路由时它都应该发送 JWT通常在 Authorization 标头中使用 Bearer 模式。因此标头的内容应如下所示。 Authorization: Bearer token 在某些情况下这可以是无状态授权机制。服务器的受保护路由将检查 Authorization 标头中的有效 JWT如果存在将允许用户访问受保护的资源。跨域资源共享 CORS 不会成为问题因为它不使用 Cookie。 请注意如果您通过 HTTP 标头发送 JWT 令牌则应尽量防止它们变得太大。某些服务器不接受超过 8 KB 的标头。 下图显示了如何获取 JWT 并用于访问 API 或资源 应用程序或客户端向授权服务器请求授权。这是通过不同的授权流之一执行的。例如典型的符合 OpenID Connect 的 Web 应用程序将使用授权代码流通过 /oauth/authorize 终端节点。授予授权后授权服务器将向应用程序返回访问令牌。应用程序使用访问令牌访问受保护的资源如 API。 JSON 解析器在大多数编程语言中都很常见因为它们直接映射到对象。相反XML 没有自然的文档到对象的映射。这使得使用 JWT 比使用 SAML 断言更容易。 关于使用情况JWT 用于 Internet 规模。这突出了在多个平台尤其是移动平台上对 JSON Web Token进行客户端处理的便利性。 基本使用 引入依赖的方式有很多种 使用jjwt库 dependencygroupIdio.jsonwebtoken/groupIdartifactIdjjwt/artifactIdversion0.12.6/version /dependency该依赖包含了 jjwt-api、jjwt-impl 和 jjwt-jackson 三个模块的所有功能。 使用java-jwt库 dependencygroupIdcom.auth0/groupIdartifactIdjava-jwt/artifactIdversion4.4.0/version /dependency根据具体的项目需求和偏好进行选择官网上好像是auth0。 创建 JWT 要创建一个 JWT你需要使用 JWT.create() 方法然后添加必要的声明claims最后使用你的密钥和算法进行签名。示例代码如下 public class Test {public static void main(String[] args) {// 创建 JWTString token JWT.create() // .withHeader(map) 自定义Header可以传map或json.withIssuer(auth0) // 发行人.withSubject(1234567890) // 主题.withAudience(app_audience) // 观众.withIssuedAt(new Date()) // 发行时间.withExpiresAt(new Date(System.currentTimeMillis() 3600 * 1000)) // 过期时间1小时后 // .withPayload(map) 自定义payload可以传map或json // .withClaim(test, test) 自定义payload指定name和value.sign(Algorithm.HMAC256(123345)); // 使用 HMAC256 算法和密钥进行签名默认用该参数的加密类型当作HeaderSystem.out.println(token);/** Output* eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9* .eyJpc3MiOiJhdXRoMCIsInN1YiI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJhcHBfYXVkaWVuY2UiLCJpYXQiOjE3MzYxMjc0ODksImV4cCI6MTczNjEzMTA4OX0* .O4VD5DOn77tzPUZabPhPhkFKW9vS31dTpTAOPmkRv2o*/} }然后去官网可以查看解密的数据如图所示 验证 JWT 验证一个 JWT我们需要创建一个 JWTVerifier 实例该实例定义了验证 JWT 时所需的条件如算法和密钥、发行人、观众等。然后我们使用 verify() 方法对 JWT 进行验证并返回一个 DecodedJWT 实例该实例包含了 JWT 中的所有声明。 public class Test {public static void main(String[] args) {// 验证 JWTJWTVerifier verifier JWT.require(Algorithm.HMAC256(123456)).build(); // 可重用验证器实例DecodedJWT jwt verifier.verify(eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 .eyJpc3MiOiJhdXRoMCIsInN1YiI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJhcHBfYXVkaWVuY2UiLCJpYXQiOjE3MzYxMjc0ODksImV4cCI6MTczNjEzMTA4OX0 .O4VD5DOn77tzPUZabPhPhkFKW9vS31dTpTAOPmkRv2o);System.out.println(Verified Token: jwt);System.out.println(Verified Token: jwt.getId());System.out.println(Verified Token: jwt.getIssuer());/** Output* Verified Token: com.auth0.jwt.JWTDecoder2aece37d* Verified Token: null* Verified Token: auth0*/} }如果验证过程中出现密钥不匹配或者token过期都会抛出异常如图所示 然后整理成Util工具类方便调用示例代码如下 public class JWTUtil {// 过期时间一天private static final long EXPIRE_TIME 24*60*60*1000;/*** 生成token签名** param username 用户名* param secret 密码* return token字符串*/public static String sign(String username, String secret) {String token JWT.create().withClaim(username, username).withIssuedAt(new Date()) // 发行时间.withExpiresAt(new Date(System.currentTimeMillis() EXPIRE_TIME)).sign(Algorithm.HMAC256(secret));return token;}/*** 校验token是否有效** param token 生成token* param username 用户名* param secret 密码* return*/public static boolean verify(String token, String username, String secret) {try {// 保证荷载参数一致JWTVerifier build JWT.require(Algorithm.HMAC256(secret)).withClaim(username, username).build();DecodedJWT decodedJWT build.verify(token);return true;} catch (Exception e) {e.printStackTrace(); // 方便查看错误信息return false;}}/*** 获取token种的用户名过期了也可以获取* param token* return*/public static String getUsername(String token){try {return JWT.decode(token).getClaim(username).asString();} catch (Exception e) {return null;}}public static void main(String[] args) {String admin sign(admin, 123345);System.out.println(verify(admin,admin,123345));System.out.println(getUsername(admin));} }有很多的案例在生成Token时secret参数是固定密钥。 整合Shiro 将 Shiro 与 JWT 整合可以实现无状态认证和授权适用于分布式系统和微服务架构。 登录 以登录为切入点创建一个登录接口用于生成JWT并返回给客户端。 一般使用UsernamePasswordToken类作为登录参数我们需要创建一个类似的JWT类实现AuthenticationToken接口即可示例代码如下 public class JWTToken implements AuthenticationToken {private String token;public JWTToken(String token) {this.token token;}Overridepublic Object getPrincipal() {return token;}Overridepublic Object getCredentials() {return token;} }建立一个用于授权时保存信息类方便访问当前登录的用户信息示例代码如下 public class UserPrincipal implements Serializable {private User user;private String token;public UserPrincipal(User user, String token) {this.user user;this.token token;}// getter and setter ...... }然后在登陆时校验用户名和密码后生成对应的Token使用JWT Token作为登录参数示例代码如下 Controller RequestMapping(value /user) public class UserController {Autowiredprivate UserService userService;PostMapping(/login)public ResponseEntity login(RequestParam(username) String username, RequestParam(password) String password) {// 校验用户User user userService.getOne(new QueryWrapperUser().ge(username, username));if (user null) {return ResponseEntity.ok(用户不存在);}SimpleHash hash new SimpleHash(MD5, password, username);if (!hash.toHex().equals(user.getPassword())) {return ResponseEntity.ok(账号或密码错误);}// 生成tokenString sign JWTUtil.sign(username, hash.toHex());JWTToken jwtToken new JWTToken(sign);// 对用户信息进行身份认证Subject subject SecurityUtils.getSubject();try {subject.login(jwtToken);} catch (AuthenticationException e) {e.printStackTrace();}// 返回响应参数response.setHeader(Authorization, sign);return ResponseEntity.ok(sign);}GetMapping(/main)public ResponseEntity main() {UserPrincipal userPrincipal (UserPrincipal) SecurityUtils.getSubject().getPrincipal();User user userPrincipal.getUser();return ResponseEntity.ok(成功);} }Realm中进行授权操作授权时需要在查询一次用户信息进行保存示例代码如下 Component public class UserRealm extends AuthorizingRealm {Autowiredprivate UserService userService;Autowiredprivate RoleService roleService;Autowiredprivate PermissionService permissionService;Overridepublic boolean supports(AuthenticationToken token) {return token instanceof JWTToken;}Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {JWTToken token (JWTToken) authenticationToken;String username JWTUtil.getUsername((String) token.getPrincipal());if (username null) {throw new UnknownAccountException(账号不存在);}User user userService.getOne(new QueryWrapperUser().ge(username, username));if (user null) {throw new UnknownAccountException(账号不存在);}// 密码验证使用tokenSimpleAuthenticationInfo simpleAuthenticationInfo new SimpleAuthenticationInfo(new UserPrincipal(user, (String) token.getPrincipal()), user.getPassword(), ByteSource.Util.bytes(username), getName());return simpleAuthenticationInfo;}Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {UserPrincipal userPrincipal (UserPrincipal) principalCollection.getPrimaryPrincipal();User user userPrincipal.getUser();ListRole roleList roleService.getByUserId(user.getId());SimpleAuthorizationInfo simpleAuthorizationInfo new SimpleAuthorizationInfo();roleList.forEach(item -{simpleAuthorizationInfo.addRole(item.getName());});ListInteger roleIds roleList.stream().map(Role::getId).collect(Collectors.toList());ListPermission permissions permissionService.listByIds(roleIds);permissions.forEach(item-{simpleAuthorizationInfo.addStringPermission(item.getName());});return simpleAuthorizationInfo;} }1你会遇到第一个错误类型转换错误JWTToken无法匹配AuthenticationToken类型这是因为默认匹配为UsernamePasswordToken错误如图所示 解决该问题的方法有两种第一种就是Config文件中调用setAuthenticationTokenClass()方法指定匹配类示例代码如下 Configuration Component public class ShiroConfig {Beanpublic Realm realm() {UserRealm userRealm new UserRealm();userRealm.setCredentialsMatcher(hashedCredentialsMatcher()); // 为realm设置指定算法userRealm.setAuthenticationTokenClass(JWTToken.class);return userRealm;} }第二种方法就是再Realm中重写匹配规则示例代码如下 public class UserRealm extends AuthorizingRealm {Overridepublic boolean supports(AuthenticationToken token) {return token instanceof JWTToken;}// 省略部分代码... ... }2第二个错误如果指定了加密方式就会报 Odd number of characters.错误如图所示 因为你的密码使用Token传输通过指定的加密方式再给密码加密时不符合规则比如加密方式为SHA-256如图所示 如果设置了指定密码加密解决办法把Config中setCredentialsMatcher()方法设置的加密匹配规则删除即可示例代码如下 Configuration Component public class ShiroConfig {Beanpublic Realm realm() {UserRealm userRealm new UserRealm();//userRealm.setCredentialsMatcher(hashedCredentialsMatcher()); // 为realm设置指定算法userRealm.setAuthenticationTokenClass(JWTToken.class);return userRealm;}/*** 指定密码加密算法类型*/Beanpublic HashedCredentialsMatcher hashedCredentialsMatcher() {HashedCredentialsMatcher hashedCredentialsMatcher new HashedCredentialsMatcher();hashedCredentialsMatcher.setHashAlgorithmName(SHA-256); // 设置哈希算法return hashedCredentialsMatcher;} }另外有思考过用UsernamePasswordToken类作为登录参数统一再自定义Realm中处理但在其他接口进入自定义过滤器的时候发现要传入用户名和密码但是你只能获取Header中的Token处理起来很繁琐所以不考虑该思路。 自定义JWT认证过滤器 Shiro和JWT在登录验证机制上存在根本的差异 在Shiro中登录成功后Shiro会创建一个会话Session并在这个会话期间维护用户的认证状态。因此在用户的整个会话期间只要会话没有过期或被显式销毁Shiro就不会再次调用login()方法如图所示认证时所经过的几个过滤器当未登录访问时就会进FormAuthenticationFilter跳转至登录页面。 对于 JWT 这种无状态认证机制它并不依赖于 Session因此每次客户端发送请求时服务端都需要验证JWT令牌的有效性通过自定义过滤器中需要手动调用 login() 方法。 接下来我们需要创建一个自定义的 Shiro 过滤器负责处理请求中的 JWT Token。 public class JWTFilter extends BasicHttpAuthenticationFilter {/*** 执行登录你可以直接将该段代码写入isAccessAllowed()中*/Overrideprotected boolean executeLogin(ServletRequest request, ServletResponse response) {try {String headerToken getHeaderToken(request);JWTToken jwtToken new JWTToken(headerToken);getSubject(request, response).login(jwtToken);} catch (Exception e) {return false;}return true;}/*** 是否允许访问*/Overrideprotected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {if (getHeaderToken(servletRequest) ! null) {return executeLogin(servletRequest, servletResponse);}return false;}/*** 访问被拒绝*/Overrideprotected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {HttpServletResponse httpServletResponse WebUtils.toHttp(servletResponse);httpServletResponse.setCharacterEncoding(UTF-8);httpServletResponse.setContentType(application/json);servletResponse.getWriter().println(JSONObject.toJSONString(ResponseEntity.ok(认证失败)));return false;}/*** 对进入自定义过滤器的接口跨域提供支持非全局过滤器*/Overrideprotected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {HttpServletRequest httpServletRequest WebUtils.toHttp(request);HttpServletResponse httpServletResponse WebUtils.toHttp(response);httpServletResponse.setHeader(Access-control-Allow-Origin, httpServletRequest.getHeader(Origin));httpServletResponse.setHeader(Access-Control-Allow-Methods, GET,POST,OPTIONS,PUT,DELETE);httpServletResponse.setHeader(Access-Control-Allow-Headers, httpServletRequest.getHeader(Access-Control-Request-Headers));// 跨域时会首先发送一个OPTIONS请求这里我们给OPTIONS请求直接返回正常状态if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());return false;}return super.preHandle(request, response);}/*** 多种方式获取token*/private String getHeaderToken(ServletRequest servletRequest) {HttpServletRequest request WebUtils.toHttp(servletRequest);// 获取请求头的tokenString jwtToken getAuthzHeader(servletRequest);// 获取表单参数、地址栏中的tokenif (jwtToken null) {jwtToken request.getParameter(AUTHORIZATION_HEADER);}// 获取cookie中的参数Cookie[] cookies request.getCookies();if (jwtToken null cookies ! null cookies.length 0) {for (Cookie cookie : cookies) {if (cookie.getName() ! null AUTHORIZATION_HEADER.equals(cookie.getName())) {jwtToken cookie.getValue();}}}return jwtToken;} }上述示例中跨域方法注释了自定义过滤器配置的跨域方案只针对于被拦截的接口具有一定局限性当然可以处理成全局过滤器使用Component注解然后注入配置文件中但是违背了隔离原则。 配置Config 创建一个Shiro配置类并配置JWT过滤器。 Configuration public class ShiroConfig {/*** 核心安全过滤器对进入应用的请求进行拦截和过滤从而实现认证、授权、会话管理等安全功能。*/Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 自定义过滤器MapString, Filter filters new HashMap();filters.put(jwt, new JWTFilter());shiroFilterFactoryBean.setFilters(filters);// 配置拦截器链指定了哪些路径需要认证、哪些路径允许匿名访问MapString, String filterChainDefinitionMap new LinkedHashMap();filterChainDefinitionMap.put(/user/login, anon);filterChainDefinitionMap.put(/user/logout, logout);filterChainDefinitionMap.put(/**, jwt);shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}/*** 创建Shiro Web应用的整体安全管理*/Beanpublic DefaultWebSecurityManager securityManager() {DefaultWebSecurityManager defaultWebSecurityManager new DefaultWebSecurityManager();defaultWebSecurityManager.setRealm(realm());// 可以添加其他配置如缓存管理器、会话管理器等return defaultWebSecurityManager;}/*** 注册Realm的对象用于执行安全相关的操作如用户认证、权限查询*/Beanpublic Realm realm() {UserRealm userRealm new UserRealm();return userRealm;} }自定义凭证匹配规则 再默认情况下进入SimpleCredentialsMatcher.doCredentialsMatch()方法通过比较两个值是否相等如图所示 所以前面自定义Realm中有一个错误会导致密码不匹配的问题如图所示 因为登录传的Token自定义Realm中传的是用户密码示例代码如下 // 密码验证使用token SimpleAuthenticationInfo simpleAuthenticationInfo new SimpleAuthenticationInfo(new UserPrincipal(user, (String) token.getPrincipal()) , user.getPassword(), ByteSource.Util.bytes(username), getName());需要将它修改为Token两边保持一致就能解决密码不匹配的问题示例代码如下 SimpleAuthenticationInfo simpleAuthenticationInfo new SimpleAuthenticationInfo(new UserPrincipal(user,(String) token.getPrincipal()), token.getCredentials(), ByteSource.Util.bytes(username), getName());但是这种规则具有局限性无法校验Token的有效时间除非你的系统不需要设置有效时间就可以使用上述方式所以需要使用JWT的校验方式来验证Token是否有效示例代码如下 public class JWTCredentialsMatcher implements CredentialsMatcher {Overridepublic boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {UserPrincipal userPrincipal (UserPrincipal) authenticationInfo.getPrincipals().getPrimaryPrincipal();User user userPrincipal.getUser();return JWTUtil.verify((String) authenticationToken.getPrincipal(), user.getUsername(), user.getPassword());} }然后再Config中指定自定义Realm的密码匹配规则示例代码如下 Configuration Component public class ShiroConfig {// 省略部分代码... ...Beanpublic Realm realm() {UserRealm userRealm new UserRealm();userRealm.setCredentialsMatcher(new JWTCredentialsMatcher()); // 为realm指定凭证匹配规则userRealm.setAuthenticationTokenClass(JWTToken.class);return userRealm;} }接口验证 一切准备就绪后我们就来验证一下登录和接口访问我们先来访问示例代码中的/user/main接口传入错误的Token和不传Token如图所示 可以看到能够正常的拦截。然后再来访问/user/login登录接口传入不存在用户名或密码以及正确账号获取Token如图所示 三种不同的测试方式都能正确执行然后我们用返回的Token访问/user/main接口看看能否正确返回数据如图所示 访问后正常返回数据基本的JWT整合就算完成。 权限控制 Shiro通过角色和权限进行授权以确定哪些用户可以访问资源。根据代码追踪登录后不会进入自定义Realm中的doGetAuthorizationInfo()授权方法要想实现访问控制有几种方法可以提供 可以使用Shiro提供的注解比如RequiresPermissions、RequiresRoles等注解。调用subject.isPermitted()、subject.checkPermission()等判断角色权限方法。配置拦截器链指定访问权限比如filterChainDefinitionMap.put(/user/list, roles[root]); 以上三种方式再访问时触发操作个人比较偏向第二种方式注解的方式不够灵活需要每个接口都加上注解拦截器链配置的方式也不够灵活而且是再登陆前进行的校验导致返回的状态码是401未授权而不是500所以第二种方式相比之下更加灵活且友好返回结果。 我们只需要再自定义过滤器中判断即可示例代码如下 public class JWTFilter extends BasicHttpAuthenticationFilter {/*** 是否允许访问*/Overrideprotected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {HttpServletRequest request WebUtils.toHttp(servletRequest);String requestURI request.getRequestURI();Subject subject getSubject(servletRequest, servletResponse);// 当header不为空且if (getHeaderToken(servletRequest) null || !executeLogin(servletRequest, servletResponse)) {return false;}// 是否拥有访问权限if (!subject.isPermitted(requestURI)) {return false;}return true;}// 省略部分代码... ... }假如说现在该用户只有/user/main的权限当他访问/user/list接口时就会被拒绝如图所示 当没有权限时为了更加友好的提示我们稍作修改示例代码如下 public class JWTFilter extends BasicHttpAuthenticationFilter {/*** 访问被拒绝*/Overrideprotected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {HttpServletResponse httpServletResponse WebUtils.toHttp(servletResponse);httpServletResponse.setCharacterEncoding(UTF-8);httpServletResponse.setContentType(application/json);PrintWriter writer servletResponse.getWriter();Object principal getSubject(servletRequest, servletResponse).getPrincipal();if (principal null) {writer.println(JSONObject.toJSONString(ResponseEntity.ok(没有登录)));} else {writer.println(JSONObject.toJSONString(ResponseEntity.ok(没有权限)));}return false;}// 省略部分代码... ... }禁用session 在前面的介绍中虽然使用的Token认证但还是会生成Session如图所示 Shiro的session管理通常用于跟踪用户的登录状态和会话信息但在某些情况下你可能希望禁用它例如当你使用基于token的认证如JWT时。 官方文档如图所示 DefaultWebSecurityManager中设置禁用代码示例代码如下 public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {Overridepublic Subject createSubject(SubjectContext context) {context.setSessionCreationEnabled(false);return super.createSubject(context);} } Configuration public class ShiroConfig {Beanpublic DefaultWebSecurityManager securityManager() {DefaultWebSecurityManager defaultWebSecurityManager new DefaultWebSecurityManager();defaultWebSecurityManager.setRealm(realm());// 每次请求不创建sessionStatelessDefaultSubjectFactory statelessDefaultSubjectFactory new StatelessDefaultSubjectFactory();defaultWebSecurityManager.setSubjectFactory(statelessDefaultSubjectFactory);// 登录不创建sessionDefaultSubjectDAO subjectDAO new DefaultSubjectDAO();DefaultSessionStorageEvaluator defaultSessionStorageEvaluator new DefaultSessionStorageEvaluator();defaultSessionStorageEvaluator.setSessionStorageEnabled(false);subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);defaultWebSecurityManager.setSubjectDAO(subjectDAO);// 可以添加其他配置如缓存管理器、会话管理器等return defaultWebSecurityManager;} }然后重新登录或每次请求就不会创建Token了如图所示 缓存的使用 Shiro 提供了缓存机制用于提高性能对应前后分离项目频繁的调用登录减少自定义Realm的验证、授权对底层数据源如数据库的频繁访问。 实现Shiro提供的Cache类示例代码如下 public class RedisCacheManage implements CacheManager {private final RedisTemplateString, Object redisTemplate;public RedisCacheManage(RedisTemplateString, Object redisTemplate) {this.redisTemplate redisTemplate;}Overridepublic K, V CacheK, V getCache(String s) throws CacheException {return new RedisCache(s, redisTemplate);} } public class RedisCacheK, V implements CacheK, V {private final HashOperationsString, K, V hashOperations;private final String name;public RedisCache(String name, RedisTemplateString, Object redisTemplate) {this.name name;this.hashOperations redisTemplate.opsForHash();}Overridepublic V get(K k) throws CacheException {return hashOperations.get(name, k);}Overridepublic V put(K k, V v) throws CacheException {hashOperations.put(name, k, v);return v;}Overridepublic V remove(K k) throws CacheException {V v hashOperations.get(name, k);hashOperations.delete(name, k);return v;}Overridepublic void clear() throws CacheException {hashOperations.delete(name);}Overridepublic int size() {return hashOperations.size(name).intValue();}Overridepublic SetK keys() {return hashOperations.keys(name);}Overridepublic CollectionV values() {return hashOperations.values(name);} }然后配置Redis并再自定义Realm中配置使用示例代码如下 Configuration public class ShiroConfig {// 省略部分代码... .../*** 创建Shiro Web应用的整体安全管理*/Beanpublic DefaultWebSecurityManager securityManager() {DefaultWebSecurityManager defaultWebSecurityManager new DefaultWebSecurityManager();defaultWebSecurityManager.setRealm(realm());// 可以添加其他配置如缓存管理器、会话管理器等return defaultWebSecurityManager;}/*** 注册Realm的对象用于执行安全相关的操作如用户认证、权限查询*/Beanpublic Realm realm() {UserRealm userRealm new UserRealm();userRealm.setCredentialsMatcher(new JWTCredentialsMatcher()); // 为realm设置指定算法userRealm.setCachingEnabled(true); // 启动全局缓存userRealm.setAuthenticationCachingEnabled(true); // 启动验证缓存userRealm.setAuthorizationCachingEnabled(true);userRealm.setAuthenticationCacheName(Authentication); // 定义授权缓存名userRealm.setAuthorizationCacheName(Authorization); // 定义认证缓存名userRealm.setCacheManager(cacheManager());userRealm.setAuthenticationTokenClass(JWTToken.class);return userRealm;}/*** redis配置*/Autowiredprivate RedisConnectionFactory redisConnectionFactory;Beanpublic RedisTemplateString, Object redisTemplate() {RedisTemplateString, Object redisTemplate new RedisTemplate();redisTemplate.setConnectionFactory(redisConnectionFactory);Jackson2JsonRedisSerializerObject jackson2JsonRedisSerializer new Jackson2JsonRedisSerializer(Object.class);ObjectMapper objectMapper new ObjectMapper();//设置了 ObjectMapper 的可见性规则。通过该设置所有字段包括 private、protected 和 package-visible 等都将被序列化和反序列化无论它们的可见性如何。objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);//启用了默认的类型信息 NON_FINAL 参数表示只有非 final 类型的对象才包含类型信息这可以帮助在反序列化时正确地将 JSON 字符串转换回对象。objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);StringRedisSerializer stringRedisSerializer new StringRedisSerializer();//key采用String的序列化方式redisTemplate.setKeySerializer(stringRedisSerializer);//hash的key也采用String的序列化方式redisTemplate.setHashKeySerializer(stringRedisSerializer);return redisTemplate;} }执行登录请求后、正常访问接口可以看到第二次请求进入自定义过滤器登录就会直接从缓存获取数据源码不做介绍如图所示 查看Redis中保存了验证、授权的信息如图所示 但是还会带来一个问题如果当用户退出后原有的Token如果没到过期时间依然可以使用进行接口访问理想状态下退出后不可再继续使用的如图所示 原因Redis缓存的校验、授权数据清空后再次请求判断缓存中没有数据进入自定义Realm中重新查询数据库进行保存 解决这个问题可以再登录时生成一个Key存入Redis缓存中请求其他接口时再自定义过滤器中判断缓存是否存在退出时删除该缓存。示例代码如下 Controller RequestMapping(value /user) public class UserController {Autowiredprivate UserService userService;Autowiredprivate RedisTemplateString,Object redisTemplate;PostMapping(/login)public ResponseEntity login(RequestParam(username) String username, RequestParam(password) String password, HttpServletResponse response) {// 校验用户User user userService.getOne(new QueryWrapperUser().ge(username, username));if (user null) {return ResponseEntity.ok(用户不存在);}SimpleHash hash new SimpleHash(MD5, password, username);if (!hash.toHex().equals(user.getPassword())) {return ResponseEntity.ok(账号或密码错误);}// 生成tokenString sign JWTUtil.sign(username, hash.toHex());JWTToken jwtToken new JWTToken(sign);// 保存用户tokenString key login_user_token_username;redisTemplate.opsForValue().set(key, sign);try {// 对用户信息进行身份认证Subject subject SecurityUtils.getSubject();subject.login(jwtToken);} catch (IncorrectCredentialsException e) {return ResponseEntity.ok(密码错误);}// 返回响应参数response.setHeader(Authorization, sign);return ResponseEntity.ok(jwtToken.getPrincipal());}PostMapping(/logout)public ResponseEntity logout() {UserPrincipal userPrincipal (UserPrincipal) SecurityUtils.getSubject().getPrincipal();// 删除用户tokenString key login_user_token_userPrincipal.getUser().getUsername();redisTemplate.delete(key);// 同时清空验证和授权缓存SecurityUtils.getSubject().logout();return ResponseEntity.ok(成功);} } Configuration public class ShiroConfig {/*** 核心安全过滤器对进入应用的请求进行拦截和过滤从而实现认证、授权、会话管理等安全功能。*/Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 配置拦截器链指定了哪些路径需要认证、哪些路径允许匿名访问MapString, Filter filters new HashMap();filters.put(jwt, new JWTFilter(redisTemplate()));shiroFilterFactoryBean.setFilters(filters);MapString, String filterChainDefinitionMap new LinkedHashMap();filterChainDefinitionMap.put(/user/login, anon);filterChainDefinitionMap.put(/**, jwt);shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}// 省略部分代码... ... } public class JWTFilter extends BasicHttpAuthenticationFilter {private RedisTemplateString,Object redisTemplate;public JWTFilter(RedisTemplateString,Object redisTemplate){this.redisTemplate redisTemplate;}/*** 执行登录*/Overrideprotected boolean executeLogin(ServletRequest request, ServletResponse response) {try {String headerToken getHeaderToken(request);JWTToken jwtToken new JWTToken(headerToken);// 获取tokenString key login_user_token_JWTUtil.getUsername(headerToken);String redisToken (String) redisTemplate.opsForValue().get(key);if (redisToken null) { // 缓存为空可能有效期过期同时删除验证和授权缓存getSubject(request, response).logout();return false;}getSubject(request, response).login(jwtToken);} catch (Exception e) {e.printStackTrace();return false;}return true;}// 省略部分代码... ... }下次访问当缓存中的数据不存在时比如缓存过期导致同时需要将Redis缓存中验证、授权时保存的数据一并清除避免占用资源也解决了失效Token继续请求的问题但是还有另一个问题如果缓存过期后直接请求登录接口导致验证、授权保存的数据未清除所以这种情况下可能还需要监听Redis过期进行处理执行结果如图所示 如果不将验证、授权进行缓存那么处理起来就非常的简单直接在自定义Realm中判断Redis的Key过期没有即可如果启用缓存在自定义Realm中判断会导致下次请求直接从缓存获取数据不进入自定义Realm中所以再过滤器中处理不需要考虑验证、授权的缓存清除问题建议这样不然你要考虑很多缓存过期数据清理的问题比如缓存过期对应数据验证、授权需要清除。 除此之外你还可以给这个Key设置过期时间每次请求当缓存未删除时需要给该缓存的过期时间进行延长过期删除后下次请求不允许访问需要重新登录后续讲解。 另外如果只允许一个用户同时登录你还需要判断当前的用户Token与缓存中的是否一致。将另外一个用户进行下线保证只有一个用户可以操作。 更多的处理方式取决于你的业务深度具体问题具体分析。 登录退出 与前后不分离项目有所区别如果登录退出使用默认logout过滤器示例代码如下 filterChainDefinitionMap.put(/user/logout, logout);会导致退出成功后无法重定向到指定页面导致报错如图所示 我们可以看下源码如图所示 当登录退出后默认重定向到/根目录因为是前后分离页面导致报错。 解决这个问题在过滤器链中不定义/user/logout接口然后就会先进入自定义过滤器中然后执行登录接口逻辑再执行具体接口业务如果Token不存在则在过滤器中被拦截不建议设置为anon匿名过滤器如果先用失效的Token再用未失效的Token会导致获取不到当前登录用户的情况导致无法清除一定是登录成功的用户才可以登录退出。 示例代码如下 Configuration public class ShiroConfig {/*** 核心安全过滤器对进入应用的请求进行拦截和过滤从而实现认证、授权、会话管理等安全功能。*/Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 配置拦截器链指定了哪些路径需要认证、哪些路径允许匿名访问MapString, Filter filters new HashMap();filters.put(jwt, new JWTFilter(redisTemplate()));shiroFilterFactoryBean.setFilters(filters);MapString, String filterChainDefinitionMap new LinkedHashMap();filterChainDefinitionMap.put(/user/login, anon);filterChainDefinitionMap.put(/**, jwt);shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}// 省略部分代码... ... } public class JWTFilter extends BasicHttpAuthenticationFilter {private RedisTemplateString,Object redisTemplate;public JWTFilter(RedisTemplateString,Object redisTemplate){this.redisTemplate redisTemplate;}/*** 执行登录*/Overrideprotected boolean executeLogin(ServletRequest request, ServletResponse response) {try {String headerToken getHeaderToken(request);JWTToken jwtToken new JWTToken(headerToken);String key login_user_token_JWTUtil.getUsername(headerToken);String redisToken (String) redisTemplate.opsForValue().get(key);if (redisToken null) { // 只有Token一致才能登录其他全部拦截getSubject(request, response).logout();return false;}getSubject(request, response).login(jwtToken);} catch (Exception e) {e.printStackTrace();return false;}return true;}// 省略部分代码... ... } Controller RequestMapping(value /user) public class UserController {PostMapping(/logout)public ResponseEntity logout() {UserPrincipal userPrincipal (UserPrincipal) SecurityUtils.getSubject().getPrincipal();if (userPrincipal ! null) {// 删除用户tokenString key login_user_token_ userPrincipal.getUser().getUsername();redisTemplate.delete(key);}SecurityUtils.getSubject().logout();return ResponseEntity.ok(成功);} }因为我们使用缓存解决登录退出后Token继续使用的问题所以登录退出时还需要将该缓存从数据清空如果开启了验证、授权缓存登录退出时logout()方法自动清除为了防止过期的Token请求登录退出接口导致Null指针错误需进行非空判断。执行结果如图 单用户登录 在实际应用中有时需要限制一个账号只能在一处登录即实现单用户登录功能。 在前面的缓存章节中有介绍过Token失效后继续使用的问题我们使用另一个缓存解决Token失效的问题当然该解决方案稍作修改也解决的单用户登录的问题示例代码如下 public class JWTFilter extends BasicHttpAuthenticationFilter {private RedisTemplateString,Object redisTemplate;public JWTFilter(RedisTemplateString,Object redisTemplate){this.redisTemplate redisTemplate;}/*** 执行登录*/Overrideprotected boolean executeLogin(ServletRequest request, ServletResponse response) {try {String headerToken getHeaderToken(request);JWTToken jwtToken new JWTToken(headerToken);String key login_user_token_JWTUtil.getUsername(headerToken);String redisToken (String) redisTemplate.opsForValue().get(key);if (redisToken null || !redisToken.equals(headerToken)) { // 只有Token一致才能登录其他全部拦截getSubject(request, response).logout();return false;}getSubject(request, response).login(jwtToken);} catch (Exception e) {e.printStackTrace();return false;}return true;}// 省略部分代码... ... }执行结果如图所示 其原理就是相同Key对应的Value被替换当Token不一致时不允许访问接口从而实现了单用户登录功能这样就确保了一个账号只能在一处登录。 如果想要实现同一账号多用户登录就不能公用一个Key每个用户登录生成不同Token当作Key可以解决一账号多用户登录也可以阻止Token失效继续访问的问题如图所示 如果验证、授权使用缓存实现单多用户登录最简单的办法就是缓存的Key进行处理保持一致实现逻辑和上面基本一致这样三个缓存验证、授权、解决Token失效的缓存都要保持一致。 Token刷新 以页面的形式进行讲解请求登录接口遇到CORS跨域问题因为登录使用匿名过滤器自定义过滤器的跨域处理不会生效如图所示 所有这里定义一个全局的跨域配置有很多种方案示例代码如下 Configuration public class MyCorsConfig {Beanpublic CorsFilter corsFilter() {CorsConfiguration config new CorsConfiguration();config.addAllowedOriginPattern(*); // 支持域config.setAllowCredentials(true); // 是否发送Cookieconfig.addAllowedMethod(*); // 支持请求方式config.addAllowedHeader(*); // 允许的原始请求头部信息config.addExposedHeader(*); // 暴露的头部信息UrlBasedCorsConfigurationSource corsConfigurationSource new UrlBasedCorsConfigurationSource();corsConfigurationSource.registerCorsConfiguration(/**, config);return new CorsFilter(corsConfigurationSource);} }在 Web 应用程序中Token令牌通常用于身份验证和授权。为了保证安全性和用户体验 Token 通常会设置一个较短的有效期。当 Token 即将过期或已经过期时需要进行刷新操作以获取新的 Token。 双Token方案 客户端在初次认证时服务器会返回一个短期有效的访问Token和一个长期有效的刷新 Token。客户端在访问 Token过期时可以使用刷新Token向服务器申请新的访问Token。 登录时生成访问令牌accessToken和刷新令牌refreshToken前端隔段时间通过refreshToken调用/user/refreshToken接口获取新的accessToken示例代码如下 Controller RequestMapping(value /user) public class UserController {Autowiredprivate UserService userService;Autowiredprivate RedisTemplateString,Object redisTemplate;PostMapping(/login)public ResponseEntity login(RequestParam(username) String username, RequestParam(password) String password, HttpServletResponse response) {User user userService.getOne(new QueryWrapperUser().ge(username, username));if (user null) {return ResponseEntity.ok(用户不存在);}Sha256Hash hash new Sha256Hash(password, username);if (!hash.toHex().equals(user.getPassword())) {return ResponseEntity.ok(账号或密码错误);}// 生成tokenString sign JWTUtil.sign(username, hash.toHex());// 生成token 假设AccessToken为30分钟有效期String generateAccessToken JWTUtil.sign(username, hash.toHex());// RefreshToken7天有效期String generateRefreshToken JWTUtil.sign(username, hash.toHex());// 保存用户tokenString accessToken access_token_username;redisTemplate.opsForValue().set(accessToken, generateAccessToken);String refreshToken refresh_token_username;redisTemplate.opsForValue().set(refreshToken, generateRefreshToken);try {// 对用户信息进行身份认证JWTToken jwtToken new JWTToken(generateAccessToken);Subject subject SecurityUtils.getSubject();subject.login(jwtToken);} catch (IncorrectCredentialsException e) {return ResponseEntity.ok(密码错误);}// 返回响应参数response.setHeader(accessToken, generateAccessToken);response.setHeader(refreshToken, generateRefreshToken);MapString, String tokenResp new HashMap();tokenResp.put(accessToken, accessToken);tokenResp.put(refreshToken, refreshToken);return ResponseEntity.ok(tokenResp);}PostMapping(/refreshToken)public ResponseEntityMapString, String refreshToken(RequestBody MapString, String request) {UserPrincipal userPrincipal (UserPrincipal) SecurityUtils.getSubject().getPrincipal();User user userPrincipal.getUser();String refreshToken request.get(refreshToken);// 校验token是否过期if (JWTUtil.verify(refreshToken, user.getUsername(), user.getPassword())) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();}String newAccessToken JWTUtil.sign(user.getUsername(), user.getPassword());MapString, String response new HashMap();response.put(accessToken, newAccessToken);return ResponseEntity.ok(response);} }双 Token 方案需要额外管理刷新令牌包括生成、存储、验证和更新等操作增加了系统的复杂度和开发成本。 单Token方案 访问 Token本身携带所有用户认证信息。当Token过期时客户端需要重新进行登录获取新的Token。这种方案非常的简单粗暴一般很少使用这种方案。 登录时保存Token并设置缓存过期时间示例代码如下 Controller RequestMapping(value /user) public class UserController {Autowiredprivate UserService userService;Autowiredprivate RedisTemplateString,Object redisTemplate;PostMapping(/login)public ResponseEntity login(RequestParam(username) String username, RequestParam(password) String password, HttpServletResponse response) {User user userService.getOne(new QueryWrapperUser().ge(username, username));if (user null) {return ResponseEntity.ok(用户不存在);}Sha256Hash hash new Sha256Hash(password, username);if (!hash.toHex().equals(user.getPassword())) {return ResponseEntity.ok(账号或密码错误);}// 生成tokenString sign JWTUtil.sign(username, hash.toHex());JWTToken jwtToken new JWTToken(sign);// 保存用户token设置token有效期30分钟String key login_user_token_username;redisTemplate.opsForValue().set(key, sign, 30, TimeUnit.HOURS);try {// 对用户信息进行身份认证Subject subject SecurityUtils.getSubject();subject.login(jwtToken);} catch (IncorrectCredentialsException e) {return ResponseEntity.ok(密码错误);}// 返回响应参数response.setHeader(Authorization, sign);// 返回cookieCookie cookie new Cookie(Authorization, sign);response.addCookie(cookie);return ResponseEntity.ok(jwtToken.getPrincipal());} }用户登录后访问其他接口进入自定义过滤器如果缓存中的Token过期被清除了要求重新登录示例代码如下 public class JWTFilter extends BasicHttpAuthenticationFilter {private RedisTemplateString,Object redisTemplate;public JWTFilter(RedisTemplateString,Object redisTemplate){this.redisTemplate redisTemplate;}/*** 执行登录*/Overrideprotected boolean executeLogin(ServletRequest request, ServletResponse response) {try {String headerToken getHeaderToken(request);JWTToken jwtToken new JWTToken(headerToken);// 获取tokenString key login_user_token_JWTUtil.getUsername(headerToken);String redisToken (String) redisTemplate.opsForValue().get(key);// 判断Token是否存在if (redisToken null || !redisToken.equals(headerToken)) {getSubject(request, response).logout();return false;}getSubject(request, response).login(jwtToken);} catch (Exception e) {e.printStackTrace();return false;}return true;}/*** 是否允许访问*/Overrideprotected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {HttpServletRequest http WebUtils.toHttp(servletRequest);String requestURI http.getRequestURI();System.out.println(requestURI);Subject subject getSubject(servletRequest, servletResponse);boolean isAccessAllowed false;if (getHeaderToken(servletRequest) ! null) {isAccessAllowed executeLogin(servletRequest, servletResponse);}System.out.println(subject.getPrincipal());if (isAccessAllowed !subject.isPermitted(requestURI)) {return false;}return isAccessAllowed;}/*** 访问被拒绝*/Overrideprotected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {HttpServletResponse httpServletResponse WebUtils.toHttp(servletResponse);httpServletResponse.setCharacterEncoding(UTF-8);httpServletResponse.setContentType(application/json);Object subject getSubject(servletRequest, servletResponse).getPrincipal();PrintWriter writer servletResponse.getWriter();if (subject null) {writer.println(JSONObject.toJSONString(ResponseEntity.ok(没有登录)));} else {writer.println(JSONObject.toJSONString(ResponseEntity.ok(没有权限)));}return false;}// 省略部分代码... ... }不需要额外实现复杂的 Token 刷新机制系统只需要关注 Token 的有效期和登录验证逻辑降低了开发和维护的难度。 用户在使用过程中一旦 Token 过期就需要重新输入用户名和密码进行登录尤其是在频繁操作或者长时间使用的场景下这会给用户带来极大的不便降低用户对系统的满意度。 访问Token有一个固定的过期时间然而每次使用Token时过期时间会重新刷新延长到固定的时间窗口。这种方式通常与刷新 Token方案结合使用。 登录时设置Token存放Redis中的有效时间Token不设置过期时间示例代码如下 public class JWTUtil {/*** 生成token签名** param username 用户名* param secret 密码* return token字符串*/public static String sign(String username, String secret) {String token JWT.create().withClaim(username, username).withIssuedAt(new Date()) // 发行时间.sign(Algorithm.HMAC256(secret));return token;}// 省略部分代码... ... } Controller RequestMapping(value /user) public class UserController {Autowiredprivate UserService userService;Autowiredprivate RedisTemplateString,Object redisTemplate;PostMapping(/login)public ResponseEntity login(RequestParam(username) String username, RequestParam(password) String password, HttpServletResponse response) {// 校验用户User user userService.getOne(new QueryWrapperUser().ge(username, username));if (user null) {return ResponseEntity.ok(用户不存在);}SimpleHash hash new SimpleHash(MD5, password, username);if (!hash.toHex().equals(user.getPassword())) {return ResponseEntity.ok(账号或密码错误);}// 生成tokenString sign JWTUtil.sign(username, hash.toHex());JWTToken jwtToken new JWTToken(sign);// 保存用户token设置token有效期30分钟String key login_user_token_username;redisTemplate.opsForValue().set(key, sign, 30, TimeUnit.MINUTES);try {// 对用户信息进行身份认证Subject subject SecurityUtils.getSubject();subject.login(jwtToken);} catch (IncorrectCredentialsException e) {return ResponseEntity.ok(密码错误);}// 返回响应参数response.setHeader(Authorization, sign);// 返回cookieCookie cookie new Cookie(Authorization, sign);response.addCookie(cookie);return ResponseEntity.ok(jwtToken.getPrincipal());} }访问其他接口时进入自定义过滤器中如果Redis中的Token不存在过期删除就要求重新登陆否则每次请求就将Redis过期时间刷新示例代码如下 public class JWTFilter extends BasicHttpAuthenticationFilter {private RedisTemplateString,Object redisTemplate;public JWTFilter(){}public JWTFilter(RedisTemplateString,Object redisTemplate){this.redisTemplate redisTemplate;}/*** 执行登录*/Overrideprotected boolean executeLogin(ServletRequest request, ServletResponse response) {try {String headerToken getHeaderToken(request);JWTToken jwtToken new JWTToken(headerToken);// 获取tokenString key login_user_token_JWTUtil.getUsername(headerToken);String redisToken (String) redisTemplate.opsForValue().get(key);if (redisToken null || !redisToken.equals(headerToken)) {getSubject(request, response).logout();return false;} else { // 重置redis过期时间System.out.println(redis-expire:redisTemplate.getExpire(key, TimeUnit.MINUTES));redisTemplate.expire(key, 30, TimeUnit.MINUTES);}getSubject(request, response).login(jwtToken);} catch (Exception e) {e.printStackTrace();return false;}return true;}// 省略部分代码... ... }每次请求刷新对应Redis有效期若长时间未操作到期自动清除重新登陆。执行结果如图所示 如果你觉得Token续期的方案安全性太低了当令牌已过期服务器返回特定的错误码如 401 Unauthorized客户端捕获到该错误后自动向服务器发送刷新令牌的请求。有三种方案定时刷新、请求拦截、响应拦截下面针对这几种方案进行详细讲解。 定时刷新 在用户登录成功后与后端确认Token过期时间一般来说应小于服务器的过期时间使用 JavaScript 的 setInterval 函数来定时触发刷新 Token的请求。 后端需要提供Token刷新接口通过当前Token的有效期防止频繁刷新Token缓存Token中不存在再拦截器中处理示例代码如下 Controller RequestMapping(value /user) public class UserController {PostMapping(/refreshToken)public ResponseEntity refreshToken(HttpServletResponse response) {UserPrincipal userPrincipal (UserPrincipal) SecurityUtils.getSubject().getPrincipal();User user userPrincipal.getUser();// 获取Token有效期Date expiry JWTUtil.getExpiry(userPrincipal.getToken());// 正确将 Date 转换为 LocalDateTimeInstant instant expiry.toInstant();LocalDateTime dateAsLocalDateTime instant.atZone(ZoneId.systemDefault()).toLocalDateTime();LocalDateTime now LocalDateTime.now();Duration between Duration.between(now, dateAsLocalDateTime);// 间隔时间大于5分钟不生成新tokenif (between.toMinutes() 5) {// 返回原tokenresponse.setHeader(Authorization, userPrincipal.getToken());return ResponseEntity.ok(userPrincipal.getToken());}String sign JWTUtil.sign(user.getUsername(), user.getPassword());// token重新赋值String key login_user_token_ userPrincipal.getUser().getUsername();redisTemplate.opsForValue().set(key, sign);// 返回tokenresponse.setHeader(Authorization, sign);return ResponseEntity.ok(sign);} }登录时生成Token并设置JWT有效期短Token和Redis有效期长Token存入缓存示例代码如下 public class JWTUtil {// 过期30分钟private static final long EXPIRE_TIME 30 * 60 * 1000;/*** 生成token签名** param username 用户名* param secret 密码* return token字符串*/public static String sign(String username, String secret) {String token JWT.create().withClaim(username, username).withIssuedAt(new Date()) // 发行时间.withExpiresAt(new Date(System.currentTimeMillis() EXPIRE_TIME)) // 过期时间.sign(Algorithm.HMAC256(secret));return token;}/*** 获取token有效期* param token* return */public static Date getExpiry(String token){try {return JWT.decode(token).getExpiresAt();} catch (Exception e) {return null;}} } Controller RequestMapping(value /user) public class UserController {PostMapping(/login)public ResponseEntity login(RequestParam(username) String username, RequestParam(password) String password, HttpServletResponse response) {User user userService.getOne(new QueryWrapperUser().ge(username, username));if (user null) {return ResponseEntity.ok(用户不存在);}Sha256Hash hash new Sha256Hash(password, username);if (!hash.toHex().equals(user.getPassword())) {return ResponseEntity.ok(账号或密码错误);}// 生成tokenString sign JWTUtil.sign(username, hash.toHex());JWTToken jwtToken new JWTToken(sign);// 保存用户token设置token有效期7天String key login_user_token_username;redisTemplate.opsForValue().set(key, sign, 7, TimeUnit.DAY);try {// 对用户信息进行身份认证Subject subject SecurityUtils.getSubject();subject.login(jwtToken);} catch (IncorrectCredentialsException e) {return ResponseEntity.ok(密码错误);}// 返回响应参数response.setHeader(Authorization, sign);// 返回cookieCookie cookie new Cookie(Authorization, sign);response.addCookie(cookie);return ResponseEntity.ok(jwtToken.getPrincipal());}// 模拟首页多个请求情况PostMapping(/main)public ResponseEntity main() {Subject subject SecurityUtils.getSubject();System.out.println(main);System.out.println(subject.getPrincipal());return ResponseEntity.ok(success);}PostMapping(/main2)public ResponseEntity main2() {return ResponseEntity.ok(success);}PostMapping(/main3)public ResponseEntity main3() {try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}return ResponseEntity.ok(success);}PostMapping(/main4)public ResponseEntity main4() {try {Thread.sleep(4000);} catch (InterruptedException e) {throw new RuntimeException(e);}return ResponseEntity.ok(success);} }如果用过期的Token访问JWT过期和Redis过期删除接口过滤器拦截后端直接认定为没有登陆JWT过期再登录时参考自定义匹配凭证章节Redis过期删除判断主要再拦截器中处理因为每隔段时间会更换Token所以不需要缓存延期示例代码如下 public class JWTFilter extends BasicHttpAuthenticationFilter {Overrideprotected boolean executeLogin(ServletRequest request, ServletResponse response) {try {String headerToken getHeaderToken(request);JWTToken jwtToken new JWTToken(headerToken);// 获取tokenString key login_user_token_JWTUtil.getUsername(headerToken);String redisToken (String) redisTemplate.opsForValue().get(key);if (redisToken null || !redisToken.equals(headerToken)) {// 通过状态码跳转登录httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);getSubject(request, response).logout();return false;}getSubject(request, response).login(jwtToken);} catch (Exception e) {e.printStackTrace();return false;}return true;}// 省略部分代码... ... }前端以JavaScript方式的 setInterval 函数来定时触发刷新 Token的请求。以登录成功后将Token存入localStorage进入首页为例示例代码如下 !DOCTYPE html html langen headmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1.0title首页页面/title /head body div classlogin-containerh2首页/h2button idfetchDataButton发起请求/buttonp idresponseMessage/p !-- 添加了这个元素 --pa href/user/logout退出登录/a/p /div script typemoduleimport { makeRequest } from ./tokenHandler.js;// 触发点击事件document.getElementById(fetchDataButton).addEventListener(click, async () {try {await makeRequest(http://localhost:8080/user/main);await makeRequest(http://localhost:8080/user/main2);await makeRequest(http://localhost:8080/user/main3);await makeRequest(http://localhost:8080/user/main4);// console.log(请求结果:, result);// document.getElementById(responseMessage).textContent result;} catch (error) {console.error(Error:, error);}}); /script /body /html封装成公用js代码方便调用定时任务每隔段时间请求刷新Token接口如果此时用户正在请求就会放入队列中进行等待示例代码如下 // 标记 Token 是否正在刷新 let isRefreshingToken false; // 请求队列 let requestQueue [];// 模拟后端接口刷新 Token async function refreshTokenApi() {isRefreshingToken true;try {// 这里模拟向服务器发送刷新 token 的请求const response await fetch(http://localhost:8080/user/refreshToken, {method: POST,headers: {Content-Type: application/json,Authorization: ${localStorage.getItem(token)}},// body: JSON.stringify({// // 可以传递一些必要的参数// })});const data await response.text();// 假设返回的新 token 字段名为 newToken根据实际情况修改localStorage.setItem(token, data);console.log(Token 刷新成功data);// 执行队列中的请求requestQueue.forEach(request request(localStorage.getItem(token)));requestQueue [];return data;} catch (error) {console.error(Token 刷新失败, error);requestQueue [];throw error;} finally {isRefreshingToken false;} }// 定时刷新 Token const refreshInterval 5000; // 每 5 秒尝试刷新 Token setInterval(() {refreshTokenApi(); }, refreshInterval);// 发起请求的函数 // 封装通用的请求函数 const makeRequest async (url, options {}) {return new Promise(async (resolve, reject) {const originalRequest async (token) {try {const newOptions {...options,headers: {...options.headers,Authorization: ${token}},method: GET};const response await fetch(url, newOptions);const data await response.text();resolve(data);} catch (error) {reject(error);}};console.log(isRefreshingToken)if (isRefreshingToken) {console.log(当前正再刷新token请求放入队列中)// 如果 Token 正在刷新将请求加入队列requestQueue.push(originalRequest);console.log(requestQueue)} else {// 检查 Token 是否过期这里简单假设响应状态码为 401 表示 Token 过期const response await fetch(url, {...options,headers: {...options.headers,Authorization: ${localStorage.getItem(token)}}});if (response.status 401) {// Token 过期开始刷新requestQueue.push(originalRequest);refreshTokenApi().catch(reject);} else {const data await response;resolve(data);}}}); }; export { makeRequest };其实前端放入队列的请求只有一个Token刷新后继续执行队列中的请求后续的请求等待第一个执行完毕后才会执行类似于同步操作。 我们需要考虑第一个问题如果请求接口模拟某个接口请求慢的情况途中触发Token刷新那么后面接口就应该等待Token刷新后使用新的Token进行请求执行结果如图所示 第二个问题如果Token刷新的途中模拟Token刷新慢发起请求应该等待Token刷新完毕后再发起请求执行结果如图所示 按照固定的时间间隔向服务器发送刷新请求会显著增加服务器的处理压力频繁的刷新操作会消耗额外的网络带宽。另一方面攻击者可以通过截获 Token 刷新请求分析其中的加密算法和数据格式从而尝试破解用户的 Token。 请求拦截 在获取 Token 时服务器通常会同时返回 Token 的过期时间也可以前端解析JWT获取。在请求拦截时通过比较当前时间和过期时间来判断 Token 是否过期若已过期则将请求挂起先刷新Token后再继续请求。 我们需要再登录接口和刷新Token接口返回参数加上Token有效期示例代码如下 Controller RequestMapping(value /user) public class UserController {PostMapping(/login)public ResponseEntity login(RequestParam(username) String username, RequestParam(password) String password, HttpServletResponse response) {// 省略部分代码... ...// 返回参数MapString,Object map new HashMap();map.put(Authorization, sign);map.put(ExpireTime, JWTUtil.getExpiry(sign).getTime());return ResponseEntity.ok(jwtToken.getPrincipal());}PostMapping(/refreshToken)public ResponseEntity refreshToken() throws InterruptedException {// 省略部分代码... ...// 返回参数MapString,Object map new HashMap();map.put(Authorization, sign);map.put(ExpireTime, JWTUtil.getExpiry(sign).getTime());return ResponseEntity.ok(sign);} }登录成功后将Token和ExpireTime存入localStorage进入首页为例主要介绍js部分的处理其他代码参考之前案例用户发起请求前先校验Token的过期时间是否达到范围如果没有则正常请求否则将请求放入队列先执行Token刷新操作示例代码如下 // 标记 Token 是否正在刷新 let isRefreshingToken false; // 请求队列 let requestQueue [];// 模拟后端接口刷新 Token async function refreshTokenApi() {isRefreshingToken true;try {// 这里模拟向服务器发送刷新 token 的请求const response await fetch(http://localhost:8080/user/refreshToken, {method: POST,headers: {Content-Type: application/json,Authorization: ${localStorage.getItem(token)}},// body: JSON.stringify({// // 可以传递一些必要的参数// })});const data await response.json();// 假设返回的新 token 字段名为 newToken根据实际情况修改localStorage.setItem(token, data.Authorization);localStorage.setItem(expireTime, data.ExpireTime);console.log(Token 刷新成功data);// 执行队列中的请求requestQueue.forEach(request request(localStorage.getItem(token)));requestQueue [];return data;} catch (error) {console.error(Token 刷新失败, error);requestQueue [];throw error;} finally {isRefreshingToken false;} } // 封装通用的请求函数 const makeRequest async (url, options {}) {return new Promise(async (resolve, reject) {const originalRequest async (token) {try {const newOptions {...options,headers: {...options.headers,Authorization: ${token}},method: GET};const response await fetch(url, newOptions);if (!response.ok) {if (response.status 401) { // 再次检查以防本地判断失误if (!isRefreshing) {requestQueue.push((newToken) originalRequest(newToken));await refreshToken();} else {requestQueue.push((newToken) originalRequest(newToken));}return;}throw new Error(Request failed with status ${response.status});}const data await response.text();resolve(data);} catch (error) {reject(error);}};// 请求拦截判断 Token 是否过期const currentTime Date.now();const expireTime localStorage.getItem(expireTime);if (expireTime isFiveMinutesApart(expireTime, currentTime)) {console.log(isRefreshingToken)if (isRefreshingToken) {console.log(当前正再刷新token请求放入队列中)// 如果 Token 正在刷新将请求加入队列requestQueue.push(originalRequest);console.log(requestQueue)} else {requestQueue.push(originalRequest);await refreshTokenApi();}} else {// 正常发送请求await originalRequest(localStorage.getItem(token));}}); };function isFiveMinutesApart(timestamp1, timestamp2) {// 计算两个时间戳的差值毫秒const difference timestamp1 - timestamp2;console.log(difference)// 5 分钟对应的毫秒数const fiveMinutesInMs 5 * 60 * 1000;// 判断差值是否等于 5 分钟对应的毫秒数return difference fiveMinutesInMs; } export { makeRequest };当Token过期时间达到范围值先触发Token刷新再将其他请求放入队列等Token刷新后再发起否则正常请求接口执行结果如图所示 仔细思考还有一个问题未解决如果用户长时间未操作此时Token已经过期了如果再次请求刷新Token接口再拦截器拦下返回没有登录该如何处理如果直接跳转登录页面根本不需要刷新Token方案。经过思考我们将刷新Token接口设置为anon匿名访问示例代码如下 Configuration public class ShiroConfig {/*** 核心安全过滤器对进入应用的请求进行拦截和过滤从而实现认证、授权、会话管理等安全功能。*/Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 配置拦截器链指定了哪些路径需要认证、哪些路径允许匿名访问MapString, Filter filters new HashMap();filters.put(jwt, new JWTFilter(redisTemplate()));shiroFilterFactoryBean.setFilters(filters);MapString, String filterChainDefinitionMap new LinkedHashMap();filterChainDefinitionMap.put(/user/login, anon);filterChainDefinitionMap.put(/user/refreshToken, anon);filterChainDefinitionMap.put(/**, jwt);shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}// 省略部分代码... }然后修改Token刷新接口只要Redis中Token没有过期清除就可以重新生成Token增加一些限制判断逻辑示例代码如下 Controller RequestMapping(value /user) public class UserController {PostMapping(/refreshToken)public ResponseEntity refreshToken(HttpServletRequest request) {String token request.getHeader(authorization);Date expiry JWTUtil.getExpiry(token);String key login_user_token_ JWTUtil.getUsername(token);// 拦截token不存在或者不匹配if (!redisTemplate.hasKey(key) || !redisTemplate.opsForValue().get(key).equals(token)) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(没有登陆);}// 正确将 Date 转换为 LocalDateTimeInstant instant expiry.toInstant();LocalDateTime dateAsLocalDateTime instant.atZone(ZoneId.systemDefault()).toLocalDateTime();LocalDateTime now LocalDateTime.now();Duration between Duration.between(now, dateAsLocalDateTime);// 间隔时间大于5分钟不生成新tokenif (between.toMinutes() 5) {// 返回原tokenMapString,Object map new HashMap();map.put(Authorization, token);map.put(ExpireTime, expiry.getTime());return ResponseEntity.ok(map);}User user userService.getOne(new QueryWrapperUser().ge(username, JWTUtil.getUsername(token)));String sign JWTUtil.sign(user.getUsername(), user.getPassword());redisTemplate.opsForValue().set(key, sign);// 返回tokenMapString,Object map new HashMap();map.put(Authorization, sign);map.put(ExpireTime, JWTUtil.getExpiry(sign).getTime());return ResponseEntity.ok(map);}// 省略部分代码... }这样即使用户长时间未操作再次请求触发刷新Token也能正常更新不会影响用户的体验。 需要注意设置为匿名访问会增加一定的安全风险尤其是可能导致恶意用户滥用该接口在实现匿名刷新时加上适当的限制如请求频率、IP 限制等。 相对于前端定时刷新Token请求拦截可以避免频繁的Token刷新请求减少不必要的网络流量。但是该方法需要后端提供Token过期字段使用本地时间判断容易被篡改增加后端维护成本增加额外的计算开销在请求非常频繁时这种开销会导致一定的延迟。 响应拦截 先发起请求当接口返回过期后先刷新Token再重新发送原始请求。 后端只需要把前面案例的有效期字段去除即可代码上没什么变化主要介绍前端js的变化当请求第一个接口返回登录过期先进行Token刷新并将请求放入队列中等待刷新完毕后再次发起请求示例代码如下 // 标记 Token 是否正在刷新 let isRefreshingToken false; // 请求队列 let requestQueue [];// 模拟后端接口刷新 Token async function refreshTokenApi() {isRefreshingToken true;try {// 这里模拟向服务器发送刷新 token 的请求const response await fetch(http://localhost:8080/user/refreshToken, {method: POST,headers: {Content-Type: application/json,Authorization: ${localStorage.getItem(token)}},// body: JSON.stringify({// // 可以传递一些必要的参数// })});const data await response.json();// 假设返回的新 token 字段名为 newToken根据实际情况修改localStorage.setItem(token, data.Authorization);localStorage.setItem(expireTime, data.ExpireTime);console.log(Token 刷新成功data);// 执行队列中的请求requestQueue.forEach(request request(localStorage.getItem(token)));requestQueue [];return data;} catch (error) {console.error(Token 刷新失败, error);requestQueue [];throw error;} finally {isRefreshingToken false;} }// 发起请求的函数 // 封装通用的请求函数 const makeRequest async (url, options {}) {return new Promise(async (resolve, reject) {const originalRequest async (token) {try {const newOptions {...options,headers: {...options.headers,Authorization: ${token}},method: GET};const response await fetch(url, newOptions);// 响应拦截const data await toData(response);if (response.ok) {if (data.body 没有登录) {if (!isRefreshingToken) {requestQueue.push((newToken) originalRequest(newToken));await refreshTokenApi();} else {requestQueue.push((newToken) originalRequest(newToken));}}} else if (!response.ok) {if (response.status 401) { // 如果返回401则直接跳转登录页面window.location.href ./login.html}throw new Error(Request failed with status ${response.status});}resolve(data);} catch (error) {reject(error);}};// 正常发送请求await originalRequest(localStorage.getItem(token));}); };function toData(data) {const contentType data.headers.get(Content-Type);if (contentType contentType.includes(application/json)) {return data.json();} else {return data.text();} } export { makeRequest };执行结果如图所示 似乎还有更好的处理方案既然接口响应Token失效了是否可以直接将新的Token生成后返回。 因为生成Token逻辑再自定义过滤器中处理为了保证过滤器的单一职责我们先修改生成Token方式使用固定密钥示例代码如下 public class JWTUtil {// 过期30分钟private static final long EXPIRE_TIME 30 * 60 * 1000;private static final String secret Zt]q5V5*MZ.WfHknK)b_;/*** 生成token签名** param username 用户名* return token字符串*/public static String sign(String username) {String token JWT.create().withClaim(username, username).withIssuedAt(new Date()) // 发行时间.withExpiresAt(new Date(System.currentTimeMillis() EXPIRE_TIME)).sign(Algorithm.HMAC256(secret));return token;}/*** 校验token是否有效** param token 生成token* param username 用户名* return*/public static boolean verify(String token, String username) {try {// 保证荷载参数一致JWTVerifier build JWT.require(Algorithm.HMAC256(secret)).withClaim(username, username).build();DecodedJWT decodedJWT build.verify(token);return true;} catch (Exception e) {e.printStackTrace();return false;}}// 省略部分代码... }在过滤器中处理Token过期后重新生成Token放入响应头中返回给前端需要设置Access-Control-Expose-Headers前端才能使用示例代码如下 public class JWTFilter extends BasicHttpAuthenticationFilter {private RedisTemplateString,Object redisTemplate;public JWTFilter(){}public JWTFilter(RedisTemplateString,Object redisTemplate){this.redisTemplate redisTemplate;}/*** 执行登录*/Overrideprotected boolean executeLogin(ServletRequest request, ServletResponse response) {String headerToken getHeaderToken(request);JWTToken jwtToken new JWTToken(headerToken);// 获取tokenString key login_user_token_JWTUtil.getUsername(headerToken);try {String redisToken (String) redisTemplate.opsForValue().get(key);if (redisToken null || !redisToken.equals(headerToken)) {getSubject(request, response).logout();// 通过状态码跳转登录httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);return false;}getSubject(request, response).login(jwtToken);} catch (IncorrectCredentialsException e) {// 生成Token并且更新RedisHttpServletResponse httpServletResponse WebUtils.toHttp(response);String newToken createToken(headerToken);httpServletResponse.setHeader(Authorization, newToken);redisTemplate.opsForValue().set(key, newToken);e.printStackTrace();return false;}return true;}/*** 对跨域提供支持*/Overrideprotected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {HttpServletRequest httpServletRequest WebUtils.toHttp(request);HttpServletResponse httpServletResponse WebUtils.toHttp(response);try {httpServletResponse.setHeader(Access-control-Allow-Origin, httpServletRequest.getHeader(Origin));httpServletResponse.setHeader(Access-Control-Allow-Methods, GET,POST,OPTIONS,PUT,DELETE);// 确保前端能获取响应头中的字段httpServletResponse.setHeader(Access-Control-Allow-Headers, httpServletRequest.getHeader(Access-Control-Request-Headers));httpServletResponse.setHeader(Access-Control-Expose-Headers,Authorization);// 跨域时会首先发送一个OPTIONS请求这里我们给OPTIONS请求直接返回正常状态if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());return false;}}catch (Exception e) {e.printStackTrace();}return super.preHandle(request, response);}private String createToken(String headerToken) {String username JWTUtil.getUsername(headerToken);return JWTUtil.sign(username);} }然后前端稍作修改直接从第一个请求中获取新Token存入localStorage然后重新请求示例代码如下 // 标记 Token 是否正在刷新 let isRefreshingToken false; // 请求队列 let requestQueue [];// 模拟后端接口刷新 api async function refreshApi() {isRefreshingToken true;try {// 执行队列中的请求requestQueue.forEach(request request(localStorage.getItem(token)));requestQueue [];} catch (error) {console.error(Token 刷新失败, error);requestQueue [];throw error;} finally {isRefreshingToken false;} }// 发起请求的函数 // 封装通用的请求函数 const makeRequest async (url, options {}) {return new Promise(async (resolve, reject) {const originalRequest async (token) {try {const newOptions {...options,headers: {...options.headers,Authorization: ${token}},method: GET};const response await fetch(url, newOptions);// 响应拦截const data await toData(response);if (response.ok) {if (data.body 没有登录) {if (!isRefreshingToken) {console.log(response.headers.get(authorization))const authorization response.headers.get(authorization);localStorage.setItem(token, authorization);// 更新tokenrequestQueue.push((newToken) originalRequest(newToken));await refreshApi();} else {requestQueue.push((newToken) originalRequest(newToken));}}} else if (!response.ok) {if (response.status 401) { // 如果返回401则直接跳转登录页面window.location.href ./login.html}throw new Error(Request failed with status ${response.status});}resolve(data);} catch (error) {reject(error);}};// 正常发送请求await originalRequest(localStorage.getItem(token));}); };function toData(data) {const contentType data.headers.get(Content-Type);if (contentType contentType.includes(application/json)) {return data.json();} else {return data.text();} } export { makeRequest };使用第一个接口返回的新Token重新发起请求执行结果如图所示 相比较于前两种方式响应拦截不需要复杂的处理没有额外字段避免不必要的判断和请求虽然会多发送一次请求但是没有单独提供Token接口并且新Token的随机性给系统的安全带来了极大的保障。
http://www.sczhlp.com/news/168861/

相关文章:

  • 顺企网吉安网站建设做网站要注意哪一点
  • 怎么宣传自己的网站推广装修房子的风格设计图软件
  • 中堂镇做网站如何下载别人网站模板
  • 环艺毕业设计代做网站百度一下主页官网
  • 详细介绍:ROS2与Unitree机器人集成指南
  • 时隔十六年的南京之旅
  • 高贵的北上广深,没有父母托举,90后很难成家
  • 中国工信备案查询网站达内网站开发视频教程
  • 视频加字幕软件app张家港网站关键词优化
  • 电子商务网站建设臧良运课后答案pc网站制作APP
  • wordpress 动漫网站创业园网站建设
  • 网站的按钮怎么做上海 建筑
  • 福州市城乡建设发展总公司网站公司建设网站的目的
  • 阿里巴巴网站中详情页怎么做网站备案 拉黑
  • 外贸网站建设排名做教育类网站
  • 张掖高端网站建设公司网站开发费用是研发费用
  • 浦江建设局网站用别人的资源做网站
  • 模板建站影响网站的优化排名wordpress点击折叠展开内容
  • 专业设计网站公司白云区是穷人区吗
  • wordpress最好的编辑器宁波seo推广费用
  • 网站建设模板 源码 特效梵克雅宝官网中国官网
  • 网站模板套用湖南岚鸿wordpress 免费中文企业主题
  • html免费模板网站正确的企业邮箱格式怎么写
  • 做单页免费模板网站中文wordpress网站模板下载
  • 国外包装设计网站wordpress如何配置
  • 1 建设网站目的是什么凡科建站后属于自己的网站吗
  • 公司网站建设外包流程图项目宣传推广方案
  • 美术网站建设钦州网站建设
  • 铁路建设网站多少网站怎么上传数据库
  • jsp怎么做网站的删除wordpress分类信息导航