shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 这篇文章介绍了如何使用Spring Boot整合Apache Shiro框架进行后端开发,包括认证和授权流程,并使用Redis存储Token以及MD5加密用户密码。

前言

一、逻辑

1. 登录逻辑

shiro 做安全权限控制。那么shiro的过滤器和数据源的处理主要是针对token的认证和授权。
而用户的密码验证则还是在service层中进行处理。

  1. 首先将用户登录的接口 /user/login 在shiro过滤器中放开,不在拦截,设置为 anon
  2. 发送post请求携带用户名和密码,进行登录。走这个请求时,在上一步已经被设置了白名单。走控制器,业务层service,在这里进行判断,用户名密码判断无误后,设置token和id,然后返回到前端。
  3. 总结一下:用户的密码登录和shiro是无关的,正常的判断即可。注意的是需要返回的数据,这里仅有id和token。token也是UUID生成的,并存在redis中。
  4. 访问其他接口时,拿着token放在请求头中,然后发请求,没有被shiro设置在白名单里的请求会被shiro拦截,拦截流程如下:
    ShiroAccessControlFilter类:isAccessAllowed():onAccessDenied(),用到了ShiroUsernamePasswordToken
    ->ShiroRealm类:doGetAuthenticationInfo方法
    ->ShiroHashedCredentialsMatcher类:doCredentialsMatch()
  5. 根据博客三,这里重写的doCredentialsMatch()方法就是 shiro的过滤器的最后一步,也是至关重要的一步。
    6. 接下来就可以去控制的接口了,如果遇到@RequiresPermissions() 注解,再去ShiroRealm类中doGetAuthorizationInfo()授权方法去授予权限即可,再去接口即可。

2. 项目目录结构

在这里插入图片描述

3. 开发逻辑

a. redis 开发工具类

redis的配置类和工具类,我就不再贴出,本博客主要写shiro的代码,代码可去GitHub上拉取下来观看

  1. config包下的RedisConfig配置类
  2. serializer包下的MyStringRedisSerializer序列化类
  3. utils包下的RedisUtil类

代码不再贴了,都在GitHub上

b. 密码加密工具类

  1. utils 包下的 PasswordUtils 工具类。
  2. PasswordEncoder密码编码类。

代码不再贴了,都在GitHub上

c. swagger配置

  1. config包下的 SwaggerConfig配置类
  2. 配置类上的启动注解 @EnableSwagger2

代码不再贴了,都在GitHub上

d. shiro开发 流程

  1. 先开发 ShiroConfig,设置自定义过滤器 ShiroAccessControlFilter,并设置过滤器的白名单
    对登录的请求设置为 anon。
  2. 开发 ShiroAccessControlFilter拦截器,这里是对 token 的简单过滤验证,并进行主体提交,提交到自定义数据域 realm。
    开发拦截器中,还要开发ShiroUsernamePasswordToken,自己实现shiro认证机制,就要重写类 UsernamePasswordToken
  3. 开发自定义realm:ShiroRealm类。数据域部分,进行授权和认证。
    认证:对token进行过滤,用户名密码登录部分还是在业务层处理
    授权:从数据库中获取,设计到表 role、permission、user_role、role_permission 四个表
  4. 开发 token 的最后过滤处理 ShiroHashedCredentialsMatcher,继承 HashedCredentialsMatcher类实现方法doCredentialsMatch()

以上四步其实就是 shiro 开发的全部,基本上都是配置式的代码。

二、shiro代码开发

1. shiroConfig.java

package com.feng.config;

import com.feng.shiro.ShiroAccessControlFilter;
import com.feng.shiro.ShiroHashedCredentialsMatcher;
import com.feng.shiro.ShiroRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    /**
     * token 的过滤
     * 自定义token 校验
     * 学习说明:其实 下面的 ShiroHashedCredentialsMatcher(自定义的) 也继承了 HashedCredentialsMatcher。
     * 需要在 CustomRealm bean 中进行设置
     * @return
     */
    @Bean(name = "shiroHashedCredentialsMatcher")
    public ShiroHashedCredentialsMatcher shiroHashedCredentialsMatcher() {
        return new ShiroHashedCredentialsMatcher();
    }

    /**
     * 登录的 认证域
     *
     * @param hashedCredentialsMatcher
     * @return
     */
    @Bean(name = "shiroRealm")
    public ShiroRealm getShiroRealm(@Qualifier("shiroHashedCredentialsMatcher") HashedCredentialsMatcher hashedCredentialsMatcher) {
        ShiroRealm shiroRealm = new ShiroRealm();
        // 自定义 处理 token 过滤
        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        return shiroRealm;
    }

    /**
     * shiro 的安全管理器
     *
     * @param shiroRealm
     * @return
     */
    @Bean(name = "securityManager")
    public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm);
        return securityManager;
    }
    /**
     * shiro 的过滤器
     * 需要了解 shiro 的权限关键字含义:
     *  anon,表示不拦截的路径
     *  authc,表示拦截的路径
     *
     *  匹配时,首先匹配 anon 的,然后最后匹配 authc
     *
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        /*
         * 自定义过滤器
         * */
        //自定义拦截器限制并发人数,参考博客:
        LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
        //用来校验token
        filtersMap.put("token", new ShiroAccessControlFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);
        /*
         * 以下为权限控制
         * */
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/user/login", "anon");
//        filterChainDefinitionMap.put("/user/test", "anon");

        // 拦截所有
        filterChainDefinitionMap.put("/**", "token,authc");

        // 没有登录的用户请求需要登录的页面时自动跳转到登录页面。 配置 shiro 默认登录界面地址,
        shiroFilterFactoryBean.setLoginUrl("/api/user/login");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 下面两个配置类 AuthorizationAttributeSourceAdvisor 和 DefaultAdvisorAutoProxyCreator,开启 shiro aop 注解 支持.
     * 使用代理方式;所以需要开启代码支持;
     * <p>
     * 如果不加 使用 @RequirePermissions 无效
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
}

2. ShiroAccessControlFilter

package com.feng.shiro;

import com.alibaba.fastjson.JSON;
import com.feng.constant.Constant;
import com.feng.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName: CustomAccessControlerFilter
 * @Description: 自定义的 token 过滤器。
 * @createTime:
 * @Author: 冯凡利
 * @UpdateUser: 冯凡利
 * @Version: 0.0.1
 */

/**
 * 这里的异常,全局异常无法处理,比较高级没有到达 方法,所以需要自己处理  try-catch
 */
@Slf4j
public class ShiroAccessControlFilter extends AccessControlFilter {
    /**
     * 是否 允许 访问下一层
     * true: 允许,交下一个Filter 处理
     * false: 交给自己处理,往下执行 onAccessDenied 方法
     * @param servletRequest
     * @param servletResponse
     * @param o
     * @return
     * @throws Exception
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        return false;
    }

    /**
     * 表示访问拒绝时是否自己处理,
     * 如果返回 true 表示自己不处理且 继续拦截器执行,往下执行
     * 返回 false 表示自己已经处理了(比如重定向到另一个界面)处理完毕。
     *
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request= (HttpServletRequest) servletRequest;
        try {
            log.info("接口请求方式{}",request.getMethod());
            log.info("接口请求地址",request.getRequestURI());
            String token=request.getHeader(Constant.TOKEN_SESSION_ID);
            if(StringUtils.isEmpty(token)){
                throw new BusinessException(4010001,"用户凭证已失效请重新登录认证");
            }
            ShiroUsernamePasswordToken customUsernamePasswordToken=new ShiroUsernamePasswordToken(token);
            getSubject(servletRequest,servletResponse).login(customUsernamePasswordToken);
        } catch (BusinessException e) {
            customResponse(e.getMessageCode(),e.getMessage(),servletResponse);
            return false;
        } catch (AuthenticationException e) {
            if(e.getCause() instanceof BusinessException){
                BusinessException businessException= (BusinessException) e.getCause();
                customResponse(businessException.getMessageCode(),businessException.getMessage(),servletResponse);
            }else {
                customResponse(4000001,"用户认证失败",servletResponse);
            }
            return false;
        }catch (Exception e){
            customResponse(5000001,"系统异常",servletResponse);
            return false;
        }
        return true;
    }

    /**
     * 异常处理
     * 因为这里的位置是高于业务层的,所以这里的异常只能通过流的形式输出到前端。
     * @param code
     * @param msg
     * @param response
     */
    private void customResponse(int code, String msg, ServletResponse response) {
        // 自定义异常的类,用户返回给客户端相应的JSON格式的信息
        try {
            Map<String, Object> result = new HashMap<>();
            result.put("code", code);
            result.put("msg", msg);
            response.setContentType("application/json; charset=utf-8");
            response.setCharacterEncoding("UTF-8");
            String userJson = JSON.toJSONString(result);
            // 写入到 流中,返回到客户端
            OutputStream out = response.getOutputStream();
            out.write(userJson.getBytes(StandardCharsets.UTF_8));
            out.flush();
        } catch (IOException e) {
            log.error("eror={}", e.getLocalizedMessage());
        }
    }
}

3. ShiroUsernamePasswordToken

package com.feng.shiro;

import org.apache.shiro.authc.UsernamePasswordToken;

public class ShiroUsernamePasswordToken extends UsernamePasswordToken {

    private String token;

    public ShiroUsernamePasswordToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

4. ShiroRealm

package com.feng.shiro;

import com.feng.bean.SysUser;
import com.feng.service.PermissionService;
import com.feng.service.RoleService;
import com.feng.service.UserService;
import com.feng.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Slf4j
public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    @Resource
    private RoleService roleService;

    @Resource
    private PermissionService permissionService;

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 设置支持令牌校验
     *
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof ShiroUsernamePasswordToken;
    }

    /**
     * 授权
     * 主要业务:
     * 系统业务出现要验证用户的角色权限的时候,就会调用这个方法
     * 来获取该用户所拥有的角色/权限
     * 这个用户授权的方法我们可以缓存起来不用每次都调用这个方法。
     * 后续的课程我们会结合 redis 实现它
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.info("ShiroRealm.doGetAuthorizationInfo()");
        String token= (String) principalCollection.getPrimaryPrincipal();
        String userId= (String) redisUtil.get(token);
        SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();

        //返回该用户的 角色信息 给授权器
        List<String> roleNames = roleService.getRoleNamesByUserId(userId);
        if (null != roleNames && !roleNames.isEmpty()) {
            info.addRoles(roleNames);
        }
        //返回该用户的 权限信息 给授权器
        Set<String> permissionPerms = permissionService.getPermissionPermsByUserId(userId);
        if (permissionPerms != null) {
            info.addStringPermissions(permissionPerms);
        }
        return info;
    }

    /**
     * 认证
     * 主要业务:
     * 当业务代码调用 subject.login(customPasswordToken); 方法后
     * 就会自动调用这个方法 验证用户名/密码
     * 这里我们改造成 验证 token 是否有效 已经自定义了 shiro 验证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("ShiroRealm.doGetAuthenticationInfo()");
        ShiroUsernamePasswordToken token = (ShiroUsernamePasswordToken) authenticationToken;
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo((String)token.getPrincipal(), (String)token.getCredentials(), ShiroRealm.class.getName());
        return info;
    }

    private List<String> getRoleByUserId(String userId){
        List<String> roles=new ArrayList<>();
        if(userId.equals("8a938151-53e6-4182-925a-684f3be840e8")){
            roles.add("admin");
        }
        roles.add("test");
        return roles;
    }

    private List<String> getPermissionsByUserId(String userId){
        List<String> permissions=new ArrayList<>();
        if(userId.equals("8a938151-53e6-4182-925a-684f3be840e8")){
            permissions.add("*");
        }
        permissions.add("sys:user:detail");
        permissions.add("sys:user:edit");
        return permissions;
    }
}

5. ShiroHashedCredentialsMatcher

public class ShiroHashedCredentialsMatcher extends HashedCredentialsMatcher {

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        ShiroUsernamePasswordToken shiroUsernamePasswordToken= (ShiroUsernamePasswordToken) token;
        String accessToken = (String) shiroUsernamePasswordToken.getPrincipal();
        if(!redisUtil.hasKey(accessToken)){
            throw new BusinessException(4001002,"授权信息信息无效请重新登录");
        }
        return true;
    }
}

三、业务逻辑控制层代码

package com.feng.controller;

import com.feng.bean.SysUser;
import com.feng.service.UserService;
import com.feng.vo.LoginReqVO;
import com.feng.vo.LoginRespVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController
@RequestMapping("/user")
@Api(tags = "用户模块",description = "用户模块相关接口")
public class LoginController {

    @Autowired
    private UserService userService;

    @GetMapping("/page")
    public String index() {
        return "login";
    }

    /**
     * 前端用表单发请求如果使用 form-data、x-www-form-urlencoded 获取 ,则不可用 @RequestBody接受(因为他接受的为json)
     * @param loginReqVO
     * @return
     */
    @ApiOperation(value = "用户登录接口")
    @PostMapping(value = "/login")
    @ResponseBody
    public Map<String, Object> loginUser(@RequestBody LoginReqVO loginReqVO) {
        LoginRespVO info = userService.login(loginReqVO);
        Map<String, Object> result = new HashMap<>();
        result.put("code", 0);
        result.put("data", info);
        return result;
    }

    @ApiOperation(value = "获取用户详情接口")
    @GetMapping("/getuser/{id}")
    @RequiresPermissions("sys:user:detail")
    public Map<String, Object> getUserAllInfo(@PathVariable("id") String id){
        Map<String, Object> result = new HashMap<>();
        SysUser detail = userService.detail(id);
        result.put("code", 0);
        result.put("data", detail);
        return result;
    }

    @GetMapping("/test")
    public Map<String, Object> test(){
        Map<String, Object> result = new HashMap<>();
        result.put("code", 0);
        result.put("data", "sucess");
        return result;
    }
}

五、postman测试和debug分析

1. 登录

登录的URL 在 ShiroConfig.shiroFilterFactoryBean() 方法中已经被设置成了白名单,在shiro的各处打了断电,也不会进入。直接进入到控制器层返回用户id和token。

http://localhost:8082/user/login
{
    "username": "feng",
    "password": "666666"
}

在这里插入图片描述

2. 获取用户信息

此URL 已经被设置走自定义的 ShiroAccessControlFilter 过滤器。
所以debug发请求分析如下:

  1. 先进入 ShiroAccessControlFilter.isAccessAllowed() 方法
  2. 进入到源码 AccessControlFilter.onPreHandle() 方法,这个方法会调用上面的方法和下面的方法
  3. 在进入到 ShiroAccessControlFilter.onAccessDenied() 方法,在这里获取token,并简单验证token是否存在,然后进行 shiro的主体登录。(一会儿,还会返回来)
  4. 主体登录后,debug 跳转到 ShiroRealm.doGetAuthenticationInfo() 进行认证。
  5. 然后流转到 自定义的核心验证类和方法ShiroHashedCredentialsMatcher.doCredentialsMatch() 方法 。
  6. 然后返回到第三步 的 ShiroAccessControlFilter.onAccessDenied() 方法 的最后一行返回值,返回值为 true。
  7. 然后流转到 ShiroRealm.doGetAuthorizationInfo() 进行授权。这里进入到授权方法是因为在控制器方法上有注解:@RequiresPermissions("sys:user:detail")
http://localhost:8082/user/getuser/8a938151-53e6-4182-925a-684f3be840e8

在这里插入图片描述

六、注意的点

1. 数据库表的设计

用户登录只涉及到 user 表
shiro的权限授权部分涉及到 role、permission、user_role、role_permission 表。

2. shiro的认证授权流程

一定要多分析下shiro的认证流程,代码执行流程的走向。

相关文章
|
19天前
|
NoSQL Java API
springboot项目Redis统计在线用户
通过本文的介绍,您可以在Spring Boot项目中使用Redis实现在线用户统计。通过合理配置Redis和实现用户登录、注销及统计逻辑,您可以高效地管理在线用户。希望本文的详细解释和代码示例能帮助您在实际项目中成功应用这一技术。
27 3
|
21天前
|
消息中间件 NoSQL Java
Spring Boot整合Redis
通过Spring Boot整合Redis,可以显著提升应用的性能和响应速度。在本文中,我们详细介绍了如何配置和使用Redis,包括基本的CRUD操作和具有过期时间的值设置方法。希望本文能帮助你在实际项目中高效地整合和使用Redis。
39 1
|
2月前
|
安全 Java 数据库
shiro学习一:了解shiro,学习执行shiro的流程。使用springboot的测试模块学习shiro单应用(demo 6个)
这篇文章是关于Apache Shiro权限管理框架的详细学习指南,涵盖了Shiro的基本概念、认证与授权流程,并通过Spring Boot测试模块演示了Shiro在单应用环境下的使用,包括与IniRealm、JdbcRealm的集成以及自定义Realm的实现。
46 3
shiro学习一:了解shiro,学习执行shiro的流程。使用springboot的测试模块学习shiro单应用(demo 6个)
|
2月前
|
缓存 NoSQL Java
Spring Boot与Redis:整合与实战
【10月更文挑战第15天】本文介绍了如何在Spring Boot项目中整合Redis,通过一个电商商品推荐系统的案例,详细展示了从添加依赖、配置连接信息到创建配置类的具体步骤。实战部分演示了如何利用Redis缓存提高系统响应速度,减少数据库访问压力,从而提升用户体验。
92 2
|
27天前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
36 0
|
2月前
|
安全 Unix 数据安全/隐私保护
企业级 文件传输加密应用,干货分享
企业级 文件传输加密应用,干货分享
21 0
|
2月前
|
机器学习/深度学习 移动开发 自然语言处理
基于人工智能技术的智能导诊系统源码,SpringBoot作为后端服务的框架,提供快速开发,自动配置和生产级特性
当身体不适却不知该挂哪个科室时,智能导诊系统应运而生。患者只需选择不适部位和症状,系统即可迅速推荐正确科室,避免排错队浪费时间。该系统基于SpringBoot、Redis、MyBatis Plus等技术架构,支持多渠道接入,具备自然语言理解和多输入方式,确保高效精准的导诊体验。无论是线上医疗平台还是大型医院,智能导诊系统均能有效优化就诊流程。
|
Java Spring
SpringBoot 开发秘籍 - 启动时配置校验
在项目开发过程中,某个功能需要依赖在配置文件中配置的参数。
193 0
SpringBoot 开发秘籍 - 启动时配置校验
|
2月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
162 1
|
1月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,创建并配置 Spring Boot 项目,实现后端 API;然后,使用 Ant Design Pro Vue 创建前端项目,配置动态路由和菜单。通过具体案例,展示了如何快速搭建高效、易维护的项目框架。
104 62

热门文章

最新文章