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)行解密邑跪,判斷是否有效。
流程圖:
整個登錄過程中呼猪,最關(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
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ù)格式:
可以看到分為3段拦宣,每段就是上面的一部分?jǐn)?shù)據(jù)
1.4.3.JWT交互流程
流程圖:
步驟翻譯:
- 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)點:安全,難以破解
- 缺點:算法比較耗時
- 基本原理:同時生成兩把密鑰:私鑰和公鑰银伟,私鑰隱秘保存,公鑰可以下發(fā)給信任客戶端
- 不可逆加密近速,如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)的請求直接攔截,如圖:
- 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)
直接看圖:
- 我們首先利用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
將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:
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):
2.1.3.授權(quán)服務(wù)
創(chuàng)建module
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):
修改路由:
zuul:
prefix: /api # 添加路由前綴
retryable: true
routes:
item-service: /item/**
search-service: /search/**
user-service: /user/**
auth-service: /auth/**
2.2.編寫JWT工具
我們在ly-auth-coomon
中編寫一些通用的工具類:
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
中編寫測試類:
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)行這段代碼:
運(yùn)行之后,查看目標(biāo)目錄:
公鑰和私鑰已經(jīng)生成了扫外!
測試生成token,把@Before之前的注釋去掉
測試解析token
正常情況:
任意改動token莉钙,發(fā)現(xiàn)報錯了:
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
中添加屬性:
2.3.3.CookieUtils
要注意预吆,這里我們使用了一個工具類龙填,CookieUtils,我們把它添加到ly-common
中拐叉,然后引入servlet相關(guān)依賴即可:
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</dependency>
代碼:略
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):
2.3.6.測試
2.3.7.添加路由規(guī)則
我們在ly-api-gateway
中添加路徑規(guī)則:
zuul:
routes:
auth-service: /auth/**
2.4.登錄頁面
接下來凤瘦,我們看看登錄頁面宿礁,是否能夠正確的發(fā)出請求。
我們在頁面輸入登錄信息蔬芥,然后點擊登錄:
查看控制臺:
發(fā)現(xiàn)請求的路徑不對梆靖,我們的認(rèn)證接口是:
/api/auth/login
我們打開login.html控汉,修改路徑信息:
頁面ajax請求:
然后再次測試,成功跳轉(zhuǎn)到了首頁