当前位置:网站首页>開源一套極簡的前後端分離專案腳手架
開源一套極簡的前後端分離專案腳手架
2020-11-06 21:07:00 【itread01】
前言
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
版权声明
本文为[itread01]所创,转载请带上原文链接,感谢
https://www.itread01.com/content/1604667665.html
边栏推荐
- Vue.js Mobile end left slide delete component
- 5.4 static resource mapping
- Solve the problem of database insert data garbled in PL / SQL developer
- Electron application uses electronic builder and electronic updater to realize automatic update
- Lane change detection
- 一部完整的游戏,需要制作哪些音乐?
- How to demote domain controllers and later in Windows Server 2012
- 每个大火的“线上狼人杀”平台,都离不开这个新功能
- 6.1.2 handlermapping mapping processor (2) (in-depth analysis of SSM and project practice)
- 6.4 viewresolver view parser (in-depth analysis of SSM and project practice)
猜你喜欢
The road of C + + Learning: from introduction to mastery
How to encapsulate distributed locks more elegantly
Summary of common algorithms of linked list
Python download module to accelerate the implementation of recording
I think it is necessary to write a general idempotent component
The dynamic thread pool in Kitty supports Nacos and Apollo multi configuration centers
What to do if you are squeezed by old programmers? I don't want to quit
Cglib 如何实现多重代理?
Python filtering sensitive word records
一篇文章教会你使用HTML5 SVG 标签
随机推荐
5.4 static resource mapping
What to do if you are squeezed by old programmers? I don't want to quit
一篇文章教会你使用Python网络爬虫下载酷狗音乐
vue-codemirror基本用法:实现搜索功能、代码折叠功能、获取编辑器值及时验证
ES6学习笔记(二):教你玩转类的继承和类的对象
一部完整的游戏,需要制作哪些音乐?
给字节的学姐讲如何准备“系统设计面试”
Summary of common algorithms of linked list
文件过多时ls命令为什么会卡住?
With the advent of tensorflow 2.0, can pytoch still shake the status of big brother?
Our best practices for writing react components
How to use Python 2.7 after installing anaconda3?
Python Jieba segmentation (stuttering segmentation), extracting words, loading words, modifying word frequency, defining thesaurus
Python基础变量类型——List浅析
Interface pressure test: installation, use and instruction of siege pressure test
Solve the problem of database insert data garbled in PL / SQL developer
The difference between gbdt and XGB, and the mathematical derivation of gradient descent method and Newton method
Five vuex plug-ins for your next vuejs project
[actual combat of flutter] pubspec.yaml Configuration file details
DRF JWT authentication module and self customization