zoukankan      html  css  js  c++  java
  • 开源一套极简的前后端分离项目脚手架

      前言

      Fast Scaffold是一套极简的前后端分离项目脚手架,包含一个portal前端、一个admin后端,可用于快速的搭建前后端分离项目进行二次开发

      技术栈

      portal前端:vue + element-ui + avue,使用typescript语法编码

      admin后端:springboot + mybatis-plus + mysql,采用jwt进行身份认证

      项目结构

      

      portal前端

      前端项目,使用的是我们:Vue项目入门实例,在此基础上做了一下跳转

      引入avue

      avue,基于element-ui开发的一个很多骚操作的前端框架,我们也在test测试模块中的Admin页面中进行了简单测试

      官网:https://avuejs.com/

      router配置

      router路由配置,新增test模块菜单路由,beforeEach中判断无令牌,跳转登录页面

    router.beforeEach(async(to, from, next) => {
        console.log("跳转开始,目标:"+to.path);
        document.title = `${to.meta.title}`;
    
        //无令牌,跳转登录页面
        if (to.name !== 'Login' && !TokenUtil.getToken()){
            console.log("无令牌,跳转登录页面");
            next({ name: 'Login' });
        }
    
        //跳转页面
        next();
    });

      store配置

      store配置,新增user属性,getters提供getUser方法,以及mutations、actions的setUser方法

    import Vue from 'vue'
    import Vuex from 'vuex'
    import User from "@/vo/user";
    import CommonUtil from "@/utils/commonUtil";
    import {Object} from  "@/utils/commonUtil"
    import AxiosUtil from "@/utils/axiosUtil";
    import TokenUtil from "@/utils/tokenUtil";
    import SessionStorageUtil from "@/utils/sessionStorageUtil";
    
    Vue.use(Vuex);
    
    /*
      约定,组件不允许直接变更属于 store 实例的 state,而应执行 action 来分发 (dispatch) 事件通知 store 去改变
     */
    export default new Vuex.Store({
      state: {
        user:User,
      },
      getters:{
        getUser: state => {
          return state.user;
        }
      },
      mutations: {
        SET_USER: (state, user) => {
          state.user = user;
        }
      },
      actions: {
        async setUser({commit}){
          let thid = this;
          console.log("调用getUserByToken接口获取登录用户!");
          AxiosUtil.post(CommonUtil.getAdminUrl()+"/getUserByToken",{token:TokenUtil.getToken()},function (result) {
            let data = result.data as Object;
            commit('SET_USER', new User(data.id,data.username));
    
            //设置到sessionStorage
            SessionStorageUtil.setItem("loginUser",thid.getters.getUser);
          });
        }
      },
      modules: {
      }
    })

      工具类封装

      axiosUtil.ts

      设置全局withCredentials,timeout

      设置request拦截,在请求头中设置token令牌

      设置response拦截,设置了统一响应异常消息提示以及令牌无效时跳转登录页面

      封装了post、get等静态方法,方便调用

      commonUtil.ts

      封装了一下常用、通用方法,比如获取后端服务地址、获取登录用户等

      sessionStorageUtil.ts

      封装sessionStorage会话级缓存,方便设置缓存

      tokenUtil.ts

      封装token令牌工具类,方便设置token令牌到cookie

      admin后端

      后端项目,使用的是我们的:SpringBoot系列——MyBatis-Plus整合封装,在此基础上进行了调整

      只保留tb_user表模块,其他表以及代码模块都不需要,密码改成MD5加密存储

      配置文件

    server:
      port: 10086
    spring:
      application:
        name: admin
      datasource: #数据库相关
        url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&characterEncoding=utf-8
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
      mvc:
        format:
          date: yyyy-MM-dd HH:mm:ss
    
      jackson:
        date-format: yyyy-MM-dd HH:mm:ss #jackson对响应回去的日期参数进行格式化
        time-zone: GMT+8
    
    portal:
      url: http://172.16.35.52:10010 #前端地址(用于跨域配置)
    
    token:
      secret: huanzi-qch #token加密私钥(很重要,注意保密)
      expire:
        time: 86400000 #token有效时长,单位毫秒 24*60*60*1000

      cors安全跨域

      创建MyConfiguration,开启cors安全跨域,详情可看回我们之前的博客:SpringBoot系列——CORS(跨源资源共享)

    @Configuration
    public class MyConfiguration {
    
        @Value("${portal.url}")
        private String portalUrl;
    
        @Bean
        public WebMvcConfigurer corsConfigurer() {
            return new WebMvcConfigurer() {
                @Override
                public void addCorsMappings(CorsRegistry registry) {
                    registry.addMapping("/**")
                            .allowedOrigins(portalUrl)
                            .allowedMethods("*")
                            .allowedHeaders("*")
                            .allowCredentials(true).maxAge(3600);
                }
            };
        }
    }

      jwt身份认证

      maven引入jwt依赖

            <!-- JWT -->
            <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>3.5.0</version>
            </dependency>

      JwtUtil工具类,封装生成token,校验token,以及根据token获取登录用户

    /**
     * JWT工具类
     */
    @Component
    public class JwtUtil {
    
        /**
         * 过期时间,毫秒
         */
        private static long TOKEN_EXPIRE_TIME;
        @Value("${token.expire.time}")
        public void setExpireTime(long expireTime) {
            JwtUtil.TOKEN_EXPIRE_TIME = expireTime;
        }
    
        /**
         * token私钥
         */
        private static String TOKEN_SECRET;
        @Value("${token.secret}")
        public void setSecret(String secret) {
            JwtUtil.TOKEN_SECRET = secret;
        }
    
        /**
         * 生成签名
         */
        public static String sign(String userId){
            //过期时间
            Date date = new Date(System.currentTimeMillis() + TOKEN_EXPIRE_TIME);
            //私钥及加密算法
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            //设置头信息
            HashMap<String, Object> header = new HashMap<>(2);
            header.put("typ", "JWT");
            header.put("alg", "HS256");
            //附带userID生成签名
            return JWT.create().withHeader(header).withClaim("userId",userId).withExpiresAt(date).sign(algorithm);
        }
    
        /**
         * 验证签名
         */
        public static boolean verity(String token){
            //令牌为空
            if(StringUtils.isEmpty(token)){
                return false;
            }
    
            try {
                Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
                JWTVerifier verifier = JWT.require(algorithm).build();
    
                //是否能解密
                DecodedJWT jwt = verifier.verify(token);
    
                //校验过期时间
                if(new Date().after(jwt.getExpiresAt())){
                    return false;
                }
    
                return true;
            } catch (IllegalArgumentException | JWTVerificationException e) {
                ErrorUtil.errorInfoToString(e);
            }
            return false;
        }
    
        /**
         * 根据token获取用户id
         */
        public static String getUserIdByToken(String token){
            try {
                Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
                JWTVerifier verifier = JWT.require(algorithm).build();
                DecodedJWT jwt = verifier.verify(token);
                return jwt.getClaim("userId").asString();
            } catch (IllegalArgumentException | JWTVerificationException e) {
                ErrorUtil.errorInfoToString(e);
            }
            return null;
        }
    }

      登录拦截器

      LoginFilter登录拦截器,不拦截登录请求、跨域预检请求,其他请求全部拦截校验是否有令牌

      PS:我们已经配置了全局安全跨域,但在拦截器中,PrintWriter.print回去的response,要手动添加一下响应头标记允许对方跨域

    //标记当前请求对方允许跨域访问
    response.setHeader("Access-Control-Allow-Credentials","true");
    response.setHeader("Access-Control-Allow-Headers","content-type, token");
    response.setHeader("Access-Control-Allow-Methods","*");
    response.setHeader("Access-Control-Allow-Origin",portalUrl);
    /**
     * 登录拦截器
     */
    @Component
    public class LoginFilter implements Filter {
    
        @Value("${server.servlet.context-path:}")
        private String contextPath;
    
        @Value("${portal.url}")
        private String portalUrl;
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            String method = request.getMethod();
    
            //不拦截登录请求、跨域预检请求,其他请求全部拦截校验是否有令牌
            if (!"/login".equals(request.getRequestURI().replaceFirst(contextPath,"")) && !"options".equals(method.toLowerCase())) {
                String token = request.getHeader("token");
    
                //验证签名
                if(!JwtUtil.verity(token)){
                    String dataString = "{"status":401,"message":"无效token令牌,访问失败,请重新登录系统!"}";
    
                    //清除cookie
                    Cookie cookie = new Cookie("PORTAL_TOKEN", null);
                    cookie.setPath("/");
                    cookie.setMaxAge(0);
                    response.addCookie(cookie);
    
                    //转json字符串并转成Object对象,设置到Result中并赋值给返回值,记得表明当前页面可以跨域访问
                    response.setHeader("Access-Control-Allow-Credentials","true");
                    response.setHeader("Access-Control-Allow-Headers","content-type, token");
                    response.setHeader("Access-Control-Allow-Methods","*");
                    response.setHeader("Access-Control-Allow-Origin",portalUrl);
    
                    response.setCharacterEncoding("UTF-8");
                    response.setContentType("application/json; charset=utf-8");
                    PrintWriter out = response.getWriter();
                    out.print(dataString);
                    out.flush();
                    out.close();
    
                    return;
                }
            }
    
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }

      简单控制器

      IndexController控制器,提供三个post方法:login登录,logout登出,getUserByToken通过token令牌获取登录用户

    @RestController
    @RequestMapping("/")
    @Slf4j
    public class IndexController {
    
        @Autowired
        private TbUserService tbUserService;
    
        /**
         * 登录
         */
        @PostMapping("login")
        public Result<String> login(@RequestBody TbUserVo entityVo){
            //只关注用户名、密码
            if(StringUtils.isEmpty(entityVo.getUsername()) || StringUtils.isEmpty(entityVo.getPassword())){
                return Result.build(400,"账号或密码不能为空......","");
            }
            TbUserVo tbUserVo = new TbUserVo();
            tbUserVo.setUsername(entityVo.getUsername());
            //密码MD5加密后密文存储,匹配时先MD5加密后匹配
            tbUserVo.setPassword(MD5Util.getMD5(entityVo.getPassword()));
            Result<List<TbUserVo>> listResult = tbUserService.list(tbUserVo);
            if(Result.OK.equals(listResult.getStatus()) && listResult.getData().size() > 0){
                TbUserVo userVo = listResult.getData().get(0);
    
                //token
                String token = JwtUtil.sign(userVo.getId()+"");
    
                return Result.build(Result.OK,"登录成功!",token);
            }
            return Result.build(400,"账号或密码错误...","");
        }
    
        /**
         * 登出
         */
        @PostMapping("logout")
        public Result<String> logout(HttpServletResponse response){
            //清除cookie
            Cookie cookie = new Cookie("PORTAL_TOKEN", null);
            cookie.setPath("/");
            cookie.setMaxAge(0);
            response.addCookie(cookie);
            return Result.build(Result.OK,"此路是我开,此树是我栽,要从此路过,留下token令牌!","");
        }
    
        /**
         * 通过token令牌获取登录用户
         */
        @PostMapping("getUserByToken")
        public Result<TbUserVo> getUserByToken(@RequestBody TbUserVo entityVo){
            String userId = JwtUtil.getUserIdByToken(entityVo.getToken());
            Result<TbUserVo> result = tbUserService.get(userId);
            result.getData().setPassword(null);
            return userId == null ? Result.build(500,"操作失败!",new TbUserVo()) : result;
        }
    }

      效果演示

      登录

      这是一个极简登录页面、登录功能,没用令牌,路由会拦截跳到登录页面

      登录成功后保存token令牌到cookie中,并获取登录用户信息,保存到Store中

      为了解决刷新页面Store数据丢失,同时要保存一份数据到sessionStorage缓存,在读取Store无数据时,先读取缓存,如果存在,再设置回Store中

      登出成功后置空Store、sessionStorage

      首页

      极简的项目首页,路径/,一般作为项目主页,现在页面就是一个简单的欢迎页面,包括了几个router-link路由以及登出按钮

      

      test测试

      集成了vue数据绑定等简单测试

      info测试

      获取当前活跃配置环境分支,读取配置文件信息等简单测试

      admin测试

      element-ui配合上avue,可以快速搭建admin后台管理页面以及功能

      打包部署

      portal前端

      已经配置好了package.json文件

      "scripts": {
        "dev": "vue-cli-service serve --mode dev",
        "test": "vue-cli-service test --mode test",
        "build": "vue-cli-service build  --mode prod"
      },

      同时,vue.config.js中配置了生成路径

        publicPath: './',
        outputDir: 'dist',
        assetsDir: 'static',

      

      执行build命令,就会在package.json的同级目录下面,创建dist文件夹,生成的文件就在里面

      把生成的文件放到Tomcat容器或者其他容器中,运行容器,前端portal项目完成部署

      

      admin后端

      pom文件已经设置了打包配置

    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
            <finalName>${project.artifactId}</finalName>
            <outputDirectory>package</outputDirectory>
        </configuration>
    </plugin>

      maven直接执行package命令,就会在与pom文件同级目录下面创建package文件夹,生成的jar包就在里面

      使用java命令:java -jar admin.jar,运行jar包,后端admin项目完成部署

      后记

      一套极简的前后端分离项目脚手架就暂时记录到这,后续再进行补充 

      代码开源

       注:admin后端数据库文件在admin后端项目的resources/sql目录下面

      代码已经开源、托管到我的GitHub、码云:

      GitHub:https://github.com/huanzi-qch/fast-scaffold

      码云:https://gitee.com/huanzi-qch/fast-scaffold

  • 相关阅读:
    百度地图中循环输出坐标点信息是重复的问题解决方法
    质问微软 WP8.1开发HTTPS 真费劲
    WebService 页面重定向错误
    Oracle 错误 maximum number of processes(150) exceeded 解决办法
    spring quartz定时任务 配置
    centos 7安装完后出现please make your choice from '1' to e
    centos单机安装Hadoop2.6
    centos 关闭防火墙
    CentOS 7 系统下安装gnome图形界面
    CentOS 7 ifconfig: command not found
  • 原文地址:https://www.cnblogs.com/huanzi-qch/p/13933461.html
Copyright © 2011-2022 走看看