日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > vue >内容正文

vue

基于SpringBoot+Vue开发的前后端分离博客项目-Java后端接口开发

發(fā)布時間:2024/9/27 vue 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 基于SpringBoot+Vue开发的前后端分离博客项目-Java后端接口开发 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

文章目錄

      • 1. 前言
      • 2. 新建Springboot項目
      • 3. 整合mybatis plus
        • 第一步:導依賴
        • 第二步:寫配置文件
        • 第三步:mapper掃描+分頁插件
        • 第四步:代碼生成配置
        • 第五步:執(zhí)行代碼生成
      • 4. 統(tǒng)一結果封裝
      • 5. 邏輯整理
        • 5.1. 登錄邏輯
        • 5.2. 用戶訪問后端邏輯
      • 1.6. 整合shiro+jwt+會話共享
        • 6.1. 導入pom依賴
        • 6.2. 編寫配置
        • 6.4. AccountRealm
        • 6.5. JwtToken
        • 6.6. JwtUtils
        • 6.7. AccountProfile
        • 6.8. shiro redis配置
        • 6.9. JwtFilter
        • 6.10. 權限注解測試
      • 7. 全局異常處理
      • 8. 實體校驗
      • 9. 跨域問題
        • 9.1.全局跨域處理
        • 9.2. jwtfliter 之前跨域
      • 10.登錄接口開發(fā)
        • 10.1. 登錄對象封裝
        • 10.2. 用戶接口
        • 10.3. 全局異常豐富
      • 11.博客接口開發(fā)
        • 11.1. 接口開發(fā)
      • 11.2.實體類校驗
        • 11.3. 博文接口測試
      • 12. 后端接口開發(fā)總結


作者:呂一明
項目代碼:https://github.com/MarkerHub/vueblog
項目視頻:https://www.bilibili.com/video/BV1PQ4y1P7hZ/

1. 前言

從零開始搭建一個項目骨架,最好選擇合適,熟悉的技術,并且在未來易拓展,適合微服務化體系等。所以一般以Springboot作為我們的框架基礎,這是離不開的了。
然后數(shù)據(jù)層,我們常用的是Mybatis,易上手,方便維護。但是單表操作比較困難,特別是添加字段或減少字段的時候,比較繁瑣,所以這里我推薦使用Mybatis Plus為簡化開發(fā)而生,為簡化開發(fā)而生,只… CRUD 操作,從而節(jié)省大量時間。
作為一個項目骨架,權限也是我們不能忽略的,Shiro配置簡單,使用也簡單,所以使用Shiro作為我們的的權限。
考慮到項目可能需要部署多臺,這時候我們的會話等信息需要共享,Redis是現(xiàn)在主流的緩存中間件,也適合我們的項目。
然后因為前后端分離,所以我們使用jwt作為我們用戶身份憑證。
ok,我們現(xiàn)在就開始搭建我們的項目腳手架!
技術棧:

  • SpringBoot
  • mybatis plus
  • shiro
  • lombok
  • redis
  • hibernate validatior
  • jwt

導圖:http://www.markerhub.com/map/131

2. 新建Springboot項目

這里,我們使用IDEA來開發(fā)我們項目,新建步驟比較簡單,我們就不截圖了。
開發(fā)工具與環(huán)境:

  • idea
  • mysql
  • jdk 8
  • maven3.3.9

新建好的項目結構如下,SpringBoot版本使用的目前最新的2.2.6.RELEASE版本

pom.xml

<!--web啟動器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--熱部署--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><!--數(shù)據(jù)庫驅動--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
  • devtools:項目的熱加載重啟插件
  • lombok:簡化代碼的工具

3. 整合mybatis plus

接下來,我們來整合mybatis plus,讓項目能完成基本的增刪改查操作。步驟很簡單:可以去官網(wǎng)看看:https://mp.baomidou.com/guide/install.html

第一步:導依賴

pom中導入mybatis plus的jar包,因為后面會涉及到代碼生成,所以我們還需要導入頁面模板引擎,這里我們用的是freemarker。

<!--mybatis-plus--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.2.0</version></dependency><!--mp代碼生成器--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.2.0</version></dependency><!--模板引擎--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId></dependency>

第二步:寫配置文件

# DataSource Config spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghaiusername: rootpassword: root mybatis-plus:mapper-locations: classpath*:/mapper/**Mapper.xml

上面除了配置數(shù)據(jù)庫的信息,還配置了myabtis plus的mapper的xml文件的掃描路徑,這一步不要忘記了。

第三步:mapper掃描+分頁插件

開啟mapper接口掃描,添加分頁插件

新建一個config包:通過@mapperScan注解指定要變成實現(xiàn)類的接口所在的包,然后包下面的所有接口在編譯之后都會生成相應的實現(xiàn)類。PaginationInterceptor是一個分頁插件。

  • com.gblfy.config.MybatisPlusConfig
package com.gblfy.config;import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement;@Configuration @EnableTransactionManagement @MapperScan("com.gblfy.mapper") public class MybatisPlusConfig {@Beanpublic PaginationInterceptor paginationInterceptor() {PaginationInterceptor paginationInterceptor = new PaginationInterceptor();return paginationInterceptor;} }

第四步:代碼生成配置

如果你沒再用其他插件,那么現(xiàn)在就已經(jīng)可以使用mybatis plus了,官方給我們提供了一個代碼生成器,然后我寫上自己的參數(shù)之后,就可以直接根據(jù)數(shù)據(jù)庫表信息生成entity、service、mapper等接口和實現(xiàn)類。

  • com.gblfy.CodeGenerator
package com.gblfy.generator;import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.InjectionConfig; import com.baomidou.mybatisplus.generator.config.*; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;import java.util.ArrayList; import java.util.List; import java.util.Scanner;// 演示例子,執(zhí)行 main 方法控制臺輸入模塊表名回車自動生成對應項目目錄中 public class CodeGenerator {/*** <p>* 讀取控制臺內(nèi)容* </p>*/public static String scanner(String tip) {Scanner scanner = new Scanner(System.in);StringBuilder help = new StringBuilder();help.append("請輸入" + tip + ":");System.out.println(help.toString());if (scanner.hasNext()) {String ipt = scanner.next();if (StringUtils.isNotEmpty(ipt)) {return ipt;}}throw new MybatisPlusException("請輸入正確的" + tip + "!");}public static void main(String[] args) {// 代碼生成器AutoGenerator mpg = new AutoGenerator();// 全局配置GlobalConfig gc = new GlobalConfig();String projectPath = System.getProperty("user.dir");gc.setOutputDir(projectPath + "/src/main/java"); // gc.setOutputDir("D:\\test");gc.setAuthor("gblfy");gc.setOpen(false);// gc.setSwagger2(true); 實體屬性 Swagger2 注解gc.setServiceName("%sService");mpg.setGlobalConfig(gc);// 數(shù)據(jù)源配置DataSourceConfig dsc = new DataSourceConfig();dsc.setUrl("jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC");// dsc.setSchemaName("public");dsc.setDriverName("com.mysql.cj.jdbc.Driver");dsc.setUsername("root");dsc.setPassword("root");mpg.setDataSource(dsc);// 包配置PackageConfig pc = new PackageConfig();pc.setModuleName(null);pc.setParent("com.gblfy");mpg.setPackageInfo(pc);// 自定義配置InjectionConfig cfg = new InjectionConfig() {@Overridepublic void initMap() {// to do nothing}};// 如果模板引擎是 freemarkerString templatePath = "/templates/mapper.xml.ftl";// 如果模板引擎是 velocity// String templatePath = "/templates/mapper.xml.vm";// 自定義輸出配置List<FileOutConfig> focList = new ArrayList<>();// 自定義配置會被優(yōu)先輸出focList.add(new FileOutConfig(templatePath) {@Overridepublic String outputFile(TableInfo tableInfo) {// 自定義輸出文件名 , 如果你 Entity 設置了前后綴、此處注意 xml 的名稱會跟著發(fā)生變化!!return projectPath + "/src/main/resources/mapper/"+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;}});cfg.setFileOutConfigList(focList);mpg.setCfg(cfg);// 配置模板TemplateConfig templateConfig = new TemplateConfig();templateConfig.setXml(null);mpg.setTemplate(templateConfig);// 策略配置StrategyConfig strategy = new StrategyConfig();strategy.setNaming(NamingStrategy.underline_to_camel);strategy.setColumnNaming(NamingStrategy.underline_to_camel);strategy.setEntityLombokModel(true);strategy.setRestControllerStyle(true);strategy.setInclude(scanner("表名,多個英文逗號分割").split(","));strategy.setControllerMappingHyphenStyle(true);strategy.setTablePrefix("m_");mpg.setStrategy(strategy);mpg.setTemplateEngine(new FreemarkerTemplateEngine());mpg.execute();} }

首先我在數(shù)據(jù)庫中新建了一個user表:

CREATE TABLE `m_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`username` varchar(64) DEFAULT NULL,`avatar` varchar(255) DEFAULT NULL,`email` varchar(64) DEFAULT NULL,`password` varchar(64) DEFAULT NULL,`status` int(5) NOT NULL,`created` datetime DEFAULT NULL,`last_login` datetime DEFAULT NULL,PRIMARY KEY (`id`),KEY `UK_USERNAME` (`username`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `m_blog` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`user_id` bigint(20) NOT NULL,`title` varchar(255) NOT NULL,`description` varchar(255) NOT NULL,`content` longtext,`created` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,`status` tinyint(4) DEFAULT NULL,PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4; INSERT INTO `vueblog`.`m_user` (`id`, `username`, `avatar`, `email`, `password`, `status`, `created`, `last_login`) VALUES ('1', 'markerhub', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', NULL, '96e79218965eb72c92a549dd5a330112', '0', '2020-04-20 10:44:01', NULL);

第五步:執(zhí)行代碼生成

運行CodeGenerator的main方法,輸入表名:m_user,m_blog生成結果如下:

簡潔!方便!經(jīng)過上面的步驟,基本上我們已經(jīng)把mybatis plus框架集成到項目中了。

在UserController中寫個測試:

@RestController @RequestMapping("/user") public class UserController {@Autowiredprivate UserService userService;@GetMapping("/{id}")public Object test(@PathVariable("id") Long id) {return userService.getById(id);}}

訪問:http://localhost:8080/user/1 獲得結果如下,整合成功!

4. 統(tǒng)一結果封裝

這里我們用到了一個Result的類,這個用于我們的異步統(tǒng)一返回的結果封裝。一般來說,結果里面有幾個要素必要的

  • 是否成功,可用code表示(如200表示成功,400表示異常)
  • 結果消息
  • 結果數(shù)據(jù)

所以可得到封裝如下:

  • com.gblfy.common.lang.Result
package com.gblfy.common.lang;import lombok.Data;import java.io.Serializable;@Data public class Result implements Serializable {private int code;//(200成功 400失敗)private String msg;private Object data;//成功的場景 可以根據(jù)需求自定義public static Result succ(Object data) {return succ(200, "操作成功", data);}public static Result succ(int code, String msg, Object data) {Result r = new Result();r.setCode(200);r.setMsg(msg);r.setData(data);return r;}//失敗的場景 可以根據(jù)需求自定義public static Result fail(String msg) {return fail(msg, null);}public static Result fail(String msg, Object data) {return fail(400, msg, data);}public static Result fail(int code, String msg, Object data) {Result r = new Result();r.setCode(code);r.setMsg(msg);r.setData(data);return r;} }

test方法改造

/*** 測試地址:http://localhost:8080/user/1* @param id* @return*/@GetMapping("/{id}")public Result test(@PathVariable("id") Long id) {User user = userService.getById(id);return Result.succ(user);}

5. 邏輯整理

5.1. 登錄邏輯

  • 1.用戶輸入用戶名密碼登錄

  • 2.登錄請求后端,會對前端傳過來的用戶名密碼進行校驗,

    • 1>用戶名或密碼錯誤就拋出異常,全局異常類就會捕獲此異常,進行統(tǒng)一異常處理

    • 2>用戶名或密碼正確,就生成jwt用戶身份憑證

  • 3.后端生成的jwt用戶身份憑證返回前端,訪問后端資源接口時,用戶攜帶者jwt用戶身份憑證

5.2. 用戶訪問后端邏輯

  • 1.用戶訪問后端資源接口,首先被JwtFilter統(tǒng)一攔截

  • 2.JwtFilter處理

    • 1>攜帶jwt用戶身份憑證,shiro登錄處理

      • ①攜帶jwt過期

      • ? ②jwt不正確

      • ③jwt用戶身份憑證正確,訪問某資源接口

? 以上不正常場景,都會拋出異常,由全局異常處理類捕獲處理

  • 2>無jwt用戶身份憑證,訪問某資源接口

  • 3.當訪問某一資源接口之前,接口方法上會有角色或者資源權限的注解過濾

    • ? 1>校驗通過,正常獲取資源,返回后端數(shù)據(jù)給用戶

    • ? 2>校驗不通過,拋出異常,由全局異常處理類捕獲處理

1.6. 整合shiro+jwt+會話共享

考慮到后面可能需要做集群、負載均衡等,所以就需要會話共享,而shiro的緩存和會話信息,我們一般考慮使用redis來存儲這些數(shù)據(jù),所以,我們不僅僅需要整合shiro,同時也需要整合redis。在開源的項目中,我們找到了一個starter可以快速整合shiro-redis,配置簡單,這里也推薦大家使用。

而因為我們需要做的是前后端分離項目的骨架,所以一般我們會采用token或者jwt作為跨域身份驗證解決方案。所以整合shiro的過程中,我們需要引入jwt的身份驗證過程。

那么我們就開始整合:

我們使用一個shiro-redis-spring-boot-starter的jar包,具體教程可以看官方文檔:

https://github.com/alexxiyang/shiro-redis/blob/master/docs/README.md#spring-boot-starter

6.1. 導入pom依賴

導入shiro-redis的starter包:還有jwt的工具包,以及為了簡化開發(fā),我引入了hutool工具包。

<!--shiro-redis--><dependency><groupId>org.crazycake</groupId><artifactId>shiro-redis-spring-boot-starter</artifactId><version>3.2.1</version></dependency><!-- hutool工具類--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.3.3</version></dependency><!-- jwt --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency>

6.2. 編寫配置

  • com.gblfy.config.ShiroConfig
package com.gblfy.config;import com.gblfy.shiro.AccountRealm; import com.gblfy.shiro.JwtFilter; import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; import org.apache.shiro.mgt.DefaultSubjectDAO; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition; import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.crazycake.shiro.RedisCacheManager; import org.crazycake.shiro.RedisSessionDAO; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;import javax.servlet.Filter; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map;/*** shiro啟用注解攔截控制器*/ @Configuration public class ShiroConfig {@AutowiredJwtFilter jwtFilter;@Beanpublic SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();sessionManager.setSessionDAO(redisSessionDAO);return sessionManager;}@Beanpublic DefaultWebSecurityManager securityManager(AccountRealm accountRealm,SessionManager sessionManager,RedisCacheManager redisCacheManager) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);securityManager.setSessionManager(sessionManager);securityManager.setCacheManager(redisCacheManager);/** 關閉shiro自帶的session,詳情見文檔*/DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();defaultSessionStorageEvaluator.setSessionStorageEnabled(false);subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);securityManager.setSubjectDAO(subjectDAO);return securityManager;}@Beanpublic ShiroFilterChainDefinition shiroFilterChainDefinition() {DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();Map<String, String> filterMap = new LinkedHashMap<>();filterMap.put("/**", "jwt"); // 主要通過注解方式校驗權限chainDefinition.addPathDefinitions(filterMap);return chainDefinition;}@Bean("shiroFilterFactoryBean")public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();shiroFilter.setSecurityManager(securityManager);Map<String, Filter> filters = new HashMap<>();filters.put("jwt", jwtFilter);shiroFilter.setFilters(filters);Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();shiroFilter.setFilterChainDefinitionMap(filterMap);return shiroFilter;}@Beanpublic static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();//強制使用cglib,防止重復代理和可能引起代理出錯的問題//https://zhuanlan.zhihu.com/p/29161098defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);return defaultAdvisorAutoProxyCreator;}/*** 啟用shoiro內(nèi)部Bean生命周期管理** @return*/@Bean(name = "lifecycleBeanPostProcessor")public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {LifecycleBeanPostProcessor lifecycleBeanPostProcessor = new LifecycleBeanPostProcessor();return lifecycleBeanPostProcessor;}/*** 啟用shiro注解** @param securityManager* @return*/@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();advisor.setSecurityManager(securityManager);return advisor;} }

上面ShiroConfig,我們主要做了幾件事情:

引入RedisSessionDAO和RedisCacheManager,為了解決shiro的權限數(shù)據(jù)和會話信息能保存到redis中,實現(xiàn)會話共享。
重寫了SessionManager和DefaultWebSecurityManager,同時在DefaultWebSecurityManager中為了關閉shiro自帶的session方式,我們需要設置為false,這樣用戶就不再能通過session方式登錄shiro。后面將采用jwt憑證登錄。
在ShiroFilterChainDefinition中,我們不再通過編碼形式攔截Controller訪問路徑,而是所有的路由都需要經(jīng)過JwtFilter這個過濾器,然后判斷請求頭中是否含有jwt的信息,有就登錄,沒有就跳過。跳過之后,有Controller中的shiro注解進行再次攔截,比如@RequiresAuthentication,這樣控制權限訪問。

那么,接下來,我們聊聊ShiroConfig中出現(xiàn)的AccountRealm,還有JwtFilter。

6.4. AccountRealm

AccountRealm是shiro進行登錄或者權限校驗的邏輯所在,算是核心了,我們需要重寫3個方法,分別是

  • supports:為了讓realm支持jwt的憑證校驗
  • doGetAuthorizationInfo:權限校驗
  • doGetAuthenticationInfo:登錄認證校驗

我們先來總體看看AccountRealm的代碼,然后逐個分析:

  • com.gblfy.shiro.AccountRealm
package com.gblfy.shiro;import cn.hutool.core.bean.BeanUtil; import com.gblfy.entity.User; import com.gblfy.service.UserService; import com.gblfy.util.JwtUtils; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component;@Slf4j @Component public class AccountRealm extends AuthorizingRealm {@AutowiredJwtUtils jwtUtils;@AutowiredUserService userService;@Overridepublic boolean supports(AuthenticationToken token) {return token instanceof JwtToken;}@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {return null;}@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {JwtToken jwtToken = (JwtToken) token;String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();User user = userService.getById(Long.valueOf(userId));if (user == null) {throw new UnknownAccountException("賬戶不存在");}if (user.getStatus() == -1) {throw new LockedAccountException("賬戶已被鎖定");}AccountProfile profile = new AccountProfile();BeanUtil.copyProperties(user, profile);return new SimpleAuthenticationInfo(profile, jwtToken.getCredentials(), getName());} }

其實主要就是doGetAuthenticationInfo登錄認證這個方法,可以看到我們通過jwt獲取到用戶信息,判斷用戶的狀態(tài),最后異常就拋出對應的異常信息,否者封裝成SimpleAuthenticationInfo返回給shiro。 接下來我們逐步分析里面出現(xiàn)的新類:

1、shiro默認supports的是UsernamePasswordToken,而我們現(xiàn)在采用了jwt的方式,所以這里我們自定義一個JwtToken,來完成shiro的supports方法。

6.5. JwtToken

com.gblfy.shiro.JwtToken

package com.gblfy.shiro;import org.apache.shiro.authc.AuthenticationToken;public class JwtToken implements AuthenticationToken {private String token;public JwtToken(String jwt) {this.token = jwt;}@Overridepublic Object getPrincipal() {return token;}@Overridepublic Object getCredentials() {return token;} }

6.6. JwtUtils

是個生成和校驗jwt的工具類,其中有些jwt相關的密鑰信息是從項目配置文件中配置的:

package com.gblfy.util;import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component;import java.util.Date;/*** jwt工具類*/ @Slf4j @Data @Component @ConfigurationProperties(prefix = "gblfy.jwt") public class JwtUtils {private String secret;private long expire;private String header;/*** 生成jwt token*/public String generateToken(long userId) {Date nowDate = new Date();//過期時間Date expireDate = new Date(nowDate.getTime() + expire * 1000);return Jwts.builder().setHeaderParam("typ", "JWT").setSubject(userId + "").setIssuedAt(nowDate).setExpiration(expireDate).signWith(SignatureAlgorithm.HS512, secret).compact();}/*** 校驗jwt是否合法** @param token* @return*/public Claims getClaimByToken(String token) {try {return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();} catch (Exception e) {log.debug("validate is token error ", e);return null;}}/*** token是否過期** @return true:過期*/public boolean isTokenExpired(Date expiration) {return expiration.before(new Date());} }

6.7. AccountProfile

而在AccountRealm我們還用到了AccountProfile,這是為了登錄成功之后返回的一個用戶信息的載體,AccountProfile

  • com.gblfy.shiro.AccountProfile
package com.gblfy.shiro;import lombok.Data;import java.io.Serializable;@Data public class AccountProfile implements Serializable {private Long id;private String username;private String avatar;private String email;}

6.8. shiro redis配置

第三步,ok,基本的校驗的路線完成之后,我們需要少量的基本信息配置:

shiro-redis:enabled: trueredis-manager:host: 127.0.0.1:6379 markerhub:jwt:# 加密秘鑰secret: f4e2e52034348f86b67cde581c0f9eb5# token有效時長,7天,單位秒expire: 604800header: token

第四步:另外,如果你項目有使用spring-boot-devtools,需要添加一個配置文件,在resources目錄下新建文件夾META-INF,然后新建文件spring-devtools.properties,這樣熱重啟時候才不會報錯。

  • resources/META-INF/spring-devtools.properties
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar

6.9. JwtFilter

第五步:定義jwt的過濾器JwtFilter。

這個過濾器是我們的重點,這里我們繼承的是Shiro內(nèi)置的AuthenticatingFilter,一個可以內(nèi)置了可以自動登錄方法的的過濾器,有些同學繼承BasicHttpAuthenticationFilter也是可以的。

我們需要重寫幾個方法:

  • createToken:實現(xiàn)登錄,我們需要生成我們自定義支持的JwtToken
  • onAccessDenied:攔截校驗,當頭部沒有Authorization時候,我們直接通過,不需要自動登錄;當帶有的時候,首先我們校驗jwt的有效性,沒問題我們就直接執(zhí)行executeLogin方法實現(xiàn)自動登錄
  • onLoginFailure:登錄異常時候進入的方法,我們直接把異常信息封裝然后拋出
  • preHandle:攔截器的前置攔截,因為我們是前后端分析項目,項目中除了需要跨域全局配置之外,我們再攔截器中也需要提供跨域支持。這樣,攔截器才不會在進入Controller之前就被限制了。
  • 下面我們看看總體的代碼:

    • com.gblfy.shiro.JwtFilter
    package com.gblfy.shiro;import cn.hutool.json.JSONUtil; import com.gblfy.common.lang.Result; import com.gblfy.util.JwtUtils; import io.jsonwebtoken.Claims; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.ExpiredCredentialsException; import org.apache.shiro.web.filter.authc.AuthenticatingFilter; import org.apache.shiro.web.util.WebUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod;import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;@Component public class JwtFilter extends AuthenticatingFilter {@AutowiredJwtUtils jwtUtils;@Overrideprotected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {// 獲取 tokenHttpServletRequest request = (HttpServletRequest) servletRequest;String jwt = request.getHeader("Authorization");if (StringUtils.isEmpty(jwt)) {return null;}return new JwtToken(jwt);}@Overrideprotected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {// 獲取 tokenHttpServletRequest request = (HttpServletRequest) servletRequest;String jwt = request.getHeader("Authorization");if (StringUtils.isEmpty(jwt)) {// 無 jwt(token)直接訪問資源方法通過權限朱姐過濾攔截return true;} else {// 校驗jwt(token)Claims claim = jwtUtils.getClaimByToken(jwt);if (claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {throw new ExpiredCredentialsException("token 已失效,請重新登陸");}// 執(zhí)行登陸return executeLogin(servletRequest, servletResponse);}}/*** 登陸失敗處理* 賬號鎖定 賬號不存在等** @param token* @param e* @param request* @param response* @return*/@Overrideprotected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {HttpServletResponse httpServletResponse = (HttpServletResponse) response;Throwable throwable = e.getCause() == null ? e : e.getCause();Result result = Result.fail(throwable.getMessage());String jsonStr = JSONUtil.toJsonStr(result);try {//響應給前端httpServletResponse.getWriter().print(jsonStr);} catch (IOException ioException) {}return false;}/*** 在JwtFilter處理邏輯之前,進行跨域處理** @param request* @param response* @return* @throws Exception*/@Overrideprotected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {HttpServletRequest httpServletRequest = WebUtils.toHttp(request);HttpServletResponse httpServletResponse = WebUtils.toHttp(response);httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));// 跨域時會首先發(fā)送一個OPTIONS請求,這里我們給OPTIONS請求直接返回正常狀態(tài)if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());return false;}return super.preHandle(request, response);} }

    那么到這里,我們的shiro就已經(jīng)完成整合進來了,并且使用了jwt進行身份校驗。

    6.10. 權限注解測試

    /*** 測試地址:http://localhost:8080/user/1* @param id* @return*/@RequiresAuthentication@GetMapping("/{id}")public Result test(@PathVariable("id") Long id) {User user = userService.getById(id);return Result.succ(user);}

    7. 全局異常處理

    有時候不可避免服務器報錯的情況,如果不配置異常處理機制,就會默認返回tomcat或者nginx的5XX頁面,對普通用戶來說,不太友好,用戶也不懂什么情況。這時候需要我們程序員設計返回一個友好簡單的格式給前端。

    處理辦法如下:通過使用@ControllerAdvice來進行統(tǒng)一異常處理,@ExceptionHandler(value = RuntimeException.class)來指定捕獲的Exception各個類型異常 ,這個異常的處理,是全局的,所有類似的異常,都會跑到這個地方處理。

    • com.gblfy.common.exception.GlobalExceptionHandler

    步驟二、定義全局異常處理,@ControllerAdvice表示定義全局控制器異常處理,@ExceptionHandler表示針對性異常處理,可對每種異常針對性處理。

    package com.gblfy.common.exception;import com.gblfy.common.lang.Result; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.ShiroException; import org.springframework.http.HttpStatus; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice;import java.io.IOException;@Slf4j @RestControllerAdvice public class GlobalExceptionHandler {/*** 處理Assert斷言的異常*/@ResponseStatus(HttpStatus.BAD_REQUEST)@ExceptionHandler(value = IllegalArgumentException.class)public Result handler(IllegalArgumentException e) throws IOException {log.error("Assert異常:-------------->{}", e.getMessage());return Result.fail(e.getMessage());}/*** @Validated 校驗錯誤異常處理*/@ResponseStatus(HttpStatus.BAD_REQUEST)@ExceptionHandler(value = MethodArgumentNotValidException.class)public Result handler(MethodArgumentNotValidException e) throws IOException {log.error("運行時異常:-------------->", e);BindingResult bindingResult = e.getBindingResult();ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();return Result.fail(objectError.getDefaultMessage());}/*** Shiro異常捕獲** @param e* @return*/@ResponseStatus(HttpStatus.UNAUTHORIZED)@ExceptionHandler(value = ShiroException.class)public Result handler(ShiroException e) {log.error("運行時異常:----------------{}", e);return Result.fail(401, e.getMessage(), null);}/*** 運行時異常捕獲** @param e* @return*/@ResponseStatus(HttpStatus.BAD_REQUEST)@ExceptionHandler(value = RuntimeException.class)public Result handler(RuntimeException e) {log.error("運行時異常:----------------{}", e);return Result.fail(e.getMessage());}}

    上面我們捕捉了幾個異常:

    • ShiroException:shiro拋出的異常,比如沒有權限,用戶登錄異常
    • IllegalArgumentException:處理Assert的異常
    • MethodArgumentNotValidException:處理實體校驗的異常
    • RuntimeException:捕捉其他異常

    8. 實體校驗

    當我們表單數(shù)據(jù)提交的時候,前端的校驗我們可以使用一些類似于jQuery Validate等js插件實現(xiàn),而后端我們可以使用Hibernate validatior來做校驗。

    我們使用springboot框架作為基礎,那么就已經(jīng)自動集成了Hibernate validatior。

    那么用起來啥樣子的呢?

    第一步:首先在實體的屬性上添加對應的校驗規(guī)則,比如:

    @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("m_user") public class User implements Serializable {private static final long serialVersionUID = 1L;@TableId(value = "id", type = IdType.AUTO)private Long id;@NotBlank(message = "昵稱不能為空")private String username;private String avatar;@NotBlank(message = "郵箱不能為空")@Email(message = "郵箱格式不正確")private String email;private String password;private Integer status;private LocalDateTime created;private LocalDateTime lastLogin; }

    測試方法

    /*** 測試實體校驗** @param user* @return*/@PostMapping("save")public Result save(@Validated @RequestBody User user) {return Result.succ(user);}

    postman 測試

    http://localhost:8080/user/save

    { "username":"gblfy","email": "xxxx" }

    9. 跨域問題

    因為是前后端分析,所以跨域問題是避免不了的,我們直接在后臺進行全局跨域處理:

    9.1.全局跨域處理

    • com.gblfy.config.CorsConfig
    package com.gblfy.config;import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** 解決跨域問題*/ @Configuration public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("*").allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS").allowCredentials(true).maxAge(3600).allowedHeaders("*");} }

    9.2. jwtfliter 之前跨域

    在JwtFilter勒種添加此方法,解決跨域處理

    /*** 在JwtFilter處理邏輯之前,進行跨域處理** @param request* @param response* @return* @throws Exception*/@Overrideprotected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {HttpServletRequest httpServletRequest = WebUtils.toHttp(request);HttpServletResponse httpServletResponse = WebUtils.toHttp(response);httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));// 跨域時會首先發(fā)送一個OPTIONS請求,這里我們給OPTIONS請求直接返回正常狀態(tài)if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());return false;}return super.preHandle(request, response);}

    ok,因為我們系統(tǒng)開發(fā)的接口比較簡單,所以我就不集成swagger2啦,也比較簡單而已。下面我們就直接進入我們的正題,進行編寫登錄接口。

    10.登錄接口開發(fā)

    10.1. 登錄對象封裝

    登錄的邏輯其實很簡答,只需要接受賬號密碼,然后把用戶的id生成jwt,返回給前段,為了后續(xù)的jwt的延期,所以我們把jwt放在header上。具體代碼如下:

    • com.gblfy.controller.AccountController
    package com.gblfy.common.dto;import lombok.Data;import javax.validation.constraints.NotBlank; import java.io.Serializable;@Data public class LoginDto implements Serializable {@NotBlank(message = "昵稱不能為空")private String username;@NotBlank(message = "密碼不能為空")private String password; }

    10.2. 用戶接口

    package com.gblfy.controller;import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.crypto.SecureUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.gblfy.common.dto.LoginDto; import com.gblfy.common.lang.Result; import com.gblfy.entity.User; import com.gblfy.service.UserService; import com.gblfy.util.JwtUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.annotation.RequiresAuthentication; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletResponse;@RestController public class AccountController {@AutowiredUserService userService;@AutowiredJwtUtils jwtUtils;/*** 用戶登錄接口** @param loginDto* @param response* @return*/@PostMapping("/login")public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response) {User user = userService.getOne(new QueryWrapper<User>().eq("username", loginDto.getUsername()));//使用Assert斷言判斷用戶存不存在 會拋出IllegalArgumentException,需要在全局異常捕獲處理Assert.notNull(user, "用戶不存在!");if (!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))) {return Result.fail("密碼不正確!");}//校驗通過生成jwtString jwt = jwtUtils.generateToken(user.getId());//將jwt放入headerresponse.setHeader("Authorization", jwt);response.setHeader("Access-control-Expose-Headers", "Authorization");return Result.succ(MapUtil.builder().put("id", user.getId()).put("username", user.getUsername()).put("aAvatar", user.getAvatar()).put("email", user.getEmail()).map());}/*** 用戶登出接口** @return*/@RequiresAuthentication@PostMapping("/logout")public Result logout() {SecurityUtils.getSubject().logout();return Result.succ(null);} }

    注意@RequiresAuthentication說明需要登錄之后才能訪問的接口,其他需要權限的接口可以添加shiro的相關注解。 接口比較簡單,我們就不多說了,基本增刪改查而已。注意的是edit方法是需要登錄才能操作的受限資源。

    10.3. 全局異常豐富

    在GlobalExceptionHandler類中添加校驗異常和斷言異常處理

    /*** 處理Assert斷言的異常*/@ResponseStatus(HttpStatus.BAD_REQUEST)@ExceptionHandler(value = IllegalArgumentException.class)public Result handler(IllegalArgumentException e) throws IOException {log.error("Assert異常:-------------->{}", e.getMessage());return Result.fail(e.getMessage());}/*** @Validated 校驗錯誤異常處理*/@ResponseStatus(HttpStatus.BAD_REQUEST)@ExceptionHandler(value = MethodArgumentNotValidException.class)public Result handler(MethodArgumentNotValidException e) throws IOException {log.error("運行時異常:-------------->", e);BindingResult bindingResult = e.getBindingResult();ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();return Result.fail(objectError.getDefaultMessage());}

    11.博客接口開發(fā)

    11.1. 接口開發(fā)

    package com.gblfy.controller;import cn.hutool.core.bean.BeanUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.gblfy.common.lang.Result; import com.gblfy.entity.Blog; import com.gblfy.service.BlogService; import com.gblfy.util.ShiroUtil; import org.apache.shiro.authz.annotation.RequiresAuthentication; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.Assert; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*;import java.time.LocalDateTime;/*** <p>* 前端控制器* </p>** @author gblfy* @since 2020-06-02*/ @RestController public class BlogController {@AutowiredBlogService blogService;/*** 博文列表** @param currentPage* @return*/@GetMapping("/blogs")public Result list(@RequestParam(defaultValue = "1") Integer currentPage) {//當前第幾頁 一頁顯示幾條 默認5條Page page = new Page(currentPage, 5);//設置分頁顯示的條件 例如:邏輯刪除的 排序等 都可以在這里面追加IPage pageData = blogService.page(page, new QueryWrapper<Blog>().orderByDesc("created"));return Result.succ(pageData);}/*** 博文查詢** @param id* @return*/@GetMapping("/blog/{id}")public Result detail(@PathVariable(name = "id") Long id) {Blog blog = blogService.getById(id);Assert.notNull(blog, "該博客已被刪除");return Result.succ(blog);}/*** 博文添加和編輯** @param blog* @return*/@RequiresAuthentication@PostMapping("/blog/edit")public Result edit(@Validated @RequestBody Blog blog) {Blog temp = null;if (blog.getId() != null) {temp = blogService.getById(blog.getId());// 只能編輯自己的文章System.out.println(ShiroUtil.getProfile().getId());Assert.isTrue(temp.getUserId().longValue() == ShiroUtil.getProfile().getId().longValue(), "沒有權限編輯");} else {temp = new Blog();temp.setUserId(ShiroUtil.getProfile().getId());temp.setCreated(LocalDateTime.now());temp.setStatus(0);}BeanUtil.copyProperties(blog, temp, "id", "userId", "created", "status");blogService.saveOrUpdate(temp);return Result.succ(null);}}

    11.2.實體類校驗

    package com.gblfy.entity;import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import java.time.LocalDateTime; import java.io.Serializable;import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors;import javax.validation.constraints.NotBlank;/*** <p>** </p>** @author gblfy* @since 2020-06-02*/ @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("m_blog") public class Blog implements Serializable {private static final long serialVersionUID = 1L;@TableId(value = "id", type = IdType.AUTO)private Long id;private Long userId;@NotBlank(message = "標題不能為空")private String title;@NotBlank(message = "摘要不能為空")private String description;@NotBlank(message = "內(nèi)容不能為空")private String content;@JsonFormat(pattern="yyyy-MM-dd")private LocalDateTime created;private Integer status;}

    11.3. 博文接口測試

    GET http://localhost:8080/blogs

    GET http://localhost:8080/blog/10

    POST http://localhost:8080/blog/edit

    { "title": "博文標題2222222222222222222", "description": "博文摘要2222222222222222", }


    從上面測試得出@Validated注解的優(yōu)先級高于@RequiresAuthentication注解
    2.測試請求頭中不攜帶token

    { "title": "博文標題2222222222222222222", "description": "博文摘要2222222222222222", "content": "博文內(nèi)容222222222222222222222" }


    3.測試請求頭中攜帶token測試

    注:這個Authorization 的值是通過登錄接口返回給用戶的,簡言之,要先訪問http://localhost:8080/login接口獲取token的值

    4.攜帶token添加博文測試
    無id即可

    { "title": "博文標題2222222222222222222", "description": "博文摘要2222222222222222", "content": "博文內(nèi)容222222222222222222222" }


    5.攜帶token編輯博文測試

    { "id": 11, "title": "博文標題55555", "description": "博文摘要55555", "content": "博文內(nèi)容555555" }


    特別注意:Authorization后面不能有空格,token的值要復制全,這個很重要,這個錯誤不好定位!

    12. 后端接口開發(fā)總結

    總結

    以上是生活随笔為你收集整理的基于SpringBoot+Vue开发的前后端分离博客项目-Java后端接口开发的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。