以前零零散散的學習過一點鑑權這方面的玩意兒,但自我感受淨他媽整些沒用的,看代碼仍是看不懂,此次咱們再統一對其進行學習一下,但願本身掌握這個技能,也但願屏幕面前的你能有點收穫html
這次的學習週期可能有點長,由於公司加班不穩定,我也不知道啥時候能坐到筆記本前開始筆伐技術,因此在還沒更完的時候,你們就先將就一下,哈哈哈哈...java
以前學習過一點Security + JWT的知識,這裏我推薦你們看一下,對於這次的學習的前期基礎鋪墊也是一個很重要的環節,固然在我噼裏啪啦打完這段文字事後,我也會對其進行一個複習,爭取此次一舉拿下這個讓人頭痛的東西:mysql
固然啦,如今不是微服務基本也是用Security + JWT的方式進行一個權限管理,在上面的複習連接中,拉過代碼的朋友應該都會有那麼一點了解,我這麼就不在作過多的贅述,能夠直接拉代碼看看,我註釋寫得仍是比較詳細github
咱們今天聊的是微服務下的鑑權,微服務你們都知道功能模塊劃分爲單獨的模塊,單獨的部署起來,不可能爲每個劃分的模塊都作權限校驗的啦,站在複用的角度來想的話,咱們確定是要將鑑權服務單獨抽離出來,作成功能模塊的,Security 作權限校驗,JWT解決分佈式Session分離問題,固然也能夠用Redis解決Session分離的問題,但咱們這裏研究的是JWT,因此就沒Session什麼事情了,而後使用的是Oauth2的思想,完成整個微服務功能模塊的保駕護航web
經典案列:Cookie客戶端技術 + Session服務端技術 = 實現SSOredis
在用戶登陸驗證經過以後,咱們將用戶相關信息存在Session,中,並把key塞到Cookie中,下次用戶來訪問咱們的服務,帶着該Cookie來,咱們獲取Cookie中的數據,去Session中查找,若是查找到有值,則免登陸,容許訪問服務算法
spring
這樣一想好像還蠻合適的勒,redis設置數據過時時間不是美滋滋嗎?sql
說到這裏,我好像也舉不出其餘的反面問題了誒,略現尷尬,哈哈哈哈哈...
Cookie中數據的安全問題,想一想也能用加密算法對其進行自定義加密;
Session佔用服務器資源問題,好像問題也不是很大,哈哈哈哈...
言歸正傳,咱們仍是繼續學習,由於生活所迫,所謂鍵盤不敲爛,月薪不過萬,學多點,加強自身複用性,哈哈哈哈...
這次的學習的流程圖,我先畫一下,給你們一個整體的思路歸納:
用戶輸入賬號和密碼,請求登陸,被Zuul攔截,作校驗
Zuul調用受權中心,爲該用戶作驗證
驗證經過後頒發使用私鑰加密的JWT令牌
標記錯誤,不作理會
網關將該令牌塞給客戶端Cookie中
客戶端使用Cookie攜帶該Token去訪問響應服務,通過網關,網關發現令牌合法,放行
微服務使用公鑰對JWT進行解密,獲得用戶數據,完成單點登陸和相應權限控制
建立一個maven工程,這些小操做不作過多贅述,我就貼一下個人pom
<?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.ninja.study</groupId> <artifactId>ninja</artifactId> <version>1.0-SNAPSHOT</version> <!-- 子模塊數據刪掉,大家暫時沒有這些子模塊 --> <modules> <module>ninja-registry</module> <module>ninja-gateway</module> <module>ninja-auto-center</module> </modules> <packaging>pom</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-cloud.version>Finchley.RC1</spring-cloud.version> <mybatis.starter.version>1.3.2</mybatis.starter.version> <mapper.starter.version>2.0.2</mapper.starter.version> <druid.starter.version>1.1.9</druid.starter.version> <mysql.version>5.1.32</mysql.version> <pageHelper.starter.version>1.2.3</pageHelper.starter.version> </properties> <dependencyManagement> <dependencies> <!-- springCloud --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- mybatis啓動器 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.starter.version}</version> </dependency> <!-- 通用Mapper啓動器 --> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>${mapper.starter.version}</version> </dependency> <!-- 分頁助手啓動器 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>${pageHelper.starter.version}</version> </dependency> <!-- mysql驅動 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <!--工具包--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> </project>
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies>
/** * @Description * @Author MSI * @Date 2019/11/5 23:38 **/ @SpringBootApplication @EnableEurekaServer public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class); } }
server: port: 5000 spring: application: name: ninja-eureka eureka: client: fetch-registry: false register-with-eureka: false service-url: defaultZone: http://127.0.0.1:${server.port}/eureka server: enable-self-preservation: false #關閉自我保護
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies>
/** * @Description * @Author MSI * @Date 2019/11/5 23:42 **/ @SpringBootApplication @EnableDiscoveryClient //eureka客戶端 @EnableZuulProxy //網關服務 public class ZuulApplication { public static void main(String[] args) { SpringApplication.run(ZuulApplication.class); } }
server: port: 6000 spring: application: name: ninja-gateway eureka: client: service-url: defaultZone: http://127.0.0.1:5000/eureka registry-fetch-interval-seconds: 5 instance: prefer-ip-address: true ip-address: 127.0.0.1 instance-id: ${spring.application.name}:${server.port} zuul: prefix: /api # 添加路由前綴 retryable: true routes: auth-service: /auth/** #請求轉發 ribbon: ConnectTimeout: 250 # 鏈接超時時間(ms) ReadTimeout: 2000 # 通訊超時時間(ms) OkToRetryOnAllOperations: true # 是否對全部操做重試 MaxAutoRetriesNextServer: 1 # 同一服務不一樣實例的重試次數 MaxAutoRetries: 1 # 同一實例的重試次數 hystrix: command: default: execution: isolation: thread: timeoutInMillisecond: 10000 #熔斷超時時長:10000ms
先給你們看一下總體結構圖,方便你們構建
這裏咱們仍是建立modle的方式,建立一個聚合工程,打包方式爲pom
<packaging>pom</packaging> <!--子模塊,大家沒有這部分數據,先刪掉--> <modules> <module>ninja-auth-common</module> <module>ninja-auth-service</module> </modules>
在剛剛這個center的基礎上再次建立一個model:通用工具類 : common
<dependencies> <!--JWT的依賴--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</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>
/** * @Description Token的載荷,用於封裝相關用戶數據到Token中的對象 * @Author MSI * @Date 2019/11/6 21:35 **/ @Data @ToString @AllArgsConstructor @NoArgsConstructor public class UserInfo { private Long id; private String username; }
/** * @Description 常量類,用於封裝JWT數據結構中 經常使用的Key * @Author MSI * @Date 2019/11/6 21:51 **/ public abstract class JwtConstans { public static final String JWT_KEY_ID = "id"; public static final String JWT_KEY_USER_NAME = "username"; }
/** * @Description * @Author MSI * @Date 2019/11/6 22:56 **/ public class ObjectUtils { public static String toString(Object obj) { if (obj == null) { return null; } return obj.toString(); } public static Long toLong(Object obj) { if (obj == null) { return 0L; } if (obj instanceof Double || obj instanceof Float) { return Long.valueOf(StringUtils.substringBefore(obj.toString(), ".")); } if (obj instanceof Number) { return Long.valueOf(obj.toString()); } if (obj instanceof String) { return Long.valueOf(obj.toString()); } else { return 0L; } } public static Integer toInt(Object obj) { return toLong(obj).intValue(); } }
/** * @Description * @Author MSI * @Date 2019/11/6 21:32 **/ public class JwtUtils { /** * @Description: 私鑰加密Token * @Param: userInfo 載荷 裏面暫時存有用戶id和username * @Param privateKey 加密須要的私鑰對象 * @Param outTime 過時時間 * @return: java.lang.String Token字符串 * @Author: Ninja * @Date: 2019/11/6 */ public static String generateToken(UserInfo userInfo, PrivateKey privateKey, int outTime){ String token = Jwts.builder() .claim(JwtConstans.JWT_KEY_ID, userInfo.getId()) //把id封裝到token中 .claim(JwtConstans.JWT_KEY_USER_NAME, userInfo.getUsername()) //把username封裝到token中 .setExpiration(DateTime.now().plusMillis(outTime).toDate()) //設置失效時間 .signWith(SignatureAlgorithm.RS256, privateKey) //指定加密算法和私鑰 .compact(); return token; } /** * @Description: 私鑰加密Token * @Param: userInfo 載荷 * @Param privateKey 私鑰 加密須要的私鑰字節數組 * @Param expireMinutes 過時時間 * @return: java.lang.String * @Author: Ninja * @Date: 2019/11/6 */ public static String generateToken(UserInfo userInfo, byte[] privateKey, int expireMinutes) throws Exception { return Jwts.builder() .claim(JwtConstans.JWT_KEY_ID, userInfo.getId()) .claim(JwtConstans.JWT_KEY_USER_NAME, userInfo.getUsername()) .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate()) .signWith(SignatureAlgorithm.RS256, RsaUtils.getPrivateKey(privateKey)) .compact(); } /** * @Description: 公鑰解密Token * @Param: token Token字符串 * @Param bytes 公鑰字節數組 * @return: java.lang.String * @Author: Ninja * @Date: 2019/11/6 */ public static Jws<Claims> parserToken(String token, byte[] bytes) throws Exception { return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(bytes)) .parseClaimsJws(token); } /** * @Description: 公鑰解密Token * @Param: token Token字符串 * @Param publicKey 公鑰對象 * @return: io.jsonwebtoken.Jws<io.jsonwebtoken.Claims> * @Author: Ninja * @Date: 2019/11/6 */ public static Jws<Claims> parserToken(String token, PublicKey publicKey){ return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token); } public static UserInfo getUserInfoByToken(String token,PublicKey publicKey){ Jws<Claims> claimsJws = parserToken(token, publicKey); Claims body = claimsJws.getBody(); //獲得封裝在Token中的 id 和username Long userId = ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)); String username = ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME)); return new UserInfo(userId, username); } }
/** * @Description RSA加密算法工具類 * @Author MSI * @Date 2019/11/6 22:02 **/ public class RsaUtils { /** * @Description: 從文件中獲取:公鑰對象 * @Param: filePath 文件路徑 * @return: java.security.PublicKey * @Author: Ninja * @Date: 2019/11/6 */ public static PublicKey getPublicKey(String filePath) throws Exception { byte[] bytes = readFile(filePath); PublicKey publicKey = getPublicKey(bytes); return publicKey; } /** * @Description: 從文件中獲取:私鑰對象 * @Param: filePath 文件路徑 * @return: java.security.PrivateKey * @Author: Ninja * @Date: 2019/11/6 */ public static PrivateKey getPrivateKey(String filePath) throws Exception { byte[] bytes = readFile(filePath); return getPrivateKey(bytes); } /** * @Description: 生成公鑰和私鑰,並寫入指定文件 * @Param: publicKey 公鑰文件路徑 * @Param privateKey 私鑰文件路徑 * @Param secret 鹽 * @return: void * @Author: Ninja * @Date: 2019/11/6 */ public static void generatorKey(String publicKeyPath,String privateKeyPath,String secret) throws NoSuchAlgorithmException, IOException { KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); SecureRandom secureRandom = new SecureRandom(secret.getBytes()); generator.initialize(1024, secureRandom); KeyPair keyPair = generator.generateKeyPair(); //獲取公鑰並寫出 byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); writeFile(publicKeyPath,publicKeyBytes); //獲取私鑰並寫出 byte[] privateKeyBytes = keyPair.getPrivate().getEncoded(); writeFile(privateKeyPath, privateKeyBytes); } //方法抽取 : 文件寫入到指定路徑下 public static void writeFile(String destPath, byte[] bytes) throws IOException { File dest = new File(destPath); if (!dest.exists()) { dest.createNewFile(); } Files.write(dest.toPath(), bytes); } //方法抽取 : 讀取文件夥子字節數組 public static byte[] readFile(String fileName) throws Exception { return Files.readAllBytes(new File(fileName).toPath()); } //方法抽取 : 從字節數組中解析獲得公鑰對象 public static PublicKey getPublicKey(byte[] bytes) throws Exception { X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes); KeyFactory factory = KeyFactory.getInstance("RSA"); return factory.generatePublic(spec); } //方法抽取 : 從字節數組中解析獲得私鑰對象 public static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException { PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes); KeyFactory factory = KeyFactory.getInstance("RSA"); return factory.generatePrivate(spec); } }
在剛剛這個center的基礎上再次建立一個model:對外服務模塊 : service