Spring Security 微服務權限管理
Spring Security:作為一個基於Spring的優秀Web網站安全框架,除了能在單體的微服務中對權限進行攔截,我們還可以應用到微服務架構中。實現單點登錄權限授權等功能。
在Spring Security中實現微服務的權限管理,最好結合 JWT
,那么接下來我們先了解一下什么是JWT
JWT(Json Web Token)
概述
Json web token (JWT)
, 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標准((RFC 7519).該token被設計為緊湊且安全的,特別適用於分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
起源
在Web安全認證中經常用到的就是傳統的基於Session
的安全驗證,和新崛起的JWT
傳統的Session認證:
我們都知道Http
請求是一種無狀態的請求協議,當用戶發送請求時我們,並不知道是誰發送的請求,那么在傳統的方案中,用戶在第一次請求時就會要求用戶就行授權登錄,授權成功服務器就會將請求的用戶信息保存在Session
中,並且給用戶響應一個加密的Cookie
,下次訪問時帶上Cookie
,服務器通過解析Cookie
,來判斷用戶是否登錄。但是基於Session的保存方式隨之互聯網的發展,會漸漸的出現很多問題。
Session認證 缺點
:
- Session的保存位置是在內存中,當網站的用戶量增大時,服務器的大量內存都用來保存Session,那么提供資源的服務也會降低,速度減慢。
- 在如今的微服務架構中,Session是保存在一台服務器的內存中,然而互聯網的發展,微服務集群,服務拆分都是常事,那么對用戶在訪問時不同服務器都需要授權。擴展能力差。
CSRF
存放在Cookies中,那么當Cookies被盜取時,很可能會受到黑客的偽裝請求攻擊,導致服務器宕機。
基於JWT的授權認證
基於JWT
的授權認證,他不需要服務器存放用戶信息,而是將用戶請求的信息保存到數據庫或者(NoSQL數據庫),推薦一款超級快的Redis
的NoSQL數據庫。具體實現原理如下。
- 用戶在第一次請求的時候,需要經行安全授權驗證。
- 驗證成功后服務器會將用戶授權的信息利用
JWT
加密生成Token
, - 將
Token
響應給用戶,並且將Token
保存到Redis中(可以是數據庫,Redis基於內存更快)。 - 用戶將接收到的
Token
保存起來(前端工程師完成)。 - 第二次請求時,只需要在請求頭中攜帶
Token
,訪問即可。 - 瀏覽器在接收到請求后,利用
Redis
,返回保存的Token
,並經行校驗。校驗成功就放行 - 利用
Redis
,可以解決多台服務器之間的授權問題,實現單點登錄
(單點登錄就是解決Session缺點的第二點的)
JWT組成
JWT的實際形式使用三部分組成,並通過 .
來分割
# 主要組成的形式
xxxxx.yyyyy.zzzzz
這三部分分別是Header
、Payload
、Signature
- Header(頭),
typ
:屬於什么類型的加密、alg
:加密方法 。通過對上面的定義在通過Base64Url
編碼形成第一部分。
{
'typ': 'JWT'
'alg': 'HS256'
}
-
Payload(載荷),攜帶的參數,主要分為
sub
: 標准的聲明name
:公共的聲明admin
:私有的聲明
通過對上面的定義在通過
Base64Url
編碼形成第二部分。
{
'sub': '123456'
'name': 'john'
'admin': true
}
- Signature,是對上面兩個加密的結果在經行加鹽加密
- 簡單來說就是利用上面兩個部分的加密結果,在經行加鹽加密,形成第三部分
var encodestring = base64UrlEncode(Header)+`.`+base64UrlEncode(payload)
var signature = HMACSHA256(encodestring, 'salt')
JWT的使用
-
構建一個
Maven
項目 -
導入以下依賴
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!--JDK 1.8 以上需要加入以下依賴--> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency>
-
測試代碼
import io.jsonwebtoken.*; import java.util.Date; import java.util.UUID; public class JWT { public static void main(String[] args) { String salt = "salt"; // 加密的鹽 JwtBuilder jwtBuilder = Jwts.builder(); String jwtToken = jwtBuilder // header 設置 .setHeaderParam("typ","JWT") .setHeaderParam("alg","HS256") // Payload 設置 .claim("username","tom") .claim("role","admin") .setSubject("admin-test") // 過期時間 .setExpiration(new Date(System.currentTimeMillis()+(1000*60*60*24))) // 設置 id .setId(UUID.randomUUID().toString()) // Signature 設置,主要是加密方式,和加鹽參數 .signWith(SignatureAlgorithm.HS256,salt) // 拼接 .compact(); System.out.println(jwtToken); // 調用解析方法 JWT.jwtDecrypt(jwtToken,salt); } public static void jwtDecrypt(String encryptionPassword,String salt){ // 傳入加鹽的參數,和加密對象 Jws<Claims> claimsJws = Jwts.parser().setSigningKey(salt).parseClaimsJws(encryptionPassword); // 解析Body(Payload)部分的值 Claims body = claimsJws.getBody(); System.out.println(body.get("username")); System.out.println(body.get("role")); System.out.println(body.getId()); System.out.println(body.getExpiration()); // 解析頭部的值 Header header = claimsJws.getHeader(); System.out.println(header.get("typ")); System.out.println(header.get("alg")); } }
基於Spring Security和JWT的微服務權限校驗
數據庫准備
-
在數據庫終准備三個表,分別是
用戶表
、角色表
、用戶角色關系表
。 -
建表語句如下
CREATE TABLE `jwt_role` ( `id` char(19) NOT NULL DEFAULT '' COMMENT '角色id', `role_name` varchar(20) NOT NULL DEFAULT '' COMMENT '角色名稱', `role_code` varchar(20) DEFAULT NULL COMMENT '角色編碼', `remark` varchar(255) DEFAULT NULL COMMENT '備注', `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '邏輯刪除 1(true)已刪除, 0(false)未刪除', `version` int(1) NOT NULL DEFAULT 1 COMMENT '版本控制,樂觀鎖', `create_time` datetime NOT NULL COMMENT '創建時間', `update_time` datetime NOT NULL COMMENT '更新時間', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `jwt_user` ( `id` char(19) NOT NULL COMMENT '用戶id', `username` varchar(20) NOT NULL DEFAULT '' COMMENT '用戶名', `password` varchar(32) NOT NULL DEFAULT '' COMMENT '用戶密碼', `salt` varchar(100) NOT NULL DEFAULT '' COMMENT '密碼加密鹽', `nick_name` varchar(50) DEFAULT NULL COMMENT '昵稱', `head_portrait` varchar(255) DEFAULT NULL COMMENT '用戶頭像', `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '邏輯刪除 1(true)已刪除, 0(false)未刪除', `version` int(1) NOT NULL DEFAULT 1 COMMENT '版本控制,樂觀鎖', `create_time` datetime NOT NULL COMMENT '創建時間', `update_time` datetime NOT NULL COMMENT '更新時間', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用戶表'; CREATE TABLE `jwt_user_role` ( `id` char(19) NOT NULL DEFAULT '' COMMENT '主鍵id', `role_id` char(19) NOT NULL DEFAULT '0' COMMENT '角色id', `user_id` char(19) NOT NULL DEFAULT '0' COMMENT '用戶id', `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '邏輯刪除 1(true)已刪除, 0(false)未刪除', `version` int(1) NOT NULL DEFAULT 1 COMMENT '版本控制,樂觀鎖', `create_time` datetime NOT NULL COMMENT '創建時間', `update_time` datetime NOT NULL COMMENT '更新時間', PRIMARY KEY (`id`), KEY `idx_role_id` (`role_id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; insert into jwt_user(id, username, password, salt, nick_name, head_portrait, deleted, version, create_time, update_time) VALUES (1,'admin','adbc396f4cd1b6b1f41967bd8a857dcc','admin','橘子有點甜',null,0,1,now(),now()), (2,'admin1','adbc396f4cd1b6b1f41967bd8a857dcc','admin','橘子有點甜2',null,0,1,now(),now()) insert into jwt_role (id, role_name, role_code, remark, deleted, version, create_time, update_time) VALUES (1,'管理員','admin','可以管理所有模塊',0,1,now(),now()),(2,'測試員','test','可以管理測試模塊',0,1,now(),now()) insert into jwt_user_role (id, role_id, user_id, deleted, version, create_time, update_time) VALUES (1,1,1,0,1,now(),now()),(2,2,1,0,1,now(),now()),(3,2,2,0,1,now(),now())
注冊中心准備(Nacos)
Nacos 如何安裝和啟動,參考如下教程:Nacos 安裝啟動教程
NoSQL數據庫准備(Redis)
Redis如何安裝啟動,參考如下教程:Redis安裝教程
安裝完成,(如果是遠程服務器)默認我們的項目是連接不了的,需要修改一些配置,參考如下地址修改配置后重新啟動
https://www.cnblogs.com/swda/p/12013439.html
項目搭建
創建父工程
-
創建一個空的
Maven
項目 -
刪除
src
目錄 -
導入如下依賴
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.wyx</groupId> <artifactId>SpringSecurity-JWT</artifactId> <packaging>pom</packaging> <version>1.0</version> <properties> <project.build.sourceEmcoding>UTF-8</project.build.sourceEmcoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <!--對應的版本--> <spring.boot.dependencies.version>2.4.3</spring.boot.dependencies.version> <spring.cloud.dependencies.version>2020.0.2</spring.cloud.dependencies.version> <spring.cloud.alibaba.dependencies.version>2.2.1.RELEASE</spring.cloud.alibaba.dependencies.version> <mysql.version>8.0.23</mysql.version> <log4j.version>1.2.17</log4j.version> <junit.version>4.13</junit.version> <lombok.version>1.18.20</lombok.version> <mybatis.plus.boot.starter.version>3.4.2</mybatis.plus.boot.starter.version> <mybatis.plus.generator.version>3.4.1</mybatis.plus.generator.version> <velocity.version>2.2</velocity.version> <jwt.version>0.9.1</jwt.version> <swagger.version>2.9.2</swagger.version> <hutool.version>5.7.2</hutool.version> </properties> <dependencyManagement> <dependencies> <!--SpringBoot 依賴--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot.dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--Spring Cloud 依賴--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring.cloud.dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--Spring Cloud Alibaba 依賴--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring.cloud.alibaba.dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--Mysql 連接驅動--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!--Lombok 依賴--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> <!--log4j 依賴--> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version> </dependency> <!--junit 單元測試 依賴--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> </dependency> <!--mybatis-plus整合SpringBoot依賴--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis.plus.boot.starter.version}</version> </dependency> <!--代碼生成器依賴--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>${mybatis.plus.generator.version}</version> </dependency> <!-- velocity 模板引擎, Mybatis Plus 代碼生成器需要 --> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>${velocity.version}</version> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>${jwt.version}</version> </dependency> <!--swagger--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger.version}</version> </dependency> <!--swagger ui--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger.version}</version> </dependency> <!--HuTool工具包--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.4.4</version> <configuration> <fork>true</fork> <addResources>true</addResources> </configuration> </plugin> </plugins> </build> </project>
創建工具包API接口
-
創建新模塊 common-api
-
導入pom.xml
<dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> </dependencies>
-
編寫工具類
統一返回類型
package com.wyx.utils; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.HashMap; @Data @NoArgsConstructor @AllArgsConstructor public class CommonResult<T> { private Integer code; private String message; private T data; public static CommonResult Success(Object data){ return new CommonResult(200,"success",data); } public static CommonResult Error(){ HashMap<String, String> map = new HashMap<>(); map.put("warning",String.valueOf("請求出錯,請聯系管理員")); return new CommonResult(444,"error",map); } public static CommonResult isNull(){ HashMap<String, String> map = new HashMap<>(); map.put("warning",String.valueOf("請求數據為空,請聯系管理員")); return new CommonResult(404,"error",map); } public static CommonResult perNoAll(){ HashMap<String, String> map = new HashMap<>(); map.put("message",String.valueOf("權限不允許,請聯系管理員")); return new CommonResult(401,"warning",map); } public static CommonResult ErrorMethod(){ HashMap<String, String> map = new HashMap<>(); map.put("message",String.valueOf("請求方法錯誤,請查看請求方法")); return new CommonResult(403,"warning",map); } public static CommonResult Exception(){ HashMap<String, String> map = new HashMap<>(); map.put("message",String.valueOf("服務器內部錯誤,請聯系管理員")); return new CommonResult(500,"error",map); } public static CommonResult MyMessage (String message){ HashMap<String, String> map = new HashMap<>(); map.put("message",message); return new CommonResult(911,"warning",map); } }
密碼加密工具類
package com.wyx.utils; import cn.hutool.crypto.digest.DigestUtil; /** * 對傳入的密碼經行加密 * @ClassName UserPasswordEncryption * @Description TODO * @Author 王玉星 * @Date 2021/8/3 13:57 * @Version 1.0 */ public class PasswordEncryption { /** * 使用 MD5 方式對用戶傳入的密碼經行加密 * @param password 用戶傳入的密碼 * @param salt 加密的鹽,最少要 4 位數 * * @return 使用用戶密碼加上加密鹽,加密后的MD5值 */ public static String encryption(String password, String salt){ String saltPassword = salt.charAt(2)+salt.charAt(3)+password+salt.charAt(0)+salt.charAt(1); return DigestUtil.md5Hex(saltPassword); } }
JWT
加密解密類package com.wyx.utils; import io.jsonwebtoken.*; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; /** * 對用戶信息利用JWT的方式設置加密解密功能 * @ClassName LoginToken * @Description None * @Author 王玉星 * @Date 2021/8/1 14:12 * @Version 1.0 */ public class TokenManager { //推薦用公司名 private static String SALT = "con.wyx"; // 過期時間 7 天 private static Integer EXPIRE_TIME = 1000*60*60*24*7; public static String encryption(Long id, String username){ return encryption(id,username,""); } /** * 加密用戶信息函數 * @param id 用戶Id * @param username 用戶姓名 * @param role 用戶角色,輸入時用,隔開。代表多個角色,形如 "admin,user" * * @return 經過JWT加密后的密文 */ public static String encryption(Long id, String username, String role){ String[] roles = role.split(","); JwtBuilder jwtBuilder = Jwts.builder(); String jwtPassword = jwtBuilder // header 設置 .setHeaderParam("typ","JWT") .setHeaderParam("alg","HS256") // Payload 設置 .claim("username",username) .claim("role",roles) .setExpiration(new Date(System.currentTimeMillis()+EXPIRE_TIME)) // 設置 id .setId(String.valueOf(id)) // Signature 設置,主要是加密方式,和加鹽參數 .signWith(SignatureAlgorithm.HS256,SALT) .compact(); return jwtPassword; } /** * 解析一個加密的密文,並將他封裝成 Map 對象返回, * username:用戶名, * roles:角色, * id:用戶ID, * expireTime:過期時間, * head_typ:加密類型, * head_alg:加密方式, * @param jwtPassword 加密的密文 * * @return 封裝了用戶加密信息的 map 對象 */ public static HashMap jwtDecrypt(String jwtPassword){ HashMap<String, Object> decryMap = new HashMap<>(); // 傳入加鹽的參數,和加密對象,這里切記不能定義變量,一行寫完 Jws<Claims> claimsJws = Jwts.parser().setSigningKey(SALT).parseClaimsJws(jwtPassword); // 解析Body(Payload)部分的值 Claims body = claimsJws.getBody(); decryMap.put("username", body.get("username")); decryMap.put("roles", body.get("role")); decryMap.put("id", body.getId()); decryMap.put("expireTime", body.getExpiration()); // 解析頭部的值 Header header = claimsJws.getHeader(); decryMap.put("head_typ",header.get("typ")); decryMap.put("head_alg",header.get("alg")); return decryMap; } }
如果自己還有工具包,可以往這個項目中放,后期好調用
,這里只是給出幾個簡單常用的工具包。
創建配置類接口
該項目主要是用於對Spring容器的一些配置,比如Redis的操作模板,Swagger配置,Mybatis-Plus的配置,等等,許多配置都可以放到這里面來,方便后期管理,主要是配置進入Spring容器的
-
創建項目Configuration-API
-
導入依賴,依賴根據自己在項目配置中需要什么依賴,就導入什么依賴,當前自需要配置以下三個,如果有更多配置,修改依賴即可
<dependencies> <dependency> <groupId>com.wyx</groupId> <artifactId>common-api</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Security--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--對Redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--swagger--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency> <!--Mybatis-Plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <!--引入swagger bootstrap ui--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> </dependency> </dependencies> <!--資源過濾器--> <build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> </resources> </build>
-
Mybatis-Plus自動填充注解
package com.wyx.MybatisPlusConfig; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.util.Date; /** * Mybatis-Plus 中的字段填充配置類 */ @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { // 執行插入操作時,查找對應的字段名,和給它更新的值,並將元數據帶入 this.setFieldValByName("createTime",new Date(),metaObject); this.setFieldValByName("updateTime",new Date(),metaObject); } @Override public void updateFill(MetaObject metaObject) { this.setFieldValByName("updateTime",new Date(),metaObject); } }
Mybatis-Plus樂觀鎖,分頁插件配置類
package com.wyx.MybatisPlusConfig; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @MapperScan("com.wyx.mapper,com.wyx.SpringSecurityConfig.mapper") //@MapperScan("com.wyx.mapper") 開始時是配置在主啟動類上,當我們創建Mybatis-Plus的配置類時,一般情況下把它移動到我們的配置類上 public class MybatisPlusConfig { /** * 舊版 @Bean public OptimisticLockerInterceptor optimisticLockerInterceptor() { return new OptimisticLockerInterceptor(); } // 舊版 @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); // 設置請求的頁面大於最大頁后操作, true調回到首頁,false 繼續請求 默認false // paginationInterceptor.setOverflow(false); // 設置最大單頁限制數量,默認 500 條,-1 不受限制 // paginationInterceptor.setLimit(500); // 開啟 count 的 join 優化,只針對部分 left join paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true)); return paginationInterceptor; } */ /** * 樂觀鎖插件,分頁插件 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); // 樂觀鎖 mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 分頁插件 mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2)); return mybatisPlusInterceptor; } }
-
Swagger配置類
package com.wyx.SwaggerConfig; import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI; import com.google.common.base.Predicates; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 @EnableSwaggerBootstrapUI public class Swagger2 { //默認文檔地址(swagger-ui)為 http://localhost:端口號/swagger-ui.html //默認文檔地址(bootstrap-ui)為 http://localhost:端口號/doc.html @Bean public Docket createRestApi(){ return new Docket(DocumentationType.SWAGGER_2) //指定Api類型為Swagger2 .apiInfo(apiInfo()) //指定文檔匯總信息 .select() .apis(RequestHandlerSelectors .basePackage("com.wyx.controller")) //指定controller包路徑 //.paths(PathSelectors.any()) //指定展示所有controller .paths(Predicates.not(PathSelectors.regex("/error.*"))) // 排除哪些路徑不掃描 .build(); } private ApiInfo apiInfo(){ //返回一個apiinfo return new ApiInfoBuilder() .title("api接口文檔") //文檔頁標題 .contact( new Contact( "王玉星", "https://www.cnblogs.com/Rampant/", "309597117@qq.com") ) // 聯系人信息 .description("api文檔") // 詳細信息 .version("1.0.1") // 文檔版本號 .termsOfServiceUrl("https://www.cnblogs.com/Rampant/") //網站地址 .build(); } }
-
Redis
模板和緩存配置package com.wyx.RedisConfig; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; @EnableCaching //開啟緩存 @Configuration //配置類 public class RedisConfig extends CachingConfigurerSupport { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); template.setConnectionFactory(factory); //key序列化方式 template.setKeySerializer(redisSerializer); //value序列化 template.setValueSerializer(jackson2JsonRedisSerializer); //value hashmap序列化 template.setHashValueSerializer(jackson2JsonRedisSerializer); return template; } @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); //解決查詢緩存轉換異常的問題 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); // 配置序列化(解決亂碼的問題),過期時間600秒 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(600)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); return cacheManager; } }
-
異常處理類
package com.wyx.exceptionhandler; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor //生成有參數構造方法 @NoArgsConstructor //生成無參數構造 public class MyException extends RuntimeException { private Integer code;//狀態碼 private String msg;//異常信息 }
package com.wyx.exceptionhandler; import com.wyx.utils.CommonResult; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; @ControllerAdvice @Slf4j public class GlobalExceptionHandler { //指定出現什么異常執行這個方法 @ExceptionHandler(Exception.class) @ResponseBody //為了返回數據 public CommonResult error(Exception e) { log.error("處理異常"); e.printStackTrace(); return CommonResult.Error(); } //特定異常 @ExceptionHandler(ArithmeticException.class) @ResponseBody //為了返回數據 public CommonResult error(ArithmeticException e) { log.error("算術處理異常"); e.printStackTrace(); return CommonResult.Error(); } //自定義異常 @ExceptionHandler(MyException.class) @ResponseBody //為了返回數據 public CommonResult error(MyException e) { log.error(e.getMessage()); e.printStackTrace(); return CommonResult.Error(); } }
-
SpringBoot自定義錯誤返回
package com.wyx.springbootconfig; import com.wyx.utils.CommonResult; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; /** * @ClassName MyErrorController * @Description TODO * @Author 王玉星 * @Date 2021/8/8 23:42 * @Version 1.0 */ @RestController public class MyErrorController implements ErrorController { @RequestMapping("/error") public CommonResult handleError(HttpServletRequest request){ //獲取statusCode:401,404,500 Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code"); if(statusCode == 401){ return CommonResult.perNoAll(); }else if(statusCode == 404){ return CommonResult.isNull(); }else if(statusCode == 403){ return CommonResult.ErrorMethod(); }else{ return CommonResult.MyMessage("出現了未知異常,請聯系管理員"); } } @Override public String getErrorPath() { return "/error"; } }
SpringSecurity安全配置(重點)
-
自定義SpringSecurity加密配置,如果時用戶注冊時請調用
encodeRandom
方法,隨機生成加密密文,和加密鹽package com.wyx.SpringSecurityConfig; import com.wyx.utils.PasswordEncryption; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.Random; /** * 自定義密碼加密功能 * */ @Component public class MyPasswordEncoder implements PasswordEncoder { private String salt; @Autowired HttpServletRequest request; public MyPasswordEncoder() { this(-1); } public MyPasswordEncoder(int strength) { } /** * 用於用戶注冊時使用 * 對密碼使用隨機的密碼經行加密,返回加密的值,和加密鹽,返回形式如下:“加密值,加密的隨機鹽” * @param rawPassword 傳入密碼 * * @return 返回加密的密碼和隨機生成的鹽,兩者通過,分開。 */ public String encodeRandom(CharSequence rawPassword) { String randomSalt = setRandomSalt(); return PasswordEncryption.encryption((String) rawPassword,randomSalt)+","+randomSalt; } /** * 對密碼經行加密的函數 * @param rawPassword 輸入的密碼 * * @return 加密后返回的密碼 */ @Override public String encode(CharSequence rawPassword) { setSalt(); return PasswordEncryption.encryption((String) rawPassword,getSalt()); } /** * 對輸入的密碼與數據庫密碼經行對比 * @param rawPassword 輸入的密碼 * @param encodedPassword 數據庫中查出來的密碼 * * @return 返回對比結果true false */ @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return encodedPassword.equals((String.valueOf(rawPassword))); } /** * 放回隨機加密的鹽 * @return String 隨機加密的鹽 */ public String setRandomSalt(){ String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; Random random=new Random(); StringBuffer sb=new StringBuffer(); for(int i=0;i<6;i++){ int number=random.nextInt(62); sb.append(str.charAt(number)); } return sb.toString(); } public String getSalt() { return salt; } public void setSalt() { this.salt = (String) request.getAttribute("loginSalt"); } }
-
權限不通過的處理類
package com.wyx.SpringSecurityConfig; import lombok.extern.java.Log; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Log public class UnauthEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletRequest.getRequestDispatcher("/authority/noAuthority").forward(httpServletRequest,httpServletResponse); httpServletRequest.getServletPath(); log.warning("請求路徑:"+httpServletRequest.getServletPath()+",權限不允許"); } }
-
登錄授權用戶實體類
package com.wyx.SpringSecurityConfig.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import java.io.Serializable; /** * @ClassName SecurityUserMapper * @Description TODO * @Author 王玉星 * @Date 2021/8/6 17:33 * @Version 1.0 */ @Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode(callSuper = false) @TableName("jwt_user") @ApiModel(value="User對象", description="用戶表") public class SecurityUser implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty(value = "用戶id") @TableId(value = "id", type = IdType.ASSIGN_ID) private String id; @ApiModelProperty(value = "用戶名") private String username; @ApiModelProperty(value = "用戶密碼") private String password; @ApiModelProperty(value = "密碼加密鹽") private String salt; }
-
登錄查詢接口
package com.wyx.SpringSecurityConfig.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.wyx.SpringSecurityConfig.pojo.SecurityUser; import java.util.List; /** * <p> * 登錄用戶 Mapper 接口 * </p> * * @author 王玉星 * @since 2021-08-03 */ public interface SecurityUserMapper extends BaseMapper<SecurityUserMapper> { SecurityUser getSecurityUserByUserName(String username); List<String> getRoleListByUserName(String username); }
-
自定義登錄查詢
Mapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.wyx.SpringSecurityConfig.mapper.SecurityUserMapper"> <resultMap id="securityUser" type="com.wyx.SpringSecurityConfig.pojo.SecurityUser"> <result column="id" property="id"/> <result column="password" property="password"/> <result column="salt" property="salt"/> <result column="username" property="username"/> </resultMap> <select id="getSecurityUserByUserName" resultMap="securityUser" parameterType="string"> select id,username,salt,password from jwt_user where username = #{username} </select> <select id="getRoleListByUserName" resultType="java.lang.String"> select role_code from jwt_role where id in (select role_id from jwt_user_role where user_id in (select id from jwt_user where username = #{username}) ) </select> </mapper>
-
授權處理器
package com.wyx.SpringSecurityConfig; import com.wyx.exceptionhandler.MyException; import com.wyx.utils.CommonResult; import com.wyx.utils.TokenManager; import lombok.extern.java.Log; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; public class TokenAuthFilter extends BasicAuthenticationFilter { private RedisTemplate redisTemplate; private AuthenticationManager authenticationManager; public TokenAuthFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate) { super(authenticationManager); this.redisTemplate = redisTemplate; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { //獲取當前認證成功用戶權限信息 UsernamePasswordAuthenticationToken authRequest = getAuthentication(request); //判斷如果有權限信息,放到權限上下文中 if(authRequest != null) { SecurityContextHolder.getContext().setAuthentication(authRequest); } chain.doFilter(request,response); } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { //從header獲取token String token = request.getHeader("token"); if(token != null) { HashMap tokenMap = null; try { tokenMap = TokenManager.jwtDecrypt(token); }catch (RuntimeException e){ logger.error("令牌已過期"); return null; } String username = (String) tokenMap.get("username"); if (!redisTemplate.opsForValue().get(username).equals(token)){ System.out.println(token); System.out.println(redisTemplate.opsForValue().get(username)); logger.error("令牌錯誤"); return null; } ArrayList roles = (ArrayList) tokenMap.get("roles"); StringBuilder authRoles = new StringBuilder(); for (Object role : roles) { role = "ROLE_"+role.toString(); authRoles.append(role); authRoles.append(","); } // 消除最后一個分號 authRoles.replace(authRoles.length()-1,authRoles.length(),""); /* //從redis獲取對應權限列表 List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username); Collection<GrantedAuthority> authority = new ArrayList<>(); for(String permissionValue : permissionValueList) { SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue); authority.add(auth); }*/ List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(String.valueOf(authRoles)); // return new UsernamePasswordAuthenticationToken(tokenMap.get("username"),token,authority); // 已經通過令牌認證成功,密碼設置為空即可 User user = new User(username,"",authorityList); return new UsernamePasswordAuthenticationToken(user,token,authorityList); } return null; } }
-
登錄過濾器
package com.wyx.SpringSecurityConfig; import com.wyx.SpringSecurityConfig.mapper.SecurityUserMapper; import com.wyx.SpringSecurityConfig.pojo.SecurityUser; import com.wyx.utils.PasswordEncryption; import com.wyx.utils.TokenManager; import lombok.extern.java.Log; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Random; import java.util.concurrent.TimeUnit; @Log public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private final RedisTemplate redisTemplate; private final SecurityUserMapper securityUserMapper; private final AuthenticationManager authenticationManager; public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate, SecurityUserMapper securityUserMapper) { this.redisTemplate = redisTemplate; this.authenticationManager = authenticationManager; this.securityUserMapper = securityUserMapper; this.setPostOnly(false); this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login","POST")); } //1 獲取表單提交用戶名和密碼 @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //獲取表單提交數據 //使用流方式獲取表單信息 //Users user = new ObjectMapper().readValue(request.getInputStream(), Users.class); //User users = new User(request.getParameter("username"), request.getParameter("password")); // 設置鹽,從數據庫中獲取 SecurityUser user = securityUserMapper.getSecurityUserByUserName(request.getParameter("username")); // 將密碼修改為用戶輸入的 user.setPassword(request.getParameter("password")); // 將加密的鹽保存到請求中 request.setAttribute("loginSalt",user.getSalt()); return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), PasswordEncryption.encryption(user.getPassword(),user.getSalt()), new ArrayList<>())); } //2 認證成功調用的方法 @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) { //認證成功,得到認證成功之后用戶信息 User user = (User) authResult.getPrincipal(); //查詢數據庫獲取用戶ID 用戶名,用戶權限。 String username = user.getUsername(); StringBuilder roles = new StringBuilder(); for (GrantedAuthority authority : authResult.getAuthorities()) { roles.append(authority); roles.append(","); } // 消除最后一個分號 if (roles.length()>=1){ roles.replace(roles.length()-1,roles.length(),""); } String token = TokenManager.encryption(Long.valueOf(new Random().nextInt()),username, String.valueOf(roles)); //把用戶名稱和用戶權限列表放到redis,並設置過期時間 HashMap tokenMap = TokenManager.jwtDecrypt(token); Date date = (Date) tokenMap.get("expireTime"); redisTemplate.opsForValue().set(username,token,(date.getTime()-new Date().getTime())/1000, TimeUnit.SECONDS); //返回token response.setHeader("token",token); } //3 認證失敗調用的方法 protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws ServletException, IOException { request.getRequestDispatcher("/authority/AuthenticationFailed").forward(request,response); log.warning("認證失敗,請重新認證"); } }
-
退出登錄處理器
package com.wyx.SpringSecurityConfig; import com.wyx.utils.TokenManager; import lombok.extern.java.Log; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; //退出處理器 @Log public class TokenLogoutHandler implements LogoutHandler { @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { // 功能較為簡單,只要請求頭包含token就能退出,后期根據自己的業務需求更改 String token = request.getHeader("token"); if(token != null) { // 從 token中獲取用戶名 HashMap tokenMap = TokenManager.jwtDecrypt(token); try { response.sendRedirect("/authority/logoutOk"); } catch (IOException e) { e.printStackTrace(); } log.info("用戶退出登錄成功"); // 在 redis 上刪除token,可以刪除,也可以不刪。 // redisTemplate.delete(tokenMap.get("username")); }else { try { request.getRequestDispatcher("/authority/logoutEr").forward(request,response); } catch (ServletException | IOException e) { e.printStackTrace(); } } } }
-
獲取數據庫用戶信息處理器
package com.wyx.SpringSecurityConfig; import com.wyx.SpringSecurityConfig.mapper.SecurityUserMapper; import com.wyx.SpringSecurityConfig.pojo.SecurityUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; @Service("userDetailsService") public class UserDetailsServiceImpl implements UserDetailsService { @Resource SecurityUserMapper securityUserMapper; @Autowired MyPasswordEncoder myPasswordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SecurityUser user = securityUserMapper.getSecurityUserByUserName(username); List<String> roleListByUserName = securityUserMapper.getRoleListByUserName(username); // 設置權限,通過逗號分割 StringBuilder role = new StringBuilder(); for (String s : roleListByUserName) { role.append("ROLE_"+s); role.append(","); } //消除最后一個分號 if (role.length()>=1){ role.replace(role.length()-1,role.length(),""); }else { role.append(""); } List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(String.valueOf(role)); SecurityUser securityUser = new SecurityUser(user.getId(),user.getUsername(), user.getPassword(),user.getSalt()); if (securityUser.getUsername()==null){ throw new UsernameNotFoundException("用戶名不存在!"); } return new User(securityUser.getUsername(),securityUser.getPassword(),authorityList); } }
-
安全框架總配置
package com.wyx.SpringSecurityConfig; import com.wyx.SpringSecurityConfig.mapper.SecurityUserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import javax.annotation.Resource; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private RedisTemplate redisTemplate; @Autowired private MyPasswordEncoder myPasswordEncoder; @Autowired private UserDetailsService userDetailsService; @Resource SecurityUserMapper securityUserMapper; /** * 配置設置 * @param http * @throws Exception */ //設置退出的地址和token,redis操作地址 @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling() .authenticationEntryPoint(new UnauthEntryPoint())//沒有權限訪問處理器 .and().csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and().logout().logoutUrl("/logout").permitAll()//退出路徑 .addLogoutHandler(new TokenLogoutHandler()).and() //退出登錄處理器 .addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate, securityUserMapper)) //登錄處理器 .addFilter(new TokenAuthFilter(authenticationManager(),redisTemplate)).httpBasic(); //授權處理器 } //調用userDetailsService和密碼處理 @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(myPasswordEncoder); } //不進行認證的路徑,可以直接訪問 @Override public void configure(WebSecurity web) throws Exception { // 配置過了swagger ui 的過濾器 web.ignoring().antMatchers( "/error", "/error/**", "/authority/logoutOk", "/doc.html","/webjars/**", "/v2/api-docs", "/swagger-resources", "/swagger-resources/**", "/configuration/ui", "/configuration/security", "/swagger-ui.html/**", "/csrf","/" ); } }
-
權限信息返回控制器
package com.wyx.SpringSecurityConfig.controller; import com.wyx.utils.CommonResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletResponse; /** * @ClassName AuthorityController * @Description TODO * @Author 王玉星 * @Date 2021/8/7 18:10 * @Version 1.0 */ @RestController @RequestMapping("/authority") public class AuthorityController { // 無權限訪問響應 @RequestMapping("/noAuthority") public CommonResult noAuthority(){ return CommonResult.perNoAll(); } // 認證失敗響應 @RequestMapping("/AuthenticationFailed") public CommonResult AuthenticationFailed(){ return CommonResult.MyMessage("認證失敗,用戶名或密碼錯誤"); } // 退出成功處理器 @RequestMapping("/logoutOk") public CommonResult logoutOk(HttpServletResponse response){ // 給前端一個空的請求頭,讓前端將請求頭參數設置為空即可 response.setHeader("token",""); return CommonResult.Success("退出登錄成功😀😀😀"); } // 退出失敗處理器 @RequestMapping("/logoutEr") public CommonResult logoutEr(){ return CommonResult.MyMessage("退出登錄失敗 😢😢😢"); } }
-
跨域請求偽造處理
package com.wyx.csrfconfig; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsWebFilter; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import org.springframework.web.util.pattern.PathPatternParser; @Configuration public class CorsConfig { //解決跨域 @Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedMethod("*"); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); source.registerCorsConfiguration("/**",config); return new CorsWebFilter(source); } }
服務提供者搭建
-
創建新模塊 Service-Provider-Main-9110 Service-Provider-Main-9111,兩者搭建都一樣
-
導入依賴
<dependencies> <dependency> <groupId>com.wyx</groupId> <artifactId>common-api</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>com.wyx</groupId> <artifactId>Configuration-API</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!--swagger--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> </dependency> <!-- velocity 模板引擎, Mybatis Plus 代碼生成器需要 --> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies>
-
application.yaml
server: port: 9110 spring: application: name: Service-Provider datasource: url: jdbc:mysql://47.97.218.81:3306/Security_JWT?useSSL=true&useUnicode=true&charterEncoding=utf8&serverTimezone=GMT%2B8 password: 970699 username: root driver-class-name: com.mysql.cj.jdbc.Driver hikari: pool-name: DateHikariCP # 連接池名 minimum-idle: 5 # 最小空閑連接數 idle-timeout: 18000 # 空閑連接最大存活時間 單位毫秒 maximum-pool-size: 10 # 最大連接數 auto-commit: true # 是否自動提交(返回連接池時) max-lifetime: 18000 # 連接最大存活時間 單位毫秒 connection-timeout: 3000 # 連接超時時間 單位毫秒 #connection-test-query: SELECT 1 # 測試連接是否可以用的查詢語句 cloud: nacos: discovery: server-addr: 47.97.218.81:8848 # 連接的注冊中心 redis: host: 47.97.218.81 # redis 地址 port: 6379 # redis 端口號 database: 0 # 使用數據庫 password: 970699 # 連接密碼 timeout: 1800000 #超時時間(毫秒) lettuce: pool: #連接池 min-idle: 0 # 最小連接 max-idle: 5 #最大連接 max-active: 20 # 連接存活時間 max-wait: -1 # 最大等待時間 jackson: date-format: yyyy-MM-dd HH:mm:ss #返回的json 時間日期格式 time-zone: GMT+8 # 設置時區 mybatis-plus: mapper-locations: classpath:mapper/*.xml #mybatis-plus xml的地址 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #配置mybatis-plus的日志輸出 management: endpoints: web: exposure: include: '*' #暴露端口
-
主啟動類
package com.wyx; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @ClassName com.wyx.ServiceProviderMain9110 * @Description TODO * @Author 王玉星 * @Date 2021/8/3 15:11 * @Version 1.0 */ @SpringBootApplication @EnableDiscoveryClient public class ServiceProviderMain9110 { public static void main(String[] args) { SpringApplication.run(ServiceProviderMain9110.class,args); } }
使用Mabatis-plus代碼生成器,生成項目代碼
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import java.util.Arrays;
/**
* @ClassName CodeGeneratorMain
* @Description TODO
* @Author 王玉星
* @Date 2021/7/31 12:07
* @Version 1.0
*/
public class CodeGeneratorMain {
public static void main(String[] args) {
//配置代碼生成器對象
AutoGenerator generator = new AutoGenerator();
// 配置策略
// 1、全局配置
GlobalConfig gc = new GlobalConfig();
//獲取當前項目路徑
String projectPath = System.getProperty("user.dir");
//生成代碼路徑 等於當前項目路徑+模塊路徑+/src/main/java
gc.setOutputDir(projectPath + "/Service-Provider-Main-9110/src/main/java");
gc.setAuthor("王玉星"); //作者信息
gc.setOpen(false); //是否打開資源管理器
gc.setFileOverride(false); //生成文件是否覆蓋
gc.setSwagger2(true); //實體屬性 Swagger2 注解
//去掉Services實現類的I前綴,並在名字后面加上Impl
gc.setServiceImplName("%sServiceImpl");
//去掉Service接口的I前綴
gc.setServiceName("%sService");
gc.setIdType(IdType.ASSIGN_ID); //主鍵生成策略
gc.setDateType(DateType.ONLY_DATE); //設置時間類型
//將全局配置放入代碼生成器對象中
generator.setGlobalConfig(gc);
// 2、數據源配置
DataSourceConfig dsc = new DataSourceConfig();
//連接url
dsc.setUrl("jdbc:mysql://47.97.218.81:3306/Security_JWT?useSSL=true&useUnicode=true&charterEncoding=utf8&serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver"); //連接驅動
dsc.setUsername("root"); //連接名稱
dsc.setPassword("970699"); //連接密碼
dsc.setDbType(DbType.MYSQL); //連接數據庫類型
//將數據源配置放入代碼生成器對象中
generator.setDataSource(dsc);
// 3、包配置
PackageConfig pc = new PackageConfig();
// pc.setModuleName("CodeGenerator"); //生成的模塊
pc.setParent(""); //生成的父路徑設置為空
String classParentPath = "com.wyx."; // 設置類的父路徑
String fileParentPath = ""; // 設置配置文件的父路徑
pc.setEntity(classParentPath+"pojo"); //實體類包
pc.setService(classParentPath+ "service"); //service層包
pc.setController(classParentPath+"controller"); //controller 層包
pc.setMapper(classParentPath+ "mapper"); //mapper 層包
pc.setServiceImpl(classParentPath+"service.impl"); //ServiceImpl層包
pc.setXml(fileParentPath+ "mapper");
//將生成包規則放入代碼生成器對象中
generator.setPackageInfo(pc);
// 4、配置策略
StrategyConfig strategy = new StrategyConfig();
//設置要映射的表,看數據庫的表來(最重要),多張數據庫表逗號來傳遞多個表名
strategy.setInclude("jwt_user","jwt_role","jwt_user_role");
strategy.setTablePrefix("jwt_"); // 移除表前綴
// strategy.setFieldPrefix("role_"); // 移除字段前綴
strategy.setNaming(NamingStrategy.underline_to_camel); //設置命名規則 下划線轉駝峰命名
strategy.setColumnNaming(NamingStrategy.underline_to_camel); //設置命名規則
strategy.setEntityLombokModel(true); //自動使用Lombok
//邏輯刪除策略 的字段名
strategy.setLogicDeleteFieldName("deleted");
// 自動填充配置
TableFill createTime = new TableFill("create_time", FieldFill.INSERT); //自動填充策略配置
TableFill updateTime = new TableFill("update_time", FieldFill.INSERT_UPDATE); //自動更新策略配置
strategy.setTableFillList(Arrays.asList(createTime,updateTime));
//樂觀鎖
strategy.setVersionFieldName("version"); //設置樂觀鎖
//生成 @RestController 控制器 @Controller -> @RestController
strategy.setRestControllerStyle(true);
//駝峰轉連字符 @RequestMapping("/managerUserActionHistory") -> @RequestMapping("/manager-user-action-history")
strategy.setControllerMappingHyphenStyle(true);
//將配置的策略放入代碼生成器對象中
generator.setStrategy(strategy);
generator.execute(); //執行代碼生成器方法
}
}
運行生成完成后,在/src/main/java/
下有一個mapper
的文件夾,將其移動到resources
目錄下,讓其和配置文件application
中的配置路徑一樣。
移動成功后,可以得到如下的目錄結構
業務類編寫
情況說明,這里主要是項目搭建,所以業務類,隨便寫一個接口即可,不多寫
package com.wyx.controller;
import com.wyx.pojo.User;
import com.wyx.service.UserService;
import com.wyx.utils.CommonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 用戶表 前端控制器
* </p>
*
* @author 王玉星
* @since 2021-08-03
*/
@RequiredArgsConstructor
@Data
@RestController
@RequestMapping("/user")
@Api(tags = "用戶信息接口", value = "對用戶信息的增刪改查,等一系列的控制") // 標注這個類的主要作用,tags 在 ui 顯示,value 不顯示
public class UserController {
@NonNull
UserService userService;
// value 表示方法的作用在 ui 上顯示 ,notes 方法的具體描述 , 給方法重新打開一個接口文檔
@ApiOperation(value = "根據id獲取用戶信息", notes = " \n 用戶請求 /get/1 對應獲取用戶ID 為 1 的用戶信息", tags = "查詢單個用戶信息")
// 表示參數的信息,name 參數名,value 參數說明(ui顯示),paramType 請求參數類型(path 路徑參數,query 查詢參數)
// dataType 請求參數的類型,required 是否為必須,默認false
@ApiImplicitParam(name = "id", value = "用戶ID", paramType = "path" ,dataType = "String",required = true)
/*
多參數時使用如下
@ApiImplicitParams(
@ApiImplicitParam(),
@ApiImplicitParam
)
*/
@GetMapping("/get/{id}")
public CommonResult getId(@PathVariable String id){
User user = userService.getById(id);
return (user == null) ? CommonResult.isNull():CommonResult.Success(user);
}
/*
@RequiredArgsConstructor,來源於 lombok
解決我們在注入參數是需要使用
@Autowired
private UserService userService;
我們只需要在類上添加 @RequiredArgsConstructor,在注入是寫出如下 需要注意的是在注入時需要用final定義,或者使用@notnull注解
final UserService userService;
或者
@NonNull
UserService userService;
@RequiredArgsConstructor 的幾個常用參數
1. access 聲明注入的對象的屬性 public private protected(默認 public)
eg : @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
2. staticName 將類中的靜態方法注明,其實在實戰中,使用較少
eg : @RequiredArgsConstructor(staticName = "getId") (默認為空,getId方法名)
3. onConstructor 設置構造器注入時使用的注解
在 jdk1.7之前如下 代表注入構造器注入時,使用 @Autowired注入
eg :onConstructor=@__({@Autowired})
在 jdk1.8之后如下
eg :onConstructor_={@Autowired}
*/
}
啟動測試訪問:
可以訪問如下地址查看自己的相關信息
bootstrap-ui 地址:http://localhost:9110/doc.html
swagger-ui 地址:http://localhost:9110/swagger-ui.html
微服務網關搭建
-
創建項目 SpringCloud-Provider-Gateway-9527
-
導入 pom.xml
<dependencies> <!--Spring-gateway 啟動依賴--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--spring-boot-web 模塊 常用的3個 網關不需要,否則啟動不了--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- <dependency>--> <!-- <groupId>org.springframework.boot</groupId>--> <!-- <artifactId>spring-boot-starter-web</artifactId>--> <!-- </dependency>--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--熱部署插件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <!--測試插件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
修改 yaml
server: port: 9527 spring: application: name: cloud-gateway cloud: nacos: discovery: server-addr: 47.97.218.81:8848 # 連接的注冊中心 gateway: discovery: locator: enabled: true #開啟網關拉取nacos的服務 routes: - id: routh #路由的ID,沒有固定規則但要求唯一,建議配合服務名 uri: lb://Service-Provider #匹配后提供服務的路由地址 predicates: - Path=/gateway/api/** #斷言,路徑相匹配的進行路由 filters: - StripPrefix=2 # 剔除前綴的幾個參數 - AddResponseHeader=X-Response-Default-Foo, Default-Bar # 添加響應頭
-
主啟動類
package com.wyx; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @ClassName SpringCloudProviderGateway9527 * @Description TODO * @Author 王玉星 * @Date 2021/8/8 15:01 * @Version 1.0 */ @EnableDiscoveryClient @SpringBootApplication public class SpringCloudProviderGateway9527 { public static void main(String[] args) { SpringApplication.run(SpringCloudProviderGateway9527.class,args); } }
啟動網關,訪問原來的路徑前面加上/gateway/api/
這里訪問 bootstrap-ui地址為:http://localhost:9527/gateway/api/doc.html
到此:基於微服務的單點登錄
授權,管理完成具體的業務邏輯修改即可
該項目已同步到碼雲
:需要可以下載https://gitee.com/Rampants/SpringSecurity-JWT