开源一套极简的前后端分离项目脚手架

微信扫一扫,分享到朋友圈

开源一套极简的前后端分离项目脚手架

前言

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

微信扫一扫,分享到朋友圈

开源一套极简的前后端分离项目脚手架

nodejs篇-实现一个koa

上一篇

国内第三季度5G手机出货量近5000万台 华米OV占据前四

下一篇

你也可能喜欢

开源一套极简的前后端分离项目脚手架

长按储存图像,分享给朋友