当前位置:网站首页>開源一套極簡的前後端分離專案腳手架
開源一套極簡的前後端分離專案腳手架
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
边栏推荐
- It's easy to operate. ThreadLocal can also be used as a cache
- 只有1个字节的文件实际占用多少磁盘空间
- Python filtering sensitive word records
- How to get started with new HTML5 (2)
- I think it is necessary to write a general idempotent component
- 文件过多时ls命令为什么会卡住?
- React design pattern: in depth understanding of react & Redux principle
- How to customize sorting for pandas dataframe
- Elasticsearch数据库 | Elasticsearch-7.5.0应用搭建实战
- Wow, elasticsearch multi field weight sorting can play like this
猜你喜欢
前端都应懂的入门基础-github基础
A course on word embedding
Linked blocking Queue Analysis of blocking queue
What is the side effect free method? How to name it? - Mario
JVM memory area and garbage collection
A brief history of neural networks
Free patent download tutorial (HowNet, Espacenet)
Thoughts on interview of Ali CCO project team
Summary of common algorithms of binary tree
Lane change detection
随机推荐
有了这个神器,快速告别垃圾短信邮件
Using NLP and ml to extract and construct web data
Summary of common algorithms of linked list
From zero learning artificial intelligence, open the road of career planning!
DRF JWT authentication module and self customization
What if the front end doesn't use spa? - Hacker News
如何玩转sortablejs-vuedraggable实现表单嵌套拖拽功能
NLP model Bert: from introduction to mastery (1)
用一个例子理解JS函数的底层处理机制
[event center azure event hub] interpretation of error information found in event hub logs
What is the difference between data scientists and machine learning engineers? - kdnuggets
Lane change detection
6.2 handleradapter adapter processor (in-depth analysis of SSM and project practice)
一篇文章教会你使用Python网络爬虫下载酷狗音乐
Not long after graduation, he earned 20000 yuan from private work!
How to get started with new HTML5 (2)
How to use Python 2.7 after installing anaconda3?
I think it is necessary to write a general idempotent component
ES6学习笔记(五):轻松了解ES6的内置扩展对象
Python filtering sensitive word records