樂優(yōu)商城學(xué)習(xí)筆記二十四-授權(quán)中心(一)


title: 樂優(yōu)商城學(xué)習(xí)筆記二十四-授權(quán)中心(一)
date: 2019-04-24 19:32:48
tags:
- 樂優(yōu)商城
- java
- springboot
categories:
- 樂優(yōu)商城


0.學(xué)習(xí)目標(biāo)

1.無狀態(tài)登錄原理

1.1.什么是有狀態(tài)崇呵?

有狀態(tài)服務(wù)蚓挤,即服務(wù)端需要記錄每次會話的客戶端信息,從而識別客戶端身份,根據(jù)用戶身份進(jìn)行請求的處理联四,典型的設(shè)計如tomcat中的session。

例如登錄:用戶登錄后,我們把登錄者的信息保存在服務(wù)端session中,并且給用戶一個cookie值正林,記錄對應(yīng)的session。然后下次請求颤殴,用戶攜帶cookie值來觅廓,我們就能識別到對應(yīng)session,從而找到用戶的信息涵但。

缺點是什么杈绸?

  • 服務(wù)端保存大量數(shù)據(jù)帖蔓,增加服務(wù)端壓力
  • 服務(wù)端保存用戶狀態(tài),無法進(jìn)行水平擴(kuò)展
  • 客戶端請求依賴服務(wù)端蝇棉,多次請求必須訪問同一臺服務(wù)器

1.2.什么是無狀態(tài)

微服務(wù)集群中的每個服務(wù)讨阻,對外提供的都是Rest風(fēng)格的接口。而Rest風(fēng)格的一個最重要的規(guī)范就是:服務(wù)的無狀態(tài)性篡殷,即:

  • 服務(wù)端不保存任何客戶端請求者信息
  • 客戶端的每次請求必須具備自描述信息,通過這些信息識別客戶端身份

帶來的好處是什么呢埋涧?

  • 客戶端請求不依賴服務(wù)端的信息板辽,任何多次請求不需要必須訪問到同一臺服務(wù)
  • 服務(wù)端的集群和狀態(tài)對客戶端透明
  • 服務(wù)端可以任意的遷移和伸縮
  • 減小服務(wù)端存儲壓力

1.3.如何實現(xiàn)無狀態(tài)

無狀態(tài)登錄的流程:

  • 當(dāng)客戶端第一次請求服務(wù)時,服務(wù)端對用戶進(jìn)行信息認(rèn)證(登錄)
  • 認(rèn)證通過棘催,將用戶信息進(jìn)行加密形成token劲弦,返回給客戶端,作為登錄憑證
  • 以后每次請求醇坝,客戶端都攜帶認(rèn)證的token
  • 服務(wù)的對token進(jìn)行解密邑跪,判斷是否有效。

流程圖:

image

整個登錄過程中呼猪,最關(guān)鍵的點是什么画畅?

token的安全性

token是識別客戶端身份的唯一標(biāo)示,如果加密不夠嚴(yán)密宋距,被人偽造那就完蛋了轴踱。

采用何種方式加密才是安全可靠的呢?

我們將采用JWT + RSA非對稱加密

1.4.JWT

1.4.1.簡介

JWT谚赎,全稱是Json Web Token淫僻, 是JSON風(fēng)格輕量級的授權(quán)和身份認(rèn)證規(guī)范,可實現(xiàn)無狀態(tài)壶唤、分布式的Web應(yīng)用授權(quán)雳灵;官網(wǎng):https://jwt.io

image

GitHub上jwt的java客戶端:https://github.com/jwtk/jjwt

1.4.2.數(shù)據(jù)格式

JWT包含三部分?jǐn)?shù)據(jù):

  • Header:頭部,通常頭部有兩部分信息:

    • 聲明類型闸盔,這里是JWT
    • 加密算法悯辙,自定義

    我們會對頭部進(jìn)行base64加密(可解密),得到第一部分?jǐn)?shù)據(jù)

  • Payload:載荷蕾殴,就是有效數(shù)據(jù)笑撞,一般包含下面信息:

    • 用戶身份信息(注意,這里因為采用base64加密钓觉,可解密茴肥,因此不要存放敏感信息)
    • 注冊聲明:如token的簽發(fā)時間,過期時間荡灾,簽發(fā)人等

    這部分也會采用base64加密瓤狐,得到第二部分?jǐn)?shù)據(jù)

  • Signature:簽名瞬铸,是整個數(shù)據(jù)的認(rèn)證信息。一般根據(jù)前兩步的數(shù)據(jù)础锐,再加上服務(wù)的的密鑰(secret)(不要泄漏嗓节,最好周期性更換),通過加密算法生成皆警。用于驗證整個數(shù)據(jù)完整和可靠性

生成的數(shù)據(jù)格式:

image

可以看到分為3段拦宣,每段就是上面的一部分?jǐn)?shù)據(jù)

1.4.3.JWT交互流程

流程圖:

image

步驟翻譯:

  • 1、用戶登錄
  • 2信姓、服務(wù)的認(rèn)證鸵隧,通過后根據(jù)secret生成token
  • 3、將生成的token返回給用戶
  • 4意推、用戶每次請求攜帶token
  • 5豆瘫、服務(wù)端利用公鑰解讀jwt簽名,判斷簽名有效后菊值,從Payload中獲取用戶信息
  • 6外驱、處理請求,返回響應(yīng)結(jié)果

因為JWT簽發(fā)的token中已經(jīng)包含了用戶的身份信息趟薄,并且每次請求都會攜帶,這樣服務(wù)的就無需保存用戶信息羡铲,甚至無需去數(shù)據(jù)庫查詢,完全符合了Rest的無狀態(tài)規(guī)范雷恃。

1.4.4.非對稱加密

加密技術(shù)是對信息進(jìn)行編碼和解碼的技術(shù),編碼是把原來可讀信息(又稱明文)譯成代碼形式(又稱密文)讨越,其逆過程就是解碼(解密)人弓,加密技術(shù)的要點是加密算法崔赌,加密算法可以分為三類:

  • 對稱加密太雨,如AES
    • 基本原理:將明文分成N個組,然后使用密鑰對各個組進(jìn)行加密锥咸,形成各自的密文,最后把所有的分組密文進(jìn)行合并雪侥,形成最終的密文。
    • 優(yōu)勢:算法公開、計算量小原茅、加密速度快、加密效率高
    • 缺陷:雙方都使用同樣密鑰贝室,安全性得不到保證
  • 非對稱加密捡偏,如RSA
    • 基本原理:同時生成兩把密鑰:私鑰和公鑰银伟,私鑰隱秘保存,公鑰可以下發(fā)給信任客戶端
      • 私鑰加密琉预,持有私鑰或公鑰才可以解密
      • 公鑰加密,持有私鑰才可解密
    • 優(yōu)點:安全,難以破解
    • 缺點:算法比較耗時
  • 不可逆加密近速,如MD5,SHA
    • 基本原理:加密過程中不需要使用密鑰,輸入明文后由系統(tǒng)直接經(jīng)過加密算法處理成密文,這種加密后的數(shù)據(jù)是無法被解密的蛮瞄,無法根據(jù)密文推算出明文。

RSA算法歷史:

1977年状土,三位數(shù)學(xué)家Rivest、Shamir 和 Adleman 設(shè)計了一種算法累驮,可以實現(xiàn)非對稱加密。這種算法用他們?nèi)齻€人的名字縮寫:RSA

1.5.結(jié)合Zuul的鑒權(quán)流程

我們逐步演進(jìn)系統(tǒng)架構(gòu)設(shè)計置侍。需要注意的是:secret是簽名的關(guān)鍵墅垮,因此一定要保密螟够,我們放到鑒權(quán)中心保存能岩,其它任何服務(wù)中都不能獲取secret。

1.5.1.沒有RSA加密時

在微服務(wù)架構(gòu)中膏燕,我們可以把服務(wù)的鑒權(quán)操作放到網(wǎng)關(guān)中钥屈,將未通過鑒權(quán)的請求直接攔截,如圖:

image
  • 1坝辫、用戶請求登錄
  • 2篷就、Zuul將請求轉(zhuǎn)發(fā)到授權(quán)中心,請求授權(quán)
  • 3近忙、授權(quán)中心校驗完成未辆,頒發(fā)JWT憑證
  • 4、客戶端請求其它功能涉瘾,攜帶JWT
  • 5赁还、Zuul將jwt交給授權(quán)中心校驗斑举,通過后放行
  • 6黎茎、用戶請求到達(dá)微服務(wù)
  • 7溺森、微服務(wù)將jwt交給鑒權(quán)中心铛铁,鑒權(quán)同時解析用戶信息
  • 8题画、鑒權(quán)中心返回用戶數(shù)據(jù)給微服務(wù)
  • 9爆办、微服務(wù)處理請求漂彤,返回響應(yīng)

發(fā)現(xiàn)什么問題了?

每次鑒權(quán)都需要訪問鑒權(quán)中心丛晦,系統(tǒng)間的網(wǎng)絡(luò)請求頻率過高,效率略差,鑒權(quán)中心的壓力較大肢扯。

1.5.2.結(jié)合RSA的鑒權(quán)

直接看圖:

image
  • 我們首先利用RSA生成公鑰和私鑰怜俐。私鑰保存在授權(quán)中心身堡,公鑰保存在Zuul和各個微服務(wù)
  • 用戶請求登錄
  • 授權(quán)中心校驗,通過后用私鑰對JWT進(jìn)行簽名加密
  • 返回jwt給用戶
  • 用戶攜帶JWT訪問
  • Zuul直接通過公鑰解密JWT拍鲤,進(jìn)行驗證贴谎,驗證通過則放行
  • 請求到達(dá)微服務(wù),微服務(wù)直接用公鑰解析JWT季稳,獲取用戶信息擅这,無需訪問授權(quán)中心

服務(wù)暴露的問題?

避免被暴露

jwt服務(wù)間鑒權(quán)

2.授權(quán)中心

2.1.創(chuàng)建授權(quán)中心

授權(quán)中心的主要職責(zé):

  • 用戶鑒權(quán):
    • 接收用戶的登錄請求景鼠,通過用戶中心的接口進(jìn)行校驗蕾哟,通過后生成JWT
    • 使用私鑰生成JWT并返回
  • 服務(wù)鑒權(quán):微服務(wù)間的調(diào)用不經(jīng)過Zuul,會有風(fēng)險,需要鑒權(quán)中心進(jìn)行認(rèn)證
    • 原理與用戶鑒權(quán)類似莲蜘,但邏輯稍微復(fù)雜一些(此處我們不做實現(xiàn))

因為生成jwt,解析jwt這樣的行為以后在其它微服務(wù)中也會用到帘营,因此我們會抽取成工具票渠。我們把鑒權(quán)中心進(jìn)行聚合,一個工具module芬迄,一個提供服務(wù)的module

2.1.1.創(chuàng)建父module

我們先創(chuàng)建父module问顷,名稱為:ly-auth-center

image

將pom打包方式改為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">
    <parent>
        <artifactId>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.service</groupId>
    <artifactId>ly-auth-center</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <modules>
        <module>ly-auth-common</module>
    </modules>
    <packaging>pom</packaging>

</project>

2.1.2.通用module

創(chuàng)建module

然后是授權(quán)服務(wù)的通用模塊:ly-auth-common:

image
image

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">
    <parent>
        <artifactId>ly-auth-center</artifactId>
        <groupId>com.leyou.service</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.service</groupId>
    <artifactId>ly-auth-common</artifactId>
    <version>1.0.0-SNAPSHOT</version>


</project>

結(jié)構(gòu):


image

2.1.3.授權(quán)服務(wù)

創(chuàng)建module

image
image

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">
    <parent>
        <artifactId>ly-auth-center</artifactId>
        <groupId>com.leyou.service</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.service</groupId>
    <artifactId>ly-auth-service</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <dependencies>
        <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-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.leyou.service</groupId>
            <artifactId>ly-auth-common</artifactId>
            <version>${leyou.latest.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>
</project>

創(chuàng)建啟動類:

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LyAuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(LyAuthApplication.class, args);
    }
}

配置application.yml

server:
  port: 8087
spring:
  application:
    name: auth-service
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
    registry-fetch-interval-seconds: 10
  instance:
    lease-renewal-interval-in-seconds: 5 # 每隔5秒發(fā)送一次心跳
    lease-expiration-duration-in-seconds: 10 # 10秒不發(fā)送就過期
    prefer-ip-address: true
    ip-address: 127.0.0.1
    instance-id: ${spring.application.name}:${server.port}

結(jié)構(gòu):

image

修改路由:

zuul:
  prefix: /api # 添加路由前綴
  retryable: true
  routes:
    item-service: /item/**
    search-service: /search/**
    user-service: /user/**
    auth-service: /auth/**

2.2.編寫JWT工具

我們在ly-auth-coomon中編寫一些通用的工具類:

image

2.2.1.RSA工具類:

/**
 * Created by ace on 2018/5/10.
 *
 * @author HuYi.Zhang
 */
public class RsaUtils {
    /**
     * 從文件中讀取公鑰
     *
     * @param filename 公鑰保存路徑,相對于classpath
     * @return 公鑰對象
     * @throws Exception
     */
    public static PublicKey getPublicKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /**
     * 從文件中讀取密鑰
     *
     * @param filename 私鑰保存路徑,相對于classpath
     * @return 私鑰對象
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 獲取公鑰
     *
     * @param bytes 公鑰的字節(jié)形式
     * @return
     * @throws Exception
     */
    public static PublicKey getPublicKey(byte[] bytes) throws Exception {
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 獲取密鑰
     *
     * @param bytes 私鑰的字節(jié)形式
     * @return
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 根據(jù)密文杜窄,生存rsa公鑰和私鑰,并寫入指定文件
     *
     * @param publicKeyFilename  公鑰文件路徑
     * @param privateKeyFilename 私鑰文件路徑
     * @param secret             生成密鑰的密文
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(1024, secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 獲取公鑰并寫出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        writeFile(publicKeyFilename, publicKeyBytes);
        // 獲取私鑰并寫出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    private static byte[] readFile(String fileName) throws Exception {
        return Files.readAllBytes(new File(fileName).toPath());
    }

    private static void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.exists()) {
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }
}

2.2.2.常量類

其中定義了jwt中的payload的常用key

public abstract class JwtConstans {
    public static final String JWT_KEY_ID = "id";
    public static final String JWT_KEY_USER_NAME = "username";
}

2.2.3.對象工具類:

從jwt解析得到的數(shù)據(jù)是Object類型肠骆,轉(zhuǎn)換為具體類型可能出現(xiàn)空指針,這個工具類進(jìn)行了一些轉(zhuǎn)換:

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();
    }
}

2.2.4.載荷:UserInfo

public class UserInfo {

    private Long id;

    private String username;

    public UserInfo() {
    }

    public UserInfo(Long id, String username) {
        this.id = id;
        this.username = username;
    }

    public Long getId() {
        return this.id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

2.2.5.JWT工具類

我們需要先在ly-auth-common中引入JWT依賴:

<dependencies>
    <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.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
</dependencies>

代碼:

/**
 * @author: HuYi.Zhang
 * @create: 2018-05-26 15:43
 **/
public class JwtUtils {
    /**
     * 私鑰加密token
     *
     * @param userInfo      載荷中的數(shù)據(jù)
     * @param privateKey    私鑰
     * @param expireMinutes 過期時間塞耕,單位秒
     * @return
     * @throws Exception
     */
    public static String generateToken(UserInfo userInfo, PrivateKey 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, privateKey)
                .compact();
    }

    /**
     * 私鑰加密token
     *
     * @param userInfo      載荷中的數(shù)據(jù)
     * @param privateKey    私鑰字節(jié)數(shù)組
     * @param expireMinutes 過期時間蚀腿,單位秒
     * @return
     * @throws Exception
     */
    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();
    }

    /**
     * 公鑰解析token
     *
     * @param token     用戶請求中的token
     * @param publicKey 公鑰
     * @return
     * @throws Exception
     */
    private static Jws<Claims> parserToken(String token, PublicKey publicKey) {
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }

    /**
     * 公鑰解析token
     *
     * @param token     用戶請求中的token
     * @param publicKey 公鑰字節(jié)數(shù)組
     * @return
     * @throws Exception
     */
    private static Jws<Claims> parserToken(String token, byte[] publicKey) throws Exception {
        return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey))
                .parseClaimsJws(token);
    }

    /**
     * 獲取token中的用戶信息
     *
     * @param token     用戶請求中的令牌
     * @param publicKey 公鑰
     * @return 用戶信息
     * @throws Exception
     */
    public static UserInfo getInfoFromToken(String token, PublicKey publicKey) throws Exception {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        return new UserInfo(
                ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)),
                ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME))
        );
    }

    /**
     * 獲取token中的用戶信息
     *
     * @param token     用戶請求中的令牌
     * @param publicKey 公鑰
     * @return 用戶信息
     * @throws Exception
     */
    public static UserInfo getInfoFromToken(String token, byte[] publicKey) throws Exception {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        return new UserInfo(
                ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)),
                ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME))
        );
    }
}

2.2.6.測試

我們在ly-auth-common中編寫測試類:

image
public class JwtTest {

    private static final String pubKeyPath = "D:\\heima\\rsa\\rsa.pub";

    private static final String priKeyPath = "D:\\heima\\rsa\\rsa.pri";

    private PublicKey publicKey;

    private PrivateKey privateKey;

    @Test
    public void testRsa() throws Exception {
        RsaUtils.generateKey(pubKeyPath, priKeyPath, "234");
    }

    //@Before
    public void testGetRsa() throws Exception {
        this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
        this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
    }

    @Test
    public void testGenerateToken() throws Exception {
        // 生成token
        String token = JwtUtils.generateToken(new UserInfo(20L, "jack"), privateKey, 5);
        System.out.println("token = " + token);
    }

    @Test
    public void testParseToken() throws Exception {
        String token = "eyJhbGciOiJSUzI1NiJ9.eyJpZCI6MjAsInVzZXJOYW1lIjoiamFjayIsImV4cCI6MTUyNzMzMDY5NX0.VpGNedy1z0aR262uAp2sM6xB4ljuxa4fzqyyBpZcGTBNLodIfuCNZkOjdlqf-km6TQPoz3epYf8cc_Rf9snsGdz4YPIwpm6X14JKU9jwL74q6zy61J8Nl9q7Zu3YnLibAvcnC_y9omiqKN8-iCi7-MvM-LwVS7y_Cx9eu0aaY8E";

        // 解析token
        UserInfo user = JwtUtils.getInfoFromToken(token, publicKey);
        System.out.println("id: " + user.getId());
        System.out.println("userName: " + user.getUsername());
    }
}

測試生成公鑰和私鑰

我們運(yùn)行這段代碼:

image

運(yùn)行之后,查看目標(biāo)目錄:


image

公鑰和私鑰已經(jīng)生成了扫外!

測試生成token,把@Before之前的注釋去掉

image
image

測試解析token

image

正常情況:

image

任意改動token莉钙,發(fā)現(xiàn)報錯了:

image

2.3.編寫登錄授權(quán)接口

接下來,我們需要在ly-auth-servcice編寫一個接口筛谚,對外提供登錄授權(quán)服務(wù)磁玉。基本流程如下:

  • 客戶端攜帶用戶名和密碼請求登錄
  • 授權(quán)中心調(diào)用客戶中心接口驾讲,根據(jù)用戶名和密碼查詢用戶信息
  • 如果用戶名密碼正確蚊伞,能獲取用戶,否則為空吮铭,則登錄失敗
  • 如果校驗成功时迫,則生成JWT并返回

2.3.1.生成公鑰和私鑰

我們需要在授權(quán)中心生成真正的公鑰和私鑰。我們必須有一個生成公鑰和私鑰的secret沐兵,這個可以配置到application.yml中:

ly:
  jwt:
    secret: ly@Login(Auth}*^31)&heiMa% # 登錄校驗的密鑰
    pubKeyPath: E:/demo/rsa/rsa.pub # 公鑰地址
    priKeyPath: E:/demorsa/rsa.pri # 私鑰地址
    expire: 30 # 過期時間,單位分鐘

然后編寫屬性類别垮,加載這些數(shù)據(jù):

@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {

    private String secret; // 密鑰

    private String pubKeyPath;// 公鑰

    private String priKeyPath;// 私鑰

    private int expire;// token過期時間

    private PublicKey publicKey; // 公鑰

    private PrivateKey privateKey; // 私鑰

    private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);

    @PostConstruct
    public void init(){
        try {
            File pubKey = new File(pubKeyPath);
            File priKey = new File(priKeyPath);
            if (!pubKey.exists() || !priKey.exists()) {
                // 生成公鑰和私鑰
                RsaUtils.generateKey(pubKeyPath, priKeyPath, secret);
            }
            // 獲取公鑰和私鑰
            this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
            this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
        } catch (Exception e) {
            logger.error("初始化公鑰和私鑰失敗扎谎!", e);
            throw new RuntimeException();
        }
    }
    
    // getter setter ...
}

2.3.2.controller

編寫授權(quán)接口碳想,我們接收用戶名和密碼,校驗成功后毁靶,寫入cookie中胧奔。

  • 請求方式:post
  • 請求路徑:/accredit
  • 請求參數(shù):username和password
  • 返回結(jié)果:無

代碼:

@RestController
@EnableConfigurationProperties(JwtProperties.class)
public class AuthController {

    @Autowired
    private AuthService authService;

    @Autowired
    private JwtProperties prop;

    /**
     * 登錄授權(quán)
     * @param username
     * @param password
     * @return
     */
    @PostMapping("login")
    public ResponseEntity<String> login(
            @RequestParam("username") String username,
            @RequestParam("password") String password,
            HttpServletRequest request,
             HttpServletResponse response){
        //登錄
        String token = authService.login(username,password);
        //寫入cookie
        CookieUtils.setCookie(request, response, prop.getCookieName(),
                token, prop.getCookieMaxAge(), true);
        return ResponseEntity.ok(token);
    }

}

這里的cookie的name和生存時間,我們配置到屬性文件:application.yml:

ly:
  jwt:
    pubKeyPath: E:/nginx/rsa/rsa.pub # 公鑰地址
    priKeyPath: E:/nginx/rsa/rsa.pri # 私鑰地址
    expire: 30 # 過期時間,單位分鐘
    cookieName: LY_TOKEN
    CookieMaxAge: 1800

然后在JwtProperties中添加屬性:

image

2.3.3.CookieUtils

要注意预吆,這里我們使用了一個工具類龙填,CookieUtils,我們把它添加到ly-common中拐叉,然后引入servlet相關(guān)依賴即可:

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
</dependency>

代碼:略


image

2.3.3.UserClient

接下來我們肯定要對用戶密碼進(jìn)行校驗岩遗,所以我們需要通過FeignClient去訪問 user-service微服務(wù):

引入user-service依賴:

<dependency>
    <groupId>com.leyou.service</groupId>
    <artifactId>ly-user-interface</artifactId>
    <version>${leyou.latest.version}</version>
</dependency>

編寫FeignClient:

@FeignClient(value = "user-service")
public interface UserClient extends UserApi {
}

2.3.4.service

@Service
@EnableConfigurationProperties(JwtLoginProperties.class)
public class AuthService {

    @Autowired
    private JwtLoginProperties jwtProp;

    @Autowired
    private UserClient userClient;

    private static Logger logger = LoggerFactory.getLogger(AuthService.class);

    public String authentication(String username, String password) {
        try {
            // 查詢用戶
            ResponseEntity<User> resp = this.userClient.queryUser(username, password);
            if (!resp.hasBody()) {
                logger.info("用戶信息不存在,{}", username);
                return null;
            }
            // 獲取登錄用戶
            User user = resp.getBody();
            // 生成token
            String token = JwtUtils.generateToken(
                    new UserInfo(user.getId(), user.getUsername()),
                    jwtProp.getPrivateKey(), jwtProp.getExpire());
            return token;
        } catch (Exception e) {
            return null;
        }
    }
}

2.3.5.項目結(jié)構(gòu):

image

2.3.6.測試

image

2.3.7.添加路由規(guī)則

我們在ly-api-gateway中添加路徑規(guī)則:

zuul:
  routes:
    auth-service: /auth/**

2.4.登錄頁面

接下來凤瘦,我們看看登錄頁面宿礁,是否能夠正確的發(fā)出請求。

我們在頁面輸入登錄信息蔬芥,然后點擊登錄:

image

查看控制臺:

image

發(fā)現(xiàn)請求的路徑不對梆靖,我們的認(rèn)證接口是:

/api/auth/login

我們打開login.html控汉,修改路徑信息:

image

頁面ajax請求:

image

然后再次測試,成功跳轉(zhuǎn)到了首頁

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末返吻,一起剝皮案震驚了整個濱河市姑子,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌测僵,老刑警劉巖街佑,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異恨课,居然都是意外死亡舆乔,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進(jìn)店門剂公,熙熙樓的掌柜王于貴愁眉苦臉地迎上來希俩,“玉大人,你說我怎么就攤上這事纲辽⊙瘴洌” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵拖吼,是天一觀的道長鳞上。 經(jīng)常有香客問我,道長吊档,這世上最難降的妖魔是什么篙议? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮怠硼,結(jié)果婚禮上鬼贱,老公的妹妹穿的比我還像新娘。我一直安慰自己香璃,他們只是感情好这难,可當(dāng)我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著葡秒,像睡著了一般姻乓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上眯牧,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天蹋岩,我揣著相機(jī)與錄音,去河邊找鬼学少。 笑死剪个,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的旱易。 我是一名探鬼主播禁偎,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼阀坏!你這毒婦竟也來了如暖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤忌堂,失蹤者是張志新(化名)和其女友劉穎盒至,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體士修,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡枷遂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了棋嘲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酒唉。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖沸移,靈堂內(nèi)的尸體忽然破棺而出痪伦,到底是詐尸還是另有隱情,我是刑警寧澤雹锣,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布网沾,位于F島的核電站,受9級特大地震影響蕊爵,放射性物質(zhì)發(fā)生泄漏辉哥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一攒射、第九天 我趴在偏房一處隱蔽的房頂上張望醋旦。 院中可真熱鬧,春花似錦匆篓、人聲如沸浑度。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽箩张。三九已至,卻和暖如春窗市,著一層夾襖步出監(jiān)牢的瞬間先慷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工咨察, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留论熙,地道東北人。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓摄狱,卻偏偏與公主長得像脓诡,于是被迫代替她去往敵國和親无午。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,864評論 2 354

推薦閱讀更多精彩內(nèi)容