(微服務架構)Security + Oauth2 + Jwt + Zuul解決微服務系統的安全問題

前言

以前零零散散的學習過一點鑑權這方面的玩意兒,但自我感受淨他媽整些沒用的,看代碼仍是看不懂,此次咱們再統一對其進行學習一下,但願本身掌握這個技能,也但願屏幕面前的你能有點收穫html

這次的學習週期可能有點長,由於公司加班不穩定,我也不知道啥時候能坐到筆記本前開始筆伐技術,因此在還沒更完的時候,你們就先將就一下,哈哈哈哈...java

往期回頭複習一輪

以前學習過一點Security + JWT的知識,這裏我推薦你們看一下,對於這次的學習的前期基礎鋪墊也是一個很重要的環節,固然在我噼裏啪啦打完這段文字事後,我也會對其進行一個複習,爭取此次一舉拿下這個讓人頭痛的東西:mysql

Spring Security + JWT + Oauth思想學習git

微服務下的應用場景解析

  • 固然啦,如今不是微服務基本也是用Security + JWT的方式進行一個權限管理,在上面的複習連接中,拉過代碼的朋友應該都會有那麼一點了解,我這麼就不在作過多的贅述,能夠直接拉代碼看看,我註釋寫得仍是比較詳細github

  • 咱們今天聊的是微服務下的鑑權,微服務你們都知道功能模塊劃分爲單獨的模塊,單獨的部署起來,不可能爲每個劃分的模塊都作權限校驗的啦,站在複用的角度來想的話,咱們確定是要將鑑權服務單獨抽離出來,作成功能模塊的,Security 作權限校驗,JWT解決分佈式Session分離問題,固然也能夠用Redis解決Session分離的問題,但咱們這裏研究的是JWT,因此就沒Session什麼事情了,而後使用的是Oauth2的思想,完成整個微服務功能模塊的保駕護航web

 

SSO單點登陸理解

經典案列:Cookie客戶端技術 + Session服務端技術 = 實現SSOredis

在用戶登陸驗證經過以後,咱們將用戶相關信息存在Session,中,並把key塞到Cookie中,下次用戶來訪問咱們的服務,帶着該Cookie來,咱們獲取Cookie中的數據,去Session中查找,若是查找到有值,則免登陸,容許訪問服務算法

  • 思考:若是是分佈式項目,某個模塊爲集羣,Session何去何從? Redis解決!spring

  • 這樣一想好像還蠻合適的勒,redis設置數據過時時間不是美滋滋嗎?sql

  • 說到這裏,我好像也舉不出其餘的反面問題了誒,略現尷尬,哈哈哈哈哈...

  • Cookie中數據的安全問題,想一想也能用加密算法對其進行自定義加密;

  • Session佔用服務器資源問題,好像問題也不是很大,哈哈哈哈...

言歸正傳,咱們仍是繼續學習,由於生活所迫,所謂鍵盤不敲爛,月薪不過萬,學多點,加強自身複用性,哈哈哈哈...

這次的學習的流程圖,我先畫一下,給你們一個整體的思路歸納:

  1. 用戶輸入賬號和密碼,請求登陸,被Zuul攔截,作校驗

  2. Zuul調用受權中心,爲該用戶作驗證

  3. 驗證經過後頒發使用私鑰加密的JWT令牌

  4. 標記錯誤,不作理會

  5. 網關將該令牌塞給客戶端Cookie中

  6. 客戶端使用Cookie攜帶該Token去訪問響應服務,通過網關,網關發現令牌合法,放行

  7. 微服務使用公鑰對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>

Eureka搭建

<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 #關閉自我保護

Zuul網關搭建

 <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

相關文章
相關標籤/搜索