springboot整合shiro+jwt+redis详解

这篇具有很好参考价值的文章主要介绍了springboot整合shiro+jwt+redis详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

原理

三大核心组件:Subject、SecurityManager、Realm

springboot整合shiro+jwt+redis详解

  • Subject
    主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;
  • SecurityManager
    安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;
  • Realm
    域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。

    总结:
    应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
    我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。

内部架构图:
springboot整合shiro+jwt+redis详解

整合

在springboot中整合shiro、redis和jwt,核心的配置:ShiroConfig、JwtFilter、ShiroRealm,其中jwt主要是负责生成token的工具,redis负责缓存token。
首先我们配置Realm,然后配置filter及jwt工具类,再用shiroConfig来将这些配置联系起来,组成完整的认证鉴权系统。

准备工作

springboot版本

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.5.RELEASE</version>
    <relativePath/>
</parent>

jwt和shiro版本

<!--JWT-->
     <dependency>
         <groupId>com.auth0</groupId>
         <artifactId>java-jwt</artifactId>
         <version>3.11.0</version>
     </dependency>
     <!--shiro-->
     <dependency>
         <groupId>org.apache.shiro</groupId>
         <artifactId>shiro-spring-boot-starter</artifactId>
         <version>1.7.1</version>
     </dependency>
     <dependency>
         <groupId>org.crazycake</groupId>
         <artifactId>shiro-redis</artifactId>
         <version>3.1.0</version>
         <exclusions>
             <exclusion>
                 <groupId>org.apache.shiro</groupId>
                 <artifactId>shiro-core</artifactId>
             </exclusion>
             <exclusion>
                 <groupId>com.puppycrawl.tools</groupId>
                 <artifactId>checkstyle</artifactId>
             </exclusion>
         </exclusions>
     </dependency>

各项配置

  • ShiroRealm
    主要负责认证(AuthenticationInfo)和鉴权(AuthorizationInfo)代码逻辑的实现。

    /**
    * 认证
    *
    * @author zwj
    */
    @Slf4j
    @Component
    public class ShiroRealm extends AuthorizingRealm {
    
       @Autowired
       private IUserService userService;
    
       @Autowired
       private StringRedisTemplate stringRedisTemplate;
    
       /**
        * 必须重写此方法,不然Shiro会报错
        */
       @Override
       public boolean supports(AuthenticationToken token) {
           return token instanceof JwtToken;
       }
    
       /**
        * 授权(验证权限时调用)
        */
       @Override
       protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
           SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
           return info;
       }
    
       /**
        * 认证(登录时调用)
        */
       @Override
       protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
           String accessToken = (String) token.getPrincipal();
           if (accessToken == null) {
               throw new AuthenticationException(CommonCode.WEB_TOKEN_NULL.getMessage());
           }
           // 校验token有效性
           User tokenEntity = this.checkUserTokenIsEffect(accessToken);
           SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(tokenEntity, accessToken, getName());
           return info;
       }
    
       /**
        * 校验token的有效性
        *  springboot2.3.+新增了一个配置项server.error.includeMessage,默认是NEVER,
        *  因此默认是不是输出message的,只要开启就可以了,否则无法拿到shiro抛出异常信息message
        * @param token
        */
       public User checkUserTokenIsEffect(String token) throws AuthenticationException {
           // 解密获得username,用于和数据库进行对比
           String userId = JwtUtil.getUserId(token);
           if (userId == null) {
               throw new AuthenticationException(CommonCode.WEB_TOKEN_ILLEGAL.getMessage());
           }
    
           // 查询用户信息
           User loginUser = userService.getById(userId);
           if (loginUser == null) {
               throw new AuthenticationException(CommonCode.WEB_USER_NOT_EXIST.getMessage());
           }
           // 判断用户状态
           if (loginUser.getStatus() != 0) {
               throw new LockedAccountException(CommonCode.WEB_ACCOUNT_LOCKED.getMessage());
           }
           // 校验token是否超时失效 & 或者账号密码是否错误
           if (!jwtTokenRefresh(token, userId, loginUser.getUserPhone())) {
               throw new IncorrectCredentialsException(CommonCode.WEB_TOKEN_FAILURE.getMessage());
           }
           return loginUser;
       }
    
       /**
        * JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
        * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
        * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
        * 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
        * 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
        * 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
        * 用户过期时间 = Jwt有效时间 * 2。
        *
        * @param userId
        * @param userPhone
        * @return
        */
       public boolean jwtTokenRefresh(String token, String userId, String userPhone) {
           //如果缓存中的token为空,直接返回失效异常
           String cacheToken = stringRedisTemplate.opsForValue().get(CommonConstant.PREFIX_USER_TOKEN + token);
           if (!StrUtils.isBlank(cacheToken)) {
               // 校验token有效性
               if (!JwtUtil.verify(cacheToken, userId, userPhone)) {
                    JwtUtil.sign(userId, userPhone);
               }
               return true;
           }
           return false;
       }
    
       /**
        * 清除当前用户的权限认证缓存
        *
        * @param principals 权限信息
        */
       @Override
       public void clearCache(PrincipalCollection principals) {
           super.clearCache(principals);
       }
    }
    
  • JwtFilter
    这里会拦截需要认证和鉴权的请求,同时会捕获相应异常并抛出

    /**
     * 过滤器
     *
     * @author zwj
     */
    public class JwtFilter extends BasicHttpAuthenticationFilter {
    
        /**
         * 功能描述: 执行登录认证
         *
         * @param request
         * @param response
         * @param mappedValue
         * @return boolean
         * @author zhouwenjie
         * @date 2021/12/24 14:45
         */
        @SneakyThrows
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            try {
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                throw new AuthenticationException(e.getMessage(), e);
            }
        }
    
        @Override
        protected boolean executeLogin(ServletRequest request, ServletResponse response) {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            String token = JwtUtil.getTokenByRequest(httpServletRequest);
            JwtToken jwtToken = new JwtToken(token);
            // 提交给realm进行登入,如果错误他会抛出异常并被捕获
            getSubject(request, response).login(jwtToken);
            // 如果没有抛出异常则代表登入成功,返回true
            return true;
        }
    
        /**
         * 对跨域提供支持
         */
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
            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"));
            // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
            if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
                httpServletResponse.setStatus(HttpStatus.OK.value());
                return false;
            }
            return super.preHandle(request, response);
        }
    
    }
    

    JwtToken

    /**
     * token
     *
     * @author Mark sunlightcs@gmail.com
     */
    public class JwtToken implements AuthenticationToken {
        private static final long serialVersionUID = 1L;
        private String token;
    
        public JwtToken(String token){
            this.token = token;
        }
    
        @Override
        public String getPrincipal() {
            return token;
        }
    
        @Override
        public Object getCredentials() {
            return token;
        }
    }
    

    JwtUtil:token工具类

    
    /**
     * @Author zwj
     * @Desc JWT工具类
     **/
    public class JwtUtil {
    
        // Token过期时间180天(用户登录过期时间是此时间的两倍,以token在reids缓存时间为准)
        public static final long EXPIRE_TIME = 24 * 180 * 60 * 60 * 1000;
        public static final int days = 360;
        private static StringRedisTemplate stringRedisTemplate = SpringContextUtils.getBean(StringRedisTemplate.class);
    
        /**
         * 校验token是否正确
         *
         * @param token     密钥
         * @param userPhone 用户的密码
         * @return 是否正确
         */
        public static boolean verify(String token, String userId, String userPhone) {
            try {
                // 根据密码生成JWT效验器
                Algorithm algorithm = Algorithm.HMAC256(userPhone);
                JWTVerifier verifier = JWT.require(algorithm).withClaim("userId", userId).build();
                // 效验TOKEN
                verifier.verify(token);
                return true;
            } catch (Exception exception) {
                return false;
            }
        }
    
        /**
         * 获得token中的信息无需secret解密也能获得
         *
         * @return token中包含的用户名
         */
        public static String getUserId(String token) {
            try {
                DecodedJWT jwt = JWT.decode(token);
                return jwt.getClaim("userId").asString();
            } catch (Exception e) {
                return null;
            }
        }
    
        /**
         * 生成签名,360天后过期
         *
         * @param userId    用户id
         * @param userPhone 用户的密码
         * @return 加密的token
         */
        public static String sign(String userId, String userPhone) {
    //        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(userPhone);
            // 附带userId信息  可以将user信息转成map存到这里
    //        String token = JWT.create().withClaim("userId", userId).withExpiresAt(date).sign(algorithm);
            String token = JWT.create().withClaim("userId", userId).sign(algorithm);
            stringRedisTemplate.opsForValue().set(CommonConstant.PREFIX_USER_TOKEN + token, token, days, TimeUnit.DAYS);
            return token;
    
        }
    
        /**
         * 根据request中的token获取用户账号
         *
         * @param request
         * @return
         */
        public static String getUserIdByToken(HttpServletRequest request) {
            String accessToken = getTokenByRequest(request);
            String userId = getUserId(accessToken);
            return userId;
        }
    
        /**
         * 获取 request 里传递的 token
         *
         * @param request
         * @return
         */
        public static String getTokenByRequest(HttpServletRequest request) {
            String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
            return token;
        }
    }
    

    过期时间根据自己需求设定。

  • ShiroConfig
    整合各项配置的联系,注意新版本和老版本的配置区别,新版本需要重新注入beanDefaultAdvisorAutoProxyCreator、AuthorizationAttributeSourceAdvisor,原因代码中也有详细注释。文章来源地址https://www.toymoban.com/news/detail-406916.html

    /**
     * Shiro配置
     *
     * @author zwj
     */
    @Configuration
    public class ShiroConfig {
    
        @Bean("shiroFilter")
        public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
            ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
            shiroFilter.setSecurityManager(securityManager);
    
    
            Map<String, String> filterMap = new LinkedHashMap<>();
            filterMap.put("/web/login/**", "anon");
            filterMap.put("/web/carOwner/list", "anon");
            filterMap.put("/web/passengers/list", "anon");
            filterMap.put("/web/user/sendSms", "anon");
            filterMap.put("/web/sysDictionary/queryByIds", "anon");
            filterMap.put("/web/user/addActive", "anon");
            filterMap.put("/web/sysNotice/list", "anon");
            filterMap.put("/web/sysAds/addViewNum", "anon");
            filterMap.put("/web/sysAds/list", "anon");
            filterMap.put("/web/sysAds/queryById", "anon");
            filterMap.put("/web/sysArea/list", "anon");
            //-------防止api文档被过滤掉
            filterMap.put("/doc.html", "anon");
            filterMap.put("/**/*.js", "anon");
            filterMap.put("/**/*.css", "anon");
            filterMap.put("/**/*.html", "anon");
            filterMap.put("/**/*.svg", "anon");
            filterMap.put("/**/*.pdf", "anon");
            filterMap.put("/**/*.jpg", "anon");
            filterMap.put("/**/*.png", "anon");
            filterMap.put("/**/*.ico", "anon");
            filterMap.put("/swagger-resources/**", "anon");
            filterMap.put("/v2/api-docs", "anon");
            filterMap.put("/v2/api-docs-ext", "anon");
            filterMap.put("/webjars/**", "anon");
            filterMap.put("/druid/**", "anon");
            filterMap.put("/", "anon");
            //=======防止api文档被过滤掉
            filterMap.put("/**", "jwt");
            //jwt过滤
            Map<String, Filter> filters = new HashMap<>();
            filters.put("jwt", new JwtFilter());
            shiroFilter.setFilters(filters);
            shiroFilter.setFilterChainDefinitionMap(filterMap);
    
            return shiroFilter;
        }
    
        /**
         * 功能描述: 注入realm进行安全管理
         *
         * @param shiroRealm
         * @return org.apache.shiro.web.mgt.DefaultWebSecurityManager
         * @author zhouwenjie
         * @date 2021/5/5 23:09
         */
        @Bean("securityManager")
        public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm,RedisProperties redisProperties) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(shiroRealm);
            //关闭shiro自带的session存放token功能
            DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            securityManager.setSubjectDAO(subjectDAO);
            //使用redis设置自定义缓存token
            securityManager.setCacheManager(redisCacheManager(redisProperties));
            return securityManager;
        }
    
        /**
         * cacheManager 缓存 redis实现
         * 使用的是shiro-redis开源插件
         *
         * @return
         */
        public RedisCacheManager redisCacheManager(RedisProperties redisProperties) {
            RedisCacheManager redisCacheManager = new RedisCacheManager();
            redisCacheManager.setRedisManager(redisManager(redisProperties));
            //redis中针对不同用户缓存(此处的id需要对应user实体中的userId字段,用于唯一标识)
            redisCacheManager.setPrincipalIdFieldName("id");
            //用户权限信息缓存时间
            redisCacheManager.setExpire(200000);
            return redisCacheManager;
        }
    
        /**
         * 配置shiro redisManager
         * 使用的是shiro-redis开源插件
         *
         * @return
         */
        @Bean
        public RedisManager redisManager(RedisProperties redisProperties) {
            RedisManager redisManager = new RedisManager();
            redisManager.setHost(redisProperties.getHost());
            redisManager.setPort(redisProperties.getPort());
            redisManager.setTimeout(0);
            if (!StringUtils.isEmpty(redisProperties.getPassword())) {
                redisManager.setPassword(redisProperties.getPassword());
            }
            return redisManager;
            /*IRedisManager manager;
            // redis 单机支持,在集群为空,或者集群无机器时候使用 add by jzyadmin@163.com
            if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
                RedisManager redisManager = new RedisManager();
                redisManager.setHost(lettuceConnectionFactory.getHostName());
                redisManager.setPort(lettuceConnectionFactory.getPort());
                redisManager.setTimeout(0);
                if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) {
                    redisManager.setPassword(lettuceConnectionFactory.getPassword());
                }
                manager = redisManager;
            }else{
                // redis 集群支持,优先使用集群配置	add by jzyadmin@163.com
                RedisClusterManager redisManager = new RedisClusterManager();
                Set<HostAndPort> portSet = new HashSet<>();
                lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort())));
                JedisCluster jedisCluster = new JedisCluster(portSet);
                redisManager.setJedisCluster(jedisCluster);
                manager = redisManager;
            }
            return manager;*/
        }
    
        @Bean("lifecycleBeanPostProcessor")
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
    
        /**
         *功能描述: 高版本shrio增加配置,否则类里方法上有@RequiresPermissions注解的,会导致整个类下的接口无法访问404
         * @author zhouwenjie
         * @date 2021/12/29 9:08
         * @param
         * @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
         */
        @Bean
        public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            advisorAutoProxyCreator.setProxyTargetClass(true);
            return advisorAutoProxyCreator;
        }
    
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
            advisor.setSecurityManager(securityManager);
            return advisor;
        }
    
    }
    

运用

/**
	 * 保存用户
	 */
	@ApiOperation(value = "保存用户", notes = "保存用户")
	@SysLog("保存用户")
	@PostMapping("/save")
	@RequiresPermissions("sys:user:save")
	public Result save(@RequestBody SysUserEntity user){
		ValidatorUtils.validateEntity(user, ValidGroups.AddGroup.class);
		
		user.setCreateUserId(getUserId());
		sysUserService.saveUser(user);
		
		return Result.ok();
	}

到了这里,关于springboot整合shiro+jwt+redis详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • SpringBoot与Shiro整合

    Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。 Authentication 认证 ---- 用户登录 Authorization 授权 ---- 用户具有哪些权限 C

    2024年02月08日
    浏览(33)
  • 详谈 springboot整合shiro

    上文学习了shrio 基本概念后,本章将进一步的落地实践学习,在springboot中如何去整合shrio,整个过程步骤有个清晰的了解。   1. 添加依赖:首先,在 pom.xml 文件中添加Spring Boot和Shiro的相关依赖。 2. 创建Shiro配置类:创建一个 ShiroConfig 类,用于配置Shiro的相关信息和组件。(

    2024年02月12日
    浏览(37)
  • Shiro整合SpringBoot项目实战

    ✅作者简介:2022年 博客新星 第八 。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏:SpringBoot 框架从入门到精通 ✨特色专栏:国学周更-心性养成之路 🥭本文内容:Shiro整合Sp

    2023年04月09日
    浏览(52)
  • 【微服务】springboot整合redis哨兵集群使用详解

    目录 一、前言 二、环境准备 三、安装redis 3.1 前置准备 3.1.1 下载安装包

    2024年02月14日
    浏览(43)
  • java之路 —— Shiro与Springboot整合开发

    在 Spring Boot 中做权限管理,一般来说,主流的方案是 Spring Security ,但是,仅仅从技术角度来说,也可以使用 Shiro。 在 Spring Boot 中做权限管理,一般来说,主流的方案是 Spring Security ,但是,仅仅从技术角度来说,也可以使用 Shiro。 在整合之前,让我们先来了解一下Shiro开发

    2024年02月11日
    浏览(41)
  • Shiro整合SpringBoot,实战下的应用场景

    整合springBoot+shiro流程: 环境准备 身份认证 2.1 密码加密 2.2 非法请求控制 授权,鉴权 项目目录: 认证框架五表设计: 准备 user用户表 、user_role用户角色关系表、 role角色表 、 role_permission角色权限关系表、 permission权限表 ; user用户表: user_role用户角色关系表: role角色表

    2024年02月09日
    浏览(44)
  • shiro 整合 spring 实战及源码详解

    前面我们学习了如下内容: 5 分钟入门 shiro 安全框架实战笔记 shiro 整合 spring 实战及源码详解 相信大家对于 shiro 已经有了最基本的认识,这一节我们一起来学习写如何将 shiro 与 spring 进行整合。 定义一个简单的服务类,用于演示 @RequiresPermissions 注解的权限校验。 我们对原

    2024年02月22日
    浏览(38)
  • 【SpringBoot整合JWT】

    目录 一、什么是JWT 二、JWT能做什么  三、为什么是JWT  1、基于传统的Session认证 2、基于JWT认证 四、JWT的结构是什么  五、JWT的第一个程序 六、封装JWT工具类  七、整合SpringBoot使用 JSON Web Token (JWT) is an open standard ([RFC 7519](https://tools.ietf.org/html/rfc7519)) that defines a compact and

    2024年02月11日
    浏览(29)
  • SpringBoot+Shiro框架整合实现前后端分离的权限管理基础Demo

    记录一下使用SpringBoot集成Shiro框架实现前后端分离Web项目的过程,后端使用SpringBoot整合Shiro,前端使用vue+elementUI,达到前后端使用token来进行交互的应用,这种方式通常叫做无状态,后端只需要使用Shiro框架根据前端传来的token信息授权访问相应资源。 案例源码:SpringBoot+Sh

    2024年02月06日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包