html
java
例如登陸:用戶登陸後,咱們把登陸者的信息保存在服務端session中,而且給用戶一個cookie值,記錄對應的session。而後下次請求,用戶攜帶cookie值來,咱們就能識別到對應session,從而找到用戶的信息。nginx
缺點是什麼?web
服務端保存大量數據,增長服務端壓力ajax
服務端保存用戶狀態,沒法進行水平擴展算法
spring
數據庫
apache
服務端不保存任何客戶端請求者信息json
客戶端的每次請求必須具有自描述信息,經過這些信息識別客戶端身份
帶來的好處是什麼呢?
客戶端請求不依賴服務端的信息,任何屢次請求不須要必須訪問到同一臺服務
服務端的集羣和狀態對客戶端透明
服務端能夠任意的遷移和伸縮
減少服務端存儲壓力
(3)
當客戶端第一次請求服務時,服務端對用戶進行信息認證(登陸)
認證經過,將用戶信息進行加密造成token,返回給客戶端,做爲登陸憑證
之後每次請求,客戶端都攜帶認證的token
token的安全性
token是識別客戶端身份的惟一標示,若是加密不夠嚴密,被人僞造那就完蛋了。
採用何種方式加密纔是安全可靠的呢?
咱們將採用
<2>
Header:頭部,一般頭部有兩部分信息:
聲明類型,這裏是JWT
咱們會對頭部進行base64編碼,獲得第一部分數據
Payload:載荷,就是有效數據,通常包含下面信息:
用戶身份信息(注意,這裏由於採用base64編碼,可解碼,所以不要存放敏感信息)
註冊聲明:如token的簽發時間,過時時間,簽發人等
這部分也會採用base64編碼,獲得第二部分數據
Signature:簽名,是整個數據的認證信息。通常根據前兩步的數據,再加上服務的的密鑰(secret)(不要泄漏,最好週期性更換),經過加密算法生成。用於驗證整個數據完整和可靠性
生成的數據格式:token==我的證件 jwt=我的身份證
能夠看到分爲3段,每段就是上面的一部分數據
<3>
一、用戶登陸
二、服務的認證,經過後根據secret生成token
三、將生成的token返回給瀏覽器
四、用戶每次請求攜帶token
五、服務端利用公鑰解讀jwt簽名,判斷簽名有效後,從Payload中獲取用戶信息
六、處理請求,返回響應結果
由於JWT簽發的token中已經包含了用戶的身份信息,而且每次請求都會攜帶,這樣服務的就無需保存用戶信息,甚至無需去數據庫查詢,徹底符合了Rest的無狀態規範。
<4>
對稱加密,如AES
基本原理:將明文分紅N個組,而後使用密鑰對各個組進行加密,造成各自的密文,最後把全部的分組密文進行合併,造成最終的密文。
優點:算法公開、計算量小、加密速度快、加密效率高
缺陷:雙方都使用一樣密鑰,安全性得不到保證
非對稱加密,如RSA
基本原理:同時生成兩把密鑰:私鑰和公鑰,私鑰隱祕保存,公鑰能夠下發給信任客戶端
私鑰加密,持有私鑰或公鑰才能夠解密
公鑰加密,持有私鑰纔可解密
優勢:安全,難以破解
缺點:算法比較耗時
不可逆加密,如MD5,SHA
基本原理:加密過程當中不須要使用密鑰,輸入明文後由系統直接通過加密算法處理成密文,這種加密後的數據是沒法被解密的,沒法根據密文推算出明文。
RSA算法歷史:
用戶請求登陸
受權中心校驗,經過後用私鑰對JWT進行簽名加密
返回jwt給用戶
用戶攜帶JWT訪問
Zuul直接經過公鑰解密JWT,進行驗證,驗證經過則放行
(1)
用戶鑑權:
接收用戶的登陸請求,經過用戶中心的接口進行校驗,經過後生成JWT
使用私鑰生成JWT並返回
服務鑑權:微服務間的調用不通過Zuul,會有風險,須要鑑權中心進行認證
原理與用戶鑑權相似,但邏輯稍微複雜一些(此處咱們不作實現)
咱們先建立父module(maven模塊),名稱爲:leyou-auth
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>lucky.leyou.parent</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <packaging>pom</packaging> <groupId>lucky.leyou.auth</groupId> <artifactId>leyou-auth</artifactId> </project>
注意:將pom打包方式改成pom
<2>
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-auth</artifactId> <groupId>lucky.leyou.auth</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>lucky.leyou.auth</groupId> <artifactId>leyou-auth-common</artifactId> </project>
結構:
<3>
pom.xml:
<?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-auth</artifactId> <groupId>lucky.leyou.auth</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>lucky.leyou.auth</groupId> <artifactId>leyou-auth-service</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</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-web</artifactId> </dependency> <dependency> <groupId>lucky.leyou.auth</groupId> <artifactId>leyou-auth-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>lucky.leyou.common</groupId> <artifactId>leyou-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies> </project>
引導類:
package lucky.leyou; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class LeyouAuthApplication { public static void main(String[] args) { SpringApplication.run(LeyouAuthApplication.class, args); } }
application.yml:
server: port: 8090 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秒發送一次心跳 lease-expiration-duration-in-seconds: 10 # 10秒不發送就過時
結構:
在leyou-gateway工程的application.yml中,修改路由:
zuul: prefix: /api # 路由路徑前綴 routes: item-service: /item/** # 商品微服務的映射路徑 search-service: /search/** # 搜索微服務 user-service: /user/** # 用戶微服務 auth-service: /auth/** # 受權中心微服務
(2)
<?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-auth</artifactId> <groupId>lucky.leyou.auth</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>lucky.leyou.auth</groupId> <artifactId>leyou-auth-common</artifactId> <dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> </dependency> </dependencies> </project>
(3)
package lucky.leyou.auth.test; import lucky.leyou.auth.entity.UserInfo; import lucky.leyou.auth.utils.JwtUtils; import lucky.leyou.auth.utils.RsaUtils; import org.junit.Before; import org.junit.Test; import java.security.PrivateKey; import java.security.PublicKey; public class JwtTest { private static final String pubKeyPath = "C:\\tmp\\rsa\\rsa.pub"; private static final String priKeyPath = "C:\\tmp\\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.eyJpZCI6MjAsInVzZXJuYW1lIjoiamFjayIsImV4cCI6MTUzMzI4MjQ3N30.EPo35Vyg1IwZAtXvAx2TCWuOPnRwPclRNAM4ody5CHk8RF55wdfKKJxjeGh4H3zgruRed9mEOQzWy79iF1nGAnvbkraGlD6iM-9zDW8M1G9if4MX579Mv1x57lFewzEo-zKnPdFJgGlAPtNWDPv4iKvbKOk1-U7NUtRmMsF1Wcg"; // 解析token UserInfo user = JwtUtils.getInfoFromToken(token, publicKey); System.out.println("id: " + user.getId()); System.out.println("userName: " + user.getUsername()); } }
運行以後,查看目標目錄:
控制檯輸出:
<3>測試解析token:
正常狀況:
任意改動token,發現報錯了:
3.
客戶端攜帶用戶名和密碼請求登陸
受權中心調用用戶中心接口,根據用戶名和密碼查詢用戶信息
若是用戶名密碼正確,能獲取用戶,不然爲空,則登陸失敗
若是校驗成功,則生成JWT並返回
邏輯分析圖:
(1)
leyou: jwt: secret: leyou@Login(Auth}*^31)&heiMa% # 登陸校驗的密鑰 pubKeyPath: D:\temp\rsa\\rsa.pub # 公鑰地址 priKeyPath: D:\temp\rsa\\rsa.pri # 私鑰地址 expire: 30 # 過時時間,單位分鐘
而後編寫屬性類,加載這些數據:
package lucky.leyou.auth.config; import lucky.leyou.auth.utils.RsaUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import javax.annotation.PostConstruct; import java.io.File; import java.security.PrivateKey; import java.security.PublicKey; @ConfigurationProperties(prefix = "leyou.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); /** * @PostContruct:在構造方法執行以後執行該方法 */ @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 ... public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } public String getPubKeyPath() { return pubKeyPath; } public void setPubKeyPath(String pubKeyPath) { this.pubKeyPath = pubKeyPath; } public String getPriKeyPath() { return priKeyPath; } public void setPriKeyPath(String priKeyPath) { this.priKeyPath = priKeyPath; } public int getExpire() { return expire; } public void setExpire(int expire) { this.expire = expire; } public PublicKey getPublicKey() { return publicKey; } public void setPublicKey(PublicKey publicKey) { this.publicKey = publicKey; } public PrivateKey getPrivateKey() { return privateKey; } public void setPrivateKey(PrivateKey privateKey) { this.privateKey = privateKey; } }
(2)controller
請求方式:post
請求路徑:/accredit
請求參數:username和password
返回結果:無
leyou: jwt: secret: leyou@Login(Auth}*^31)&heiMa% # 登陸校驗的密鑰 pubKeyPath: D:\temp\rsa\\rsa.pub # 公鑰地址 priKeyPath: D:\temp\rsa\\rsa.pri # 私鑰地址 expire: 30 # 過時時間,單位分鐘 cookieName: LY_TOKEN #cookie的名稱 cookieMaxAge: 30
代碼:
package lucky.leyou.auth.controller; import lucky.leyou.auth.config.JwtProperties; import lucky.leyou.common.utils.CookieUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Controller @EnableConfigurationProperties(JwtProperties.class) public class AuthController { @Autowired private AuthService authService; @Autowired private JwtProperties prop; /** * 登陸受權 * * @param username * @param password * @return */ @PostMapping("accredit") public ResponseEntity<Void> authentication( @RequestParam("username") String username, @RequestParam("password") String password, HttpServletRequest request, HttpServletResponse response) { // 登陸校驗 String token = this.authService.authentication(username, password); if (StringUtils.isBlank(token)) { return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); } // 將token寫入cookie,並指定httpOnly爲true,防止經過JS獲取和修改 CookieUtils.setCookie(request, response, prop.getCookieName(), token, prop.getCookieMaxAge(), null, true); return ResponseEntity.ok().build(); } }
注意:
這裏咱們使用了一個工具類,CookieUtils
(3)
package lucky.leyou.user.api; import lucky.leyou.user.domain.User; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; public interface UserApi { /** * 根據用戶名和密碼查詢用戶 * @param username * @param password * @return */ @GetMapping("query") public User queryUser( @RequestParam("username") String username, @RequestParam("password") String password ); }
<2>在leyou-auth-service中編寫FeignClient
在leyou-auth中引入user-service-interface依賴:
<dependency> <groupId>lucky.leyou.user</groupId> <artifactId>leyou-user-interface</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
參照leyou-search或者leyou-goods-web
package lucky.leyou.auth.client; import lucky.leyou.user.api.UserApi; import org.springframework.cloud.openfeign.FeignClient; @FeignClient(value = "user-service") public interface UserClient extends UserApi { }
(4)
package lucky.leyou.auth.service; import lucky.leyou.auth.client.UserClient; import lucky.leyou.auth.config.JwtProperties; import lucky.leyou.auth.entity.UserInfo; import lucky.leyou.auth.utils.JwtUtils; import lucky.leyou.user.domain.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class AuthService { @Autowired private UserClient userClient; @Autowired private JwtProperties properties; public String authentication(String username, String password) { try { // 調用微服務,執行查詢 User user = this.userClient.queryUser(username, password); // 若是查詢結果爲null,則直接返回null if (user == null) { return null; } // 若是有查詢結果,則生成token String token = JwtUtils.generateToken(new UserInfo(user.getId(), user.getUsername()), properties.getPrivateKey(), properties.getExpire()); return token; } catch (Exception e) { e.printStackTrace(); } return null; } }
(5)
(6)測試
打開postman進行測試:http://localhost:8090/accredit
4.
查看控制檯:
發現請求的路徑不對,咱們的認證接口是:
/api/auth/accredit
頁面ajax請求:
而後再次測試,成功跳轉到了首頁:
5.
咱們發現內部有一個方法,用來獲取Domain:
它獲取domain是經過服務器的host來計算的,然而咱們的地址居然是:127.0.0.1:8087,所以後續的運算,最終獲得的domain就變成了:
問題找到了:咱們請求時的serverName明明是:api.leyou.com,如今卻被變成了:127.0.0.1,所以計算domain是錯誤的,從而致使cookie設置失敗!
(2)
咱們使用了nginx反向代理,當監聽到api.leyou.com的時候,會自動將請求轉發至127.0.0.1:10010,即Zuul。
然後請求到達咱們的網關Zuul,Zuul就會根據路徑匹配,咱們的請求是/api/auth,根據規則被轉發到了 127.0.0.1:8087 ,即咱們的受權中心。
咱們首先去更改nginx配置,讓它不要修改咱們的host:
把nginx進行reload:
nginx -s reload
這樣就解決了nginx這裏的問題。可是Zuul還會有一次轉發,因此要去修改網關的配置(leyou-gateway工程):
zuul: prefix: /api # 路由路徑前綴 routes: item-service: /item/** # 商品微服務的映射路徑 search-service: /search/** #搜索微服務 user-service: /user/** #用戶微服務 auth-service: /auth/** # 受權中心微服務 add-host-header: true #攜帶請求自己的頭信息
重啓後,咱們再次測試。
最終獲得的domainName:
(3)
Zuul內部有默認的過濾器,會對請求和響應頭信息進行重組,過濾掉敏感的頭信息:
而這個SensitiveHeaders
的默認值就包含了set-cookie
解決方案:
把敏感頭設置爲null
zuul:
prefix: /api # 路由路徑前綴
routes:
item-service: /item/** # 商品微服務的映射路徑
search-service: /search/** #搜索微服務
user-service: /user/** #用戶微服務
auth-service: /auth/** # 受權中心微服務
add-host-header: true #攜帶請求自己的頭信息
sensitive-headers: #覆蓋默認敏感頭信息
(4)測試