目錄
- 第一章 OAuth2.0的預備篇
- 1.1、Base64
- 1.1.1、Base64的介紹
- 1.1.2、Base64的演示
- 1.2、JWT
- 1.2.1、JWT的介紹
- 1.2.2、JWT的組成
- 1.2.3、JWT的特點
- 1.2.4、JWT的演示
- 1.2.5、JWT的隱患
- 1.3、RSA
- 1.3.1、RSA的介紹
- 1.3.2、公鑰私鑰的介紹
- 1.3.3、公鑰私鑰的生成
- 1.4、SSO
- 1.4.1、SSO的介紹
- 1.4.2、SSO的實作
- 第二章 OAuth2.0的介紹篇
- 2.1、OAuth2.0的概述
- 2.2、OAuth2.0的模式
- 2.2.1、授權碼模式
- 2.2.2、簡化模式
- 2.2.3、密碼模式
- 2.2.4、客戶端模式
- 第三章 OAuth2.0的權限控制
- 3.1、專案的準備與介紹
- 3.2、資料庫的資料匯入
- 3.3、資料庫表設計架構
- 3.4、Domain的撰寫
- 3.5、Mapper的撰寫
- 3.6、Service 的撰寫
- 3.7、核心的配置物件
- 3.8、改造控制層方法
- 3.9、啟動專案并測驗
- 第四章 OAuth2.0第三方授權
- 4.1、搭建認證中心
- 4.1.1、注入認證管理器
- 4.1.2、創建并啟用配置
- 4.1.3、重寫父類的方法
- 4.1.4、配置客戶端的詳情
- 4.1.5、配置令牌訪問端點
- 4.1.6、配置令牌端點安全
- 4.1.7、啟動認證的服務器
- 4.1.8、測驗四種模式使用
- 4.1.8.1、測驗授權碼模式
- 4.1.8.2、測驗簡化模式
- 4.1.8.3、測驗密碼模式
- 4.1.8.4、測驗客戶端模式
- 4.1.8.5、測驗重繪Token
- 4.2、搭建訂單資源
- 4.2.1、匯入相關的依賴
- 4.2.2、創建并啟用配置
- 4.2.3、重寫父類的方法
- 4.2.4、配置資源的資訊
- 4.2.5、配置資源的權限
- 4.2.6、拷貝控制器代碼
- 4.2.7、啟動訂單的資源
- 4.2.8、測驗權限的控制
- 第五章 OAuth2.0對接資料庫
- 5.1、升級認證中心
- 5.1.1、匯入官方資料庫表
- 5.1.2、還原配置物件狀態
- 5.1.3、配置客戶端的詳情
- 5.1.4、配置令牌訪問端點
- 5.1.5、配置令牌端點安全
- 5.1.6、重啟認證的服務器
- 5.1.7、測驗四種模式使用
- 5.1.7.1、測驗授權碼模式
- 5.1.7.2、測驗簡化模式
- 5.1.7.3、測驗密碼模式
- 5.1.7.4、測驗客戶端模式
- 5.1.7.5、測驗重繪token
- 5.2、升級訂單資源
- 5.2.1、修改配置的物件
- 5.2.2、重啟訂單的資源
- 5.2.3、測驗權限的控制
- 第六章 OAuth2.0對接公私鑰
- 6.1、創建工具類別庫
- 6.2、生成公鑰私鑰
- 6.3、修改認證服務配置
- 6.4、修改資源服務配置
- 6.5、測驗四種模式使用
- 6.5.1、測驗授權碼模式
- 6.5.2、測驗簡化模式
- 6.5.3、測驗密碼模式
- 6.5.4、測驗客戶端模式
- 6.5.5、測驗重繪token
- 6.6、測驗資源權限控制
- 第七章 OAuth2.0實作單點登錄
- 7.1、注入Rest模板
- 7.2、創建控制器類
- 7.3、開放登錄埠
- 7.4、測驗登錄效果
- 7.5、測驗權限控制
- 第八章 OAuth2.0服務呼叫問題
- 8.1、商品資源服務準備
- 8.2、訂單資源服務準備
- 8.3、訂單呼叫商品問題
- 8.4、解決服務呼叫問題
- 8.5、演示最終解決效果
配套資料,免費下載
鏈接:https://pan.baidu.com/s/1la_3-HW-UvliDRJzfBcP_w
提取碼:lxfx
復制這段內容后打開百度網盤手機App,操作更方便哦
第一章 OAuth2.0的預備篇
1.1、Base64
1.1.1、Base64的介紹
Base64是一種用64個字符來表示任意二進制資料的編碼方式,
1.1.2、Base64的演示
@Test
public void testEncodeAndDecode() {
byte[] bytes = "Hello,World".getBytes();
bytes = Base64.getEncoder().encode(bytes);
System.out.println("編碼后:" + new String(bytes));
bytes = Base64.getDecoder().decode(bytes);
System.out.println("解碼后:" + new String(bytes));
}
編碼后:SGVsbG8sV29ybGQ=
解碼后:Hello,World
1.2、JWT
1.2.1、JWT的介紹
JWT,全稱JSON Web Tokens,官網地址:https://jwt.io,是一款出色的分布式身份校驗方案,可以生成token,也可以決議檢驗token,
1.2.2、JWT的組成
JWT由三部分組成,它們之間用點(.)連接,這三部分分別是:
- HEADER:頭部,頭部主要是用來定義令牌簽名使用的演算法和令牌型別,是一個JSON串,需要使用Base64Url進行編碼,
- PAYLOAD:載荷,載荷主要用來存放傳輸的資料,是一個JSON串,需要使用Base64Url進行編碼,
- SIGNATURE:簽名,根據你HEADER定義的演算法型別,簽名主要用于防止資料被篡改,
注意:Base64Url這個演算法跟Base64演算法基本類似,但有一些小的不同,JWT作為一個令牌(token),有些場合可能會放到URL(比如 api.example.com/?token=xxx),Base64有三個字符+、/和=,在 URL 里面有特殊含義,所以要被替換掉:=被省略、+替換成-,/替換成_ ,這就是Base64Url演算法,
那么最終JWT就是一個字串,這個字串由三部分組成,第一部分頭部,第二部分載荷,第三部分簽名,最終形式:Header.Payload.Signature

HEADER
Header部分是一個JSON串,描述JWT的元資料,通常是下面的樣子,

上面代碼中,alg屬性表示簽名的演算法(algorithm),默認是HMAC SHA256(寫成HS256);typ屬性表示這個令牌(token)的型別(type),JWT令牌統一寫為JWT,
PAYLOAD
Payload部分也是一個JSON串,用來存放實際需要傳遞的資料,JWT 規定了7個官方欄位,可供選用,
- iss (issuer):簽發人
- aud (audience):接收者
- sub (subject):令牌描述
- iat (Issued At):簽發時間
- exp (expiration time):過期時間
- nbf (Not Before):生效時間
- jti (JWT ID):令牌編號
除了官方欄位,你還可以在這個部分定義私有欄位,下面就是一個例子,

SIGNATURE
由于Header頭部和載荷Payload的資料是使用Base64Url進行的編碼,因此,可以這么說,只要你拿到了這兩部分,就能知道,這兩部分的內容,因此,在Payload載荷中是不可以存放用戶密碼的,同時,為了防止別人造假這兩部分的資料,JWT規定第三部分是一個簽名,這個簽名是通過使用Header頭部定義簽名演算法的型別,對前兩部分進行加密,由于前兩部分很容易獲得,因此,我們還需要加入一個別人不知道的密鑰(secret),這個密鑰只有服務器才知道,不能泄露給用戶,然后使用Header里面指定的簽名演算法(默認是HMAC SHA256),按照下面的公式產生簽名,

算出簽名以后,把Header、Payload、Signature三個部分拼成一個字串,每個部分之間用點(.)分隔,就可以回傳給用戶,
1.2.3、JWT的特點
- JWT 默認是不加密,但也是可以加密的,生成原始 Token 以后,可以用密鑰再加密一次,
- JWT 不加密的情況下,不能將秘密資料寫入 JWT,
- JWT 不僅可以用于認證,也可以用于交換資訊,有效使用 JWT,可以降低服務器查詢資料庫的次數,
- JWT 的最大缺點是,由于服務器不保存 session 狀態,因此無法在使用程序中廢止某個 token,或者更改 token 的權限,也就是說,一旦 JWT 簽發了,在到期之前就會始終有效,除非服務器部署額外的邏輯,
- JWT 本身包含了認證資訊,一旦泄露,任何人都可以獲得該令牌的所有權限,為了減少盜用,JWT 的有效期應該設定得比較短,對于一些比較重要的權限,使用時應該再次對用戶進行認證,
- 為了減少盜用,JWT 不應該使用 HTTP 協議明碼傳輸,要使用 HTTPS 協議傳輸,
1.2.4、JWT的演示
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
@Test
public void testJWTDemo() {
JwtBuilder jwtBuilder = Jwts.builder();
/**
* ===================================接下來準備生成token
*/
//設定官方規定的欄位,根據需求設定
jwtBuilder.setIssuer("曹晨磊");//令牌頒發者
jwtBuilder.setIssuedAt(new Date());//令牌頒發時間
jwtBuilder.setAudience("不知名的客戶端");//令牌接收者
jwtBuilder.setExpiration(new Date(System.currentTimeMillis() + 3600000));//令牌過期時間,1小時以后
jwtBuilder.setId(UUID.randomUUID().toString());//設定令牌編號
//設定簽名演算法和密鑰(鹽)
jwtBuilder.signWith(SignatureAlgorithm.HS256, "123456789abcdefg");
//設定自定義的欄位,根據需求設定
Map<String, Object> claims = new HashMap<>();
claims.put("age", 24);
claims.put("money", 1234);
jwtBuilder.addClaims(claims);
//生成一個token令牌
String token = jwtBuilder.compact();
System.out.println(token);
/**
* ===================================接下來準備決議token
*/
Claims body = Jwts.parser().setSigningKey("123456789abcdefg").parseClaimsJws(token).getBody();
System.out.println(body);
}
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiLmm7nmmajno4oiLCJpYXQiOjE2MTI3MDE4NzgsImF1ZCI6IuS4jeefpeWQjeeahOWuouaIt-erryIsImV4cCI6MTYxMjcwNTQ3OCwianRpIjoiYjMwOGE0OWUtZGYyNi00M2M5LThmN2UtMjA2YTVjNWI4M2RlIiwibW9uZXkiOjEyMzQsImFnZSI6MjR9.vgCFmoWKsxWaoKJqwyrcoDavQ_wmNjremjrTqkd6ZvA
{iss=曹晨磊, iat=1612701878, aud=不知名的客戶端, exp=1612705478, jti=b308a49e-df26-43c9-8f7e-206a5c5b83de, money=1234, age=24}
下面這幅圖是我用官網的可視化工具進行決議的:

1.2.5、JWT的隱患
從JWT生成的token組成上來看,要想避免token被偽造,主要就得看簽名部分了,而簽名部分又有三部分組成,其中頭部和載荷的Base64Url編碼,幾乎是透明的,毫無安全性可言,那么最終守護token安全的重擔就落在了加入的密鑰(鹽)上面了! 試想:如果生成token所用的鹽與決議token時加入的鹽是一樣的,豈不是類似于中國人民銀行把人民幣防偽技術公開了?大家可以用這個鹽來決議token,就能用來偽造token, 這時,我們就需要對鹽采用非對稱加密的方式進行加密,以達到生成token與校驗token方所用的鹽不一致的安全效果!
1.3、RSA
1.3.1、RSA的介紹
1976年,兩位美國計算機學家Whitfield Diffie和Martin Hellman,提出了一種嶄新構思,可以在不直接傳遞密鑰的情況下,完成解密,這被稱為"Diffie-Hellman密鑰交換演算法",這個演算法啟發了其他科學家,人們認識到,加密和解密可以使用不同的規則,只要這兩種規則之間存在某種對應關系即可,這樣就避免了直接傳遞密鑰,而這種新的加密模式被稱為"非對稱加密演算法",
RSA是1977年由羅納德·李維斯特(Ron Rivest)、阿迪·薩莫爾(Adi Shamir)和倫納德·阿德曼(Leonard Adleman)一起提出的,當時他們三人都在麻省理工學院作業,RSA就是他們三人姓氏開頭字母拼在一起組成的 ,從那時直到現在,RSA演算法一直是最廣為使用的"非對稱加密演算法",毫不夸張地說,只要有計算機網路的地方,就有RSA演算法,
這種演算法非常可靠,密鑰越長,它就越難破解,根據已經披露的文獻,目前被破解的最長RSA密鑰是768個二進制位,也就是說,長度超過768位的密鑰,還無法破解(至少沒人公開宣布),因此可以認為,1024位的RSA密鑰基本安全,2048位的密鑰極其安全,
1.3.2、公鑰私鑰的介紹
在對稱加密的時代,加密和解密用的是同一個密鑰,這個密鑰既用于加密,又用于解密,這樣做有一個明顯的缺點,如果兩個人之間傳輸檔案,兩個人都要知道密鑰,如果是三個人呢,五個人呢?于是就產生了非對稱加密,用一個密鑰進行加密(公鑰),用另一個密鑰進行解密(私鑰),
我們假設,張三有兩把鑰匙,一把是公鑰,另一把是私鑰,
張三把公鑰送給他的朋友們:李四、王五、趙六,每人一把,
李四要給張三寫一封保密的信,她寫完后用張三的公鑰加密,就可以達到保密的效果,
張三收信后,用私鑰解密,就看到了信件內容,這里要強調的是,只要張三的私鑰不泄露,這封信就是安全的,即使落在別人手里,也無法解密,
張三給李四回信,決定采用"數字簽名",他寫完后先用Hash函式,生成信件的摘要(digest),然后使用張三自己的私鑰進行加密得到簽名,張三將這個簽名,附在信件上面,一起發給李四,類似JWT,

李四收信后,取下數字簽名,用張三的公鑰解密,得到信件的摘要,李四再對信件本身使用Hash函式,將得到的結果,與上一步得到的摘要進行對比,如果兩者一致,就證明這封信未被修改過,由此證明,這封信確實是張三發出的,

最終總結:既然是加密,那肯定是不希望別人知道我的訊息,所以只有我才能解密,所以可得出公鑰負責加密,私鑰負責解密;同理,既然是簽名,那肯定是不希望有人冒充我發訊息,只有我才能發布這個簽名,所以可得出私鑰負責簽名,公鑰負責驗證,
1.3.3、公鑰私鑰的生成
@Test
public void testGenerateKeyPair() throws Exception {
// 定義密鑰
String secret = "123456";
// 固定格式
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
keyPairGenerator.initialize(2048, secureRandom);
// 生成一對公鑰和私鑰,KeyPair內部就是由PublicKey和PrivateKey組成
KeyPair keyPair = keyPairGenerator.genKeyPair();
// 獲取公鑰并對公鑰進行Base64編碼(編碼后方便查看,你不編碼啥都看不懂)
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
System.out.println("公鑰Base64編碼后:" + new String(publicKeyBytes));
// 獲取私鑰并對私鑰進行Base64編碼(編碼后方便查看,你不編碼啥都看不懂)
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
System.out.println("私鑰Base64編碼后:" + new String(privateKeyBytes));
}
公鑰Base64編碼后:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmtyGIVnd2eDDN+3wQebfUjWd2KtKfuy50T1is1eNkcmI72aYW90FJhgI4vdiNW5G6XDW2MWgIRevqkNZO4tn3oJ5rFEcu2bSY2DuIaDiWdgUI2A1N5JAweZ5GVlxnfqMDMKvI9qT9aEUQanNqAgONtf4AofCiJt8N8ZyzwMiiD7YKjx19Njdwc3MNMscUC4Bcx8QrLcn5wQC27hhTgvcvj09e9V8FlibowSK+nURiQAfSQqMQ1SZNIM0WSobLDTZhgYD/5j/SPh3wok1ne2pYAmj01K8EgvQr/k/Lk9nIfs41FRLljlSbWTOnr2nb7BHmMbKeYG+nlZm7SIBV2tKvwIDAQAB
私鑰Base64編碼后:MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa3IYhWd3Z4MM37fBB5t9SNZ3Yq0p+7LnRPWKzV42RyYjvZphb3QUmGAji92I1bkbpcNbYxaAhF6+qQ1k7i2fegnmsURy7ZtJjYO4hoOJZ2BQjYDU3kkDB5nkZWXGd+owMwq8j2pP1oRRBqc2oCA421/gCh8KIm3w3xnLPAyKIPtgqPHX02N3Bzcw0yxxQLgFzHxCstyfnBALbuGFOC9y+PT171XwWWJujBIr6dRGJAB9JCoxDVJk0gzRZKhssNNmGBgP/mP9I+HfCiTWd7algCaPTUrwSC9Cv+T8uT2ch+zjUVEuWOVJtZM6evadvsEeYxsp5gb6eVmbtIgFXa0q/AgMBAAECggEASmU+mq8NgSoVHr1T+pTrHBdd6UUA2NDow7h1viqFfFARVNE4yIj5fD93pXGq4HhF4MewrxrhvoQeg/Eu4Qgrsh2ETl/5KZ5P3CYowEcF9ptzsTr61eOQ8JXD/4WUq4w907ODZ/oNsqbbkF/+yIZ2Laq7HpwRvIbVugXACes7n6+sn2SduP3uMFMPvzF12EbJUVsw/oKxAhrHg5QZOdfvQbXRlv0SK2wqH7Ti0TWlhr+QgRLLbJEEk7mGxqx8HhyXhljkueKLfSx+nmH+QdmrvJezY2EdAAwzojKq8FqeZEUBQCnhUk9tMyXuyrfn8TW4C1x//zLitngMi5vNhwxaYQKBgQD7m6+lu+tq6qLT8EyjgnMoXsCEPrEF9YyjN0mQMS5+6qO2u/riZ5um8hak2NZYTVyvXAQ0GSl5DdDiDOfUmGTzkB6VWNF+nAzQkMQfamw66i7rSaoFoSA5pnkBxz6lydkB1/OsBB/dj90i0Ti0v/SmITynwWsU5qU711EtF+K39QKBgQCdkIYjBD2gx3scBrAQMNv6OIhKdNL5vIz9PJXfRxJv/o5HUfbzxXQnTNwOFjuRGDXeTwbUBeZstStKyHnOBUi+8JlhjdbHKIN6nM2JsmJTQGOE1NGoaeKdpxAvETiCpWqquxugetafaz7+5cK5C0aY+CMJzsy4Jbt8j8/ov3grYwKBgQDVWsZOHpTZW8/pMip6uIKYKAjN2y9XY0n3mUlK+Tl5K9TZfnuXAs5teXmUHb9cr3U5yihSWUfeu8V1+gWYNAXet0YH1III/6CqNyfnj+Ho724L3LJNBb2CxVR1GpRYF1pqAspBAlpXEcgt3wZb1y5ItYRuqEf6OD7DCKlwOIHrBQKBgG/3YoqBmfWlq4Mn8Xcf8UHnaFpYmA+lgB74LZxDmgOBxcNCqJVjy/2dbYaJH/0kUitOxxBlvO+k8kWrHntbX+1ndedP7r8JuByqTpi57YsxZ0beILpnvATB0gtQVnLob1sxqRkqEVep01M5HF14eMt9EREIJov5LDkAzQKdBRz3AoGBAO7IOzHp/c8pdsVpWdZNdBXPGZTlFHm0V3MemsgOad00ly11NQg8Nh5l2JXF+iDlgqepXJjoSwk5tVvxVB0MrDS1/efFVHyt8PRR/RbNilNSBjQRkKdBt8mdWEpJpiAPBhr4ln04odTsY/QTFPPrHMcu/pnvHS+NIvPHH84YCF9K
1.4、SSO
1.4.1、SSO的介紹

假設用戶訪問的專案中,至少有3個微服務需要識別用戶身份,如果用戶訪問每個微服務都登錄一次就太麻煩了,為了提高用戶的體驗,我們需要實作讓用戶在一個系統中登錄,其他任意受信任的系統都可以訪問,這個功能就叫單點登錄,
單點登錄(Single Sign On),簡稱為 SSO,是目前比較流行的企業業務整合的解決方案之一, SSO的定義是在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統,
1.4.2、SSO的實作
Java中有很多用戶認證的框架都可以實作單點登錄:
- Shiro
- CAS
- Spring Security CAS
- Spring Security JWT
- Spring Security OAuth2.0
第二章 OAuth2.0的介紹篇
2.1、OAuth2.0的概述
OAuth是Open Authorization的簡寫,OAuth協議為用戶資源的授權提供了一個安全的、開放而又簡易的標準,與以往的授權方式不同之處是OAuth的授權不會使第三方觸及到用戶的帳號資訊(如用戶名與密碼),即第三方無需使用用戶的用戶名與密碼就可以申請獲得該用戶資源的授權,因此OAuth是安全的,
OAuth2.0是OAuth協議的延續版本,但不向前兼容(即完全廢止了OAuth1.0),
那么,我們接下來的學習目標是什么,可以參考如下學習串列:
- 學習精確到按鈕級別的權限控制管理系統的設計(這一塊不算是OAuth2.0的內容)
- 學習如何為第三方應用授予訪問系統資源的權限(類似實作網站上的QQ快速登錄)
- 學習如何在微服務環境下實作統一單點登錄功能
- 解決單點登錄后如何實作服務與服務之間的呼叫
2.2、OAuth2.0的模式

2.2.1、授權碼模式
模式名稱
authorization code
使用流程
說明:【A服務客戶端】需要用到【B服務資源服務】中的資源,
- 第一步:【A服務客戶端】將用戶自動導航到【B服務認證服務】,這一步用戶需要提供一個回呼地址,以備 【B服務認證服務】回傳授權碼使用,
- 第二步:用戶點擊授權按鈕表示讓【A服務客戶端】使用【B服務資源服務】,這一步需要用戶登錄B服務,也就是說用戶要事先具有B服務的使用權限,
- 第三步:【B服務認證服務】生成授權碼,授權碼將通過第一步提供的回呼地址,回傳給【A服務客戶端】, 注意這個授權碼并非通行【B服務資源服務】的通行憑證,
- 第四步:【A服務認證服務】攜帶上一步得到的授權碼向【B服務認證服務】發送請求,獲取通行憑證token,
- 第五步:【B服務認證服務】給【A服務認證服務】回傳令牌token和更新令牌refresh token,
使用場景
授權碼模式是OAuth2.0中最安全最完善的一種模式,應用場景最廣泛,可以實作服務之間的呼叫,常見的微信,QQ等第三方登錄也可采用這種方式實作,
2.2.2、簡化模式
模式名稱
implicit
使用流程
說明:簡化模式中沒有【A服務認證服務】這一部分,全部由【A服務客戶端】與B服務互動,整個程序不再有授權碼,token直接暴露在瀏覽器,
- 第一步:【A服務客戶端】將用戶自動導航到【B服務認證服務】,這一步用戶需要提供一個回呼地址,以備 【B服務認證服務】回傳token使用,還會攜帶一個【A服務客戶端】的狀態標識state,
- 第二步:用戶點擊授權按鈕表示讓【A服務客戶端】使用【B服務資源服務】,這一步需要用戶登錄B服務,也就是說用戶要事先具有B服務的使用權限,
- 第三步:【B服務認證服務】生成通行令牌token,token將通過第一步提供的回呼地址,回傳給【A服務客戶端】,
使用場景
適用于A服務沒有服務器的情況,比如:純手機小程式,JavaScript語言實作的網頁插件等,
2.2.3、密碼模式
模式名稱
resource owner password credentials
使用流程
- 第一步:直接告訴【A服務客戶端】自己的【B服務認證服務】的用戶名和密碼,
- 第二步:【A服務客戶端】攜帶【B服務認證服務】的用戶名和密碼向【B服務認證服務】發起請求獲取 token,
- 第三步:【B服務認證服務】給【A服務客戶端】頒發token,
使用場景
此種模式雖然簡單,但是用戶將B服務的用戶名和密碼暴露給了A服務,需要兩個服務信任度非常高才能使用,
2.2.4、客戶端模式
模式名稱
client credentials
使用流程
說明:這種模式其實已經不太屬于OAuth2.0的范疇了,A服務完全脫離用戶,以自己的身份去向B服務索取token,換言之,用戶無需具備B服務的使用權也可以,完全是A服務與B服務內部的互動,與用戶無關了,
- 第一步:A服務向B服務索取token,
- 第二步:B服務回傳token給A服務,
使用場景
A服務本身需要B服務資源,與用戶無關,
第三章 OAuth2.0的權限控制
3.1、專案的準備與介紹
請到配套資料中的01-基礎代碼中找到對應的工程代碼,并使用idea打開這個工程,如果一切正常,那么你將看到如下界面:

為了學習的方便,我特意重新建了這個工程,這里邊有四個專案,他們基本上都是空的,我已經把要用到的相關依賴、組態檔、包結構都準備好了,如果你打開查看,基本都能看懂 ,并沒有什么特別的代碼,并且這四個專案,在學習的前期,我們基本上就會用到其中的一到兩個,oauth2-server-eureka1000注冊中心我們只要單純的啟動就可以了,本章也不對注冊中心進行介紹,重點學習將在oauth2-server-auth1001和oauth2-resource-order1002工程中,
我們本章將要學習精確到按鈕級別的權限控制管理系統的設計,大部分都是代碼,而且涉及到的陳述句都是最基本的增刪改查,相信不難理解,
3.2、資料庫的資料匯入
我們采用的資料庫是mysql 5.5,我建議你和我保持一致,打開你的圖形化界面工具,運行以下sql陳述句:
/*創建資料庫*/
CREATE DATABASE `spring-cloud-oauth2`;
/*使用資料庫*/
USE `spring-cloud-oauth2`;
/*匯入用戶表*/
DROP TABLE IF EXISTS `sys_user` ;
CREATE TABLE `sys_user` (
`id` INT (11) NOT NULL AUTO_INCREMENT COMMENT '用戶編號',
`username` VARCHAR (64) NOT NULL COMMENT '用戶姓名',
`password` VARCHAR (64) NOT NULL COMMENT '用戶密碼',
`avatar` VARCHAR (128) NOT NULL COMMENT '用戶頭像',
`mobile` VARCHAR (64) NOT NULL COMMENT '用戶手機',
`email` VARCHAR (64) NOT NULL COMMENT '用戶郵箱',
`status` INT (1) NOT NULL DEFAULT '0' COMMENT '用戶狀態',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1003 DEFAULT CHARSET=utf8;
/*匯入用戶資料*/
INSERT INTO `sys_user`(`id`,`username`,`password`,`avatar`,`mobile`,`email`,`status`)
VALUES (1000,'zhangsan','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',
'https://qlogo2.store.qq.com/qzone/774908833/774908833/100','15633029014','774908833@qq.com',0);
INSERT INTO `sys_user`(`id`,`username`,`password`,`avatar`,`mobile`,`email`,`status`)
VALUES (1001,'lisi','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',
'https://qlogo2.store.qq.com/qzone/774908833/774908833/100','15633029014','774908833@qq.com',0);
INSERT INTO `sys_user`(`id`,`username`,`password`,`avatar`,`mobile`,`email`,`status`)
VALUES (1002,'wangwu','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',
'https://qlogo2.store.qq.com/qzone/774908833/774908833/100','15633029014','774908833@qq.com',0);
/*匯入角色表*/
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '角色編號',
`name` VARCHAR(64) NOT NULL COMMENT '角色名稱',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1003 DEFAULT CHARSET=utf8;
/*匯入角色資料*/
INSERT INTO `sys_role`(`id`,`name`) VALUES (1000,'系統管理員');
INSERT INTO `sys_role`(`id`,`name`) VALUES (1001,'訂單管理員');
INSERT INTO `sys_role`(`id`,`name`) VALUES (1002,'商品管理員');
/*匯入選單表*/
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '選單編號',
`name` VARCHAR(64) DEFAULT NULL COMMENT '選單名稱',
`code` VARCHAR(64) DEFAULT NULL COMMENT '選單權限',
`type` INT(11) DEFAULT NULL COMMENT '選單型別(0:目錄、1:頁面、2:按鈕)',
`icon` VARCHAR(64) DEFAULT NULL COMMENT '選單圖示',
`url` VARCHAR(64) DEFAULT NULL COMMENT '選單地址',
`level` INT(11) DEFAULT NULL COMMENT '選單級別(用于快速區分當前選單層級)',
`path` VARCHAR(256) DEFAULT NULL COMMENT '選單路徑(用于快速找到當前選單祖輩)',
`sort` INT(11) DEFAULT NULL COMMENT '選單排序',
`status` INT(11) DEFAULT '0' COMMENT '選單狀態(0:正常、1:禁用)',
`parent_id` INT(11) DEFAULT NULL COMMENT '上級選單',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1028 DEFAULT CHARSET=utf8;
/*匯入選單資料*/
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1000,'系統','system',0,'el-icon-folder',NULL,1,NULL,1,0,0);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1001,'用戶管理','userMgr',1,'el-icon-menu',NULL,2,NULL,1,0,1000);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1002,'角色管理','roleMgr',1,'el-icon-menu',NULL,2,NULL,2,0,1000);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1003,'選單管理','menuMgr',1,'el-icon-menu',NULL,2,NULL,3,0,1000);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1004,'用戶管理:查詢','userMgr:find',2,'el-icon-search',NULL,3,NULL,1,0,1001);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1005,'用戶管理:新增','userMgr:add',2,'el-icon-plus',NULL,3,NULL,2,0,1001);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1006,'用戶管理:洗掉','userMgr:delete',2,'el-icon-delete',NULL,3,NULL,3,0,1001);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1007,'用戶管理:修改','userMgr:update',2,'el-icon-edit',NULL,3,NULL,4,0,1001);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1008,'角色管理:查詢','roleMgr:find',2,'el-icon-search',NULL,3,NULL,1,0,1002);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1009,'角色管理:新增','roleMgr:add',2,'el-icon-plus',NULL,3,NULL,2,0,1002);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1010,'角色管理:洗掉','roleMgr:delete',2,'el-icon-delete',NULL,3,NULL,3,0,1002);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1011,'角色管理:修改','roleMgr:update',2,'el-icon-edit',NULL,3,NULL,4,0,1002);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1012,'選單管理:查詢','menuMgr:find',2,'el-icon-search',NULL,3,NULL,1,0,1003);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1013,'選單管理:新增','menuMgr:add',2,'el-icon-plus',NULL,3,NULL,2,0,1003);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1014,'選單管理:洗掉','menuMgr:delete',2,'el-icon-delete',NULL,3,NULL,3,0,1003);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1015,'選單管理:修改','menuMgr:update',2,'el-icon-edit',NULL,3,NULL,4,0,1003);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1016,'訂單','order',0,'el-icon-folder',NULL,1,NULL,2,0,0);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1017,'訂單管理','orderMgr',1,'el-icon-menu',NULL,2,NULL,1,0,1016);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1018,'訂單管理:查詢','orderMgr:find',2,'el-icon-search',NULL,3,NULL,1,0,1017);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1019,'訂單管理:新增','orderMgr:add',2,'el-icon-plus',NULL,3,NULL,2,0,1017);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1020,'訂單管理:洗掉','orderMgr:delete',2,'el-icon-delete',NULL,3,NULL,3,0,1017);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1021,'訂單管理:修改','orderMgr:update',2,'el-icon-edit',NULL,3,NULL,4,0,1017);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1022,'商品','goods',0,'el-icon-folder',NULL,1,NULL,3,0,0);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1023,'商品管理','goodsMgr',1,'el-icon-menu',NULL,2,NULL,1,0,1022);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1024,'商品管理:查詢','goodsMgr:find',2,'el-icon-search',NULL,3,NULL,1,0,1023);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1025,'商品管理:新增','goodsMgr:add',2,'el-icon-plus',NULL,3,NULL,2,0,1023);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1026,'商品管理:洗掉','goodsMgr:delete',2,'el-icon-delete',NULL,3,NULL,3,0,1023);
INSERT INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1027,'商品管理:修改','goodsMgr:update',2,'el-icon-edit',NULL,3,NULL,4,0,1023);
/*匯入用戶與角色中間表*/
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`uid` INT(11) NOT NULL COMMENT '用戶編號',
`rid` INT(11) NOT NULL COMMENT '角色編號',
PRIMARY KEY (`uid`,`rid`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
/*匯入用戶與角色中間表資料*/
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (1000,1000);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (1000,1001);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (1000,1002);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (1001,1001);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (1002,1002);
/*匯入角色與選單中間表*/
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`rid` INT(11) NOT NULL COMMENT '角色編號',
`mid` INT(11) NOT NULL COMMENT '選單編號',
PRIMARY KEY (`rid`,`mid`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
/*匯入角色與選單中間表資料*/
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1000);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1001);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1002);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1003);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1004);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1005);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1006);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1007);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1008);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1009);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1010);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1011);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1012);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1013);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1014);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1015);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1001,1016);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1001,1017);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1001,1018);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1001,1019);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1001,1020);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1001,1021);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1002,1022);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1002,1023);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1002,1024);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1002,1025);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1002,1026);
INSERT INTO `sys_role_menu`(`rid`,`mid`) VALUES (1002,1027);
資料匯入完成以后,請檢查oauth2-server-auth1001、oauth2-resource-order1002、oauth2-resource-goods1003這三個專案的資料源的配置,
3.3、資料庫表設計架構

在初始化的資料中,有三個用戶分別是:zhangsan、lisi、wangwu,他們三個用戶分別具有以下的角色關系:
- zhangsan:系統管理員、訂單管理員、商品管理員
- lisi:訂單管理員
- wangwu:商品管理員
在初始化的資料中,有三個角色分別是:系統管理員、訂單管理員、商品管理員,他們三個角色分別具有以下的選單權限關系:
- 系統管理員:用戶管理(增刪改查)、角色管理(增刪改查)、選單管理(增刪改查)
- 訂單管理員:訂單管理(增刪改查)
- 商品管理員:商品管理(增刪改查)
3.4、Domain的撰寫
首先我們需要匯入spring-cloud-starter-oauth2的依賴檔案,請把以下依賴拷貝到oauth2-server-auth1001中,
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

我們需要撰寫物體來與資料庫中的欄位進行對應,但是為了后邊的授權更加方便,我們分別繼承Spring Security特有的類,在他們的基礎上進行拓展,
com.caochenlei.domain.SysMenu
@Data
public class SysMenu implements GrantedAuthority {
private Integer id;
private String name;
private String code;
private Integer type;
private String icon;
private String url;
private Integer level;
private String path;
private Integer sort;
private Integer status;
private Integer parent_id;
@Override
public String getAuthority() {
return code;
}
}
com.caochenlei.domain.SysRole
@Data
public class SysRole implements Serializable {
private Integer id;
private String name;
private List<SysMenu> sysMenus;
}
com.caochenlei.domain.SysUser
@Data
public class SysUser implements UserDetails {
private Integer id;
private String username;
private String password;
private String avatar;
private String mobile;
private String email;
private Integer status;
private List<SysRole> sysRoles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SysMenu> authorities = new ArrayList<>();
for (SysRole sysRole : sysRoles) {
List<SysMenu> sysMenus = sysRole.getSysMenus();
authorities.addAll(sysMenus);
}
return authorities;
}
/**
* 是否賬號已過期
*/
@Override
public boolean isAccountNonExpired() {
return status != 1;
}
/**
* 是否賬號已被鎖
*/
@Override
public boolean isAccountNonLocked() {
return status != 2;
}
/**
* 是否賬號已禁用
*/
@Override
public boolean isEnabled() {
return status != 3;
}
/**
* 是否密碼已過期
*/
@Override
public boolean isCredentialsNonExpired() {
return status != 4;
}
}
3.5、Mapper的撰寫
com.caochenlei.mapper.SysMenuMapper
@Mapper
public interface SysMenuMapper {
//根據角色編號查詢選單串列
@Select("select * from `sys_menu` where id in (" +
" select mid from `sys_role_menu` where rid = #{rid}" +
")")
@Results({
//主鍵欄位映射,property代表Java物件屬性,column代表資料庫欄位
@Result(property = "id", column = "id", id = true),
//普通欄位映射,property代表Java物件屬性,column代表資料庫欄位
@Result(property = "name", column = "name"),
@Result(property = "code", column = "code"),
@Result(property = "type", column = "type"),
@Result(property = "icon", column = "icon"),
@Result(property = "url", column = "url"),
@Result(property = "level", column = "level"),
@Result(property = "path", column = "path"),
@Result(property = "sort", column = "sort"),
@Result(property = "status", column = "status"),
@Result(property = "parent_id", column = "parent_id")
})
List<SysMenu> findByRid(Integer rid);
}
com.caochenlei.mapper.SysRoleMapper
@Mapper
public interface SysRoleMapper {
//根據用戶編號查詢角色串列
@Select("select * from `sys_role` where id in (" +
" select rid from `sys_user_role` where uid = #{uid}" +
")")
@Results({
//主鍵欄位映射,property代表Java物件屬性,column代表資料庫欄位
@Result(property = "id", column = "id", id = true),
//普通欄位映射,property代表Java物件屬性,column代表資料庫欄位
@Result(property = "name", column = "name"),
//選單串列映射,根據角色id查詢該用戶所對應的選單串列sysMenus
@Result(property = "sysMenus", column = "id",
javaType = List.class,
many = @Many(select = "com.caochenlei.mapper.SysMenuMapper.findByRid")
)
})
List<SysRole> findByUid(Integer uid);
}
com.caochenlei.mapper.SysUserMapper
@Mapper
public interface SysUserMapper {
//根據用戶名稱查詢用戶資訊
@Select("select * from `sys_user` where `username` = #{username}")
@Results({
//主鍵欄位映射,property代表Java物件屬性,column代表資料庫欄位
@Result(property = "id", column = "id", id = true),
//普通欄位映射,property代表Java物件屬性,column代表資料庫欄位
@Result(property = "username", column = "username"),
@Result(property = "password", column = "password"),
@Result(property = "avatar", column = "avatar"),
@Result(property = "mobile", column = "mobile"),
@Result(property = "email", column = "email"),
@Result(property = "status", column = "status"),
//角色串列映射,根據用戶id查詢該用戶所對應的角色串列sysRoles
@Result(property = "sysRoles", column = "id",
javaType = List.class,
many = @Many(select = "com.caochenlei.mapper.SysRoleMapper.findByUid")
)
})
SysUser findByUsername(String username);
}
3.6、Service 的撰寫
com.caochenlei.service.SysUserDetailsService
public interface SysUserDetailsService extends UserDetailsService {
}
com.caochenlei.service.impl.SysUserDetailsServiceImpl
@Service
public class SysUserDetailsServiceImpl implements SysUserDetailsService {
//(required = false)可以不寫,去掉會報紅色波浪線
@Autowired(required = false)
private SysUserMapper sysUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根據用戶名去資料庫中查詢指定用戶,這就要保證資料庫中的用戶的名稱必須唯一,否則將會報錯
SysUser sysUser = sysUserMapper.findByUsername(username);
//如果沒有查詢到這個用戶,說明資料庫中不存在此用戶,認證失敗,此時需要拋出用戶賬戶不存在
if (sysUser == null) {
throw new UsernameNotFoundException("user not exist.");
}
return sysUser;
}
}
3.7、核心的配置物件
com.caochenlei.config.WebSecurityConfig
@Configuration//說明這是一個配置類
@EnableWebSecurity//開啟Web安全保護
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟方法權限控制
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SysUserDetailsService userDetailsService;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);//指明認證詳情的服務
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());//指明密碼的加密方式
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);//開啟用戶找不到例外
return daoAuthenticationProvider;
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider());//配置認證提供者
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()//所有請求都需要驗證
.and().formLogin().permitAll()//表單登錄我們要放行
.and().csrf().disable();//禁用csrf跨站保護
}
}
3.8、改造控制層方法
com.caochenlei.controller.OrderController
@RestController
@PreAuthorize("hasAuthority('orderMgr')")
@RequestMapping("/order")
public class OrderController {
//查詢所有
@PreAuthorize("hasAuthority('orderMgr:find')")
@RequestMapping("/findAll")
public String findAll() {
return "order findAll ...";
}
//分頁查詢
@PreAuthorize("hasAuthority('orderMgr:find')")
@RequestMapping("/findPage")
public String findPage() {
return "order findPage ...";
}
//主鍵查詢
@PreAuthorize("hasAuthority('orderMgr:find')")
@RequestMapping("/findById")
public String findById() {
return "order findById ...";
}
//新增訂單
@PreAuthorize("hasAuthority('orderMgr:add')")
@RequestMapping("/add")
public String add() {
return "order add ...";
}
//洗掉訂單
@PreAuthorize("hasAuthority('orderMgr:delete')")
@RequestMapping("/delete")
public String delete() {
return "order delete ...";
}
//修改訂單
@PreAuthorize("hasAuthority('orderMgr:update')")
@RequestMapping("/update")
public String update() {
return "order update ...";
}
}
com.caochenlei.controller.GoodstController
@RestController
@PreAuthorize("hasAuthority('goodsMgr')")
@RequestMapping("/goods")
public class GoodstController {
//查詢所有
@PreAuthorize("hasAuthority('goodsMgr:find')")
@RequestMapping("/findAll")
public String findAll() {
return "goods findAll ...";
}
//分頁查詢
@PreAuthorize("hasAuthority('goodsMgr:find')")
@RequestMapping("/findPage")
public String findPage() {
return "goods findPage ...";
}
//主鍵查詢
@PreAuthorize("hasAuthority('goodsMgr:find')")
@RequestMapping("/findById")
public String findById() {
return "goods findById ...";
}
//新增訂單
@PreAuthorize("hasAuthority('goodsMgr:add')")
@RequestMapping("/add")
public String add() {
return "goods add ...";
}
//洗掉訂單
@PreAuthorize("hasAuthority('goodsMgr:delete')")
@RequestMapping("/delete")
public String delete() {
return "goods delete ...";
}
//修改訂單
@PreAuthorize("hasAuthority('goodsMgr:update')")
@RequestMapping("/update")
public String update() {
return "goods update ...";
}
}
3.9、啟動專案并測驗
(1)首先啟動專案:oauth2-server-eureka1000
(2)然后啟動專案:oauth2-server-auth1001
(3)查看注冊中心:http://localhost:1000/

(4)登錄用戶李四:http://localhost:1001/login,賬戶:lisi,密碼:123456,登錄成功會報錯不用管,測驗地址:http://localhost:1001/order/findAll

(5)登錄用戶王五:http://localhost:1001/login,賬戶:wangwu,密碼:123456,登錄成功會報錯不用管,測驗地址:http://localhost:1001/order/findAll

(6)我們把之前測驗的結果總結一下
- 登錄成功會報錯,這是因為登錄成功后,默認會跳轉到首頁,可是我們根本沒有首頁,所以會報錯,這不是我們學習的重點,我們暫時忽略即可,
- 同一個地址,李四能訪問,但是王五不能訪問,這是因為李四有訂單管理的所有選單權限,而王五只有商品管理的所有選單權限,要想實作前端按鈕級別的的控制,前端的按鈕一般是對應后端的一個功能,前端的頁面一般是對應后端的一個類,這就是按鈕級別的權限管理系統的設計與實作,
第四章 OAuth2.0第三方授權
4.1、搭建認證中心
4.1.1、注入認證管理器
我們的OAuth2.0的實作是基于Spring Security框架的,因此,我們必須要用到Spring Security的認證管理器AuthenticationManager,具體做法如下:
@Configuration//說明這是一個配置類
@EnableWebSecurity//開啟Web安全保護
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟方法權限控制
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
...
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
4.1.2、創建并啟用配置
com.caochenlei.config.AuthorizationServerConfig
@Configuration//說明這是一個配置類
@EnableAuthorizationServer//開啟認證服務器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
}
4.1.3、重寫父類的方法
在當前類AuthorizationServerConfig按下重寫父類快捷鍵CTRL+O,彈出對話框,選中該類的直接父類除構造方法之外的三個主要方法,然后重寫即可,
@Configuration//說明這是一個配置類
@EnableAuthorizationServer//開啟認證服務器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure(endpoints);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure(security);
}
}
以下是這三個方法的介紹,接下來的所有配置都是圍繞這三個方法進行展開的,千萬不要死記硬背,要知道為什么用他以及怎么用他,
- ClientDetailsServiceConfigurer clients:用來配置第三方客戶端接入本系統的資訊,例如客戶端id、客戶端secret、授權方式等等,
- AuthorizationServerEndpointsConfigurer endpoints:用來配置令牌的訪問端點和令牌服務資訊,第一步注入的認證管理器就是用在這里,
- AuthorizationServerSecurityConfigurer security:用來配置令牌端點的安全約束,
4.1.4、配置客戶端的詳情
@Configuration//說明這是一個配置類
@EnableAuthorizationServer//開啟認證服務器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()//使用記憶體模式存盤第三方客戶端的資訊,多個客戶端之間使用and()方法連接,這里為了方便只寫一個客戶端
.withClient("myclient1")//客戶端id
.secret(passwordEncoder.encode("123456"))//客戶端密碼,這里使用了加密
.resourceIds("RESOURCE-ORDER", "RESOURCE-GOODS")//該客戶端擁有的資源標識,資源標識也是自己隨便定義的
.authorizedGrantTypes(//該客戶端允許的授權型別,refresh_token不是四種模式之一,僅用于重繪token
"authorization_code",//開啟授權碼模式,這個字串就長這樣是框架內部固定的
"implicit",//開啟簡化模式,這個字串就長這樣是框架內部固定的
"password",//開啟密碼模式,這個字串就長這樣是框架內部固定的
"client_credentials",//開啟客戶端模式,這個字串就長這樣是框架內部固定的
"refresh_token")//該字串僅僅用于重繪token的時候會使用,跟四種模式沒有關系
.scopes("read", "write")//該客戶端允許的授權范圍,自己隨便定義的,表示可以訪問資源服務器的哪些資源
.autoApprove(false)//是否通過,false代表需要展示授權界面,讓用戶自己選擇是不是通過授權,類似網頁上的QQ快速登錄界面,可能有點簡陋
.redirectUris("http://www.baidu.com");//回傳授權碼的回呼地址,由于現在沒有第三方應用接入,為了方便測驗,這里填寫百度地址
}
...
...
}
4.1.5、配置令牌訪問端點
@Configuration//說明這是一個配置類
@EnableAuthorizationServer//開啟認證服務器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
...
...
//注入認證管理器
@Autowired
private AuthenticationManager authenticationManager;
//注入客戶端詳情服務
@Autowired
private ClientDetailsService clientDetailsService;
//生成的token儲存在記憶體中
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
//生成的授權碼儲存在記憶體中
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service = new DefaultTokenServices();//使用默認的token服務
service.setClientDetailsService(clientDetailsService);//客戶端詳情服務
service.setTokenStore(tokenStore());//token生成以后存盤在哪里
service.setSupportRefreshToken(true);//是否支持重繪token,默認值為:false
service.setReuseRefreshToken(false);//是否拒絕重繪token,默認值為:true
service.setAccessTokenValiditySeconds(3600);//生成的token默認有效期為1小時,默認值為:43200
service.setRefreshTokenValiditySeconds(7200);//重繪token的默認有效期為2小時,默認值為:2592000
return service;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)//使用認證管理器
.tokenServices(tokenService())//生成的token服務
.authorizationCodeServices(authorizationCodeServices())//生成的授權碼存盤在哪里
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//只允許post請求訪問token端點
}
...
...
}
4.1.6、配置令牌端點安全
@Configuration//說明這是一個配置類
@EnableAuthorizationServer//開啟認證服務器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
...
...
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")//oauth/token_key公開
.checkTokenAccess("permitAll()")//oauth/check_token公開
.allowFormAuthenticationForClients();//允許表單認證
}
}
4.1.7、啟動認證的服務器
重新啟動專案:oauth2-server-auth1001
4.1.8、測驗四種模式使用
4.1.8.1、測驗授權碼模式
首先要獲取授權碼,正常來說,一旦獲取到授權碼就應該要申請token令牌,但是為了讓大家更加清楚這個流程,申請授權碼和申請token令牌分開來做,
首先打開瀏覽器,我們要使用一個地址并攜帶有效引數來獲取授權碼,第一次獲取提示你需要登錄,登錄以后就會提示你是否授權應用相應的訪問權限,如圖:
http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=code&scope=read%20write&&redirect_uri=http://www.baidu.com



第二步我們需要使用postman來發送post請求,使用授權碼模式來申請token令牌,具體引數如下:
請求地址:http://localhost:1001/oauth/token
請求型別:POST
請求引數:
- client_id:客戶端id
- client_secret:客戶端密碼
- grant_type:授權型別
- redirect_uri:回呼地址
- code:授權碼
- username:系統內某個用戶名稱
- password:系統內某個用戶密碼
演示效果:

4.1.8.2、測驗簡化模式
訪問地址:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=token&scope=read%20write&redirect_uri=http://www.baidu.com


4.1.8.3、測驗密碼模式
請求地址:http://localhost:1001/oauth/token
請求型別:POST
請求引數:
- client_id:客戶端id
- client_secret:客戶端密碼
- grant_type:授權型別
- username:系統內某個用戶名稱
- password:系統內某個用戶密碼
演示效果:

4.1.8.4、測驗客戶端模式
請求地址:http://localhost:1001/oauth/token
請求型別:POST
請求引數:
- client_id:客戶端id
- client_secret:客戶端密碼
- grant_type:授權型別
演示效果:

4.1.8.5、測驗重繪Token
只有授權碼模式和密碼模式的回傳值中含有refresh_token,因此,只有這兩種模式可以進行token重繪,
請求地址:http://localhost:1001/oauth/token
請求型別:POST
請求引數:
- client_id:客戶端id
- client_secret:客戶端密碼
- grant_type:授權型別
- refresh_token:重繪的token值,對應每次生成的
refresh_token
演示效果:

4.2、搭建訂單資源
4.2.1、匯入相關的依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
4.2.2、創建并啟用配置
com.caochenlei.config.ResourceServerConfig
@Configuration//說明這是一個配置類
@EnableResourceServer//開啟資源服務器
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟方法權限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
}
4.2.3、重寫父類的方法
在當前類ResourceServerConfig按下重寫父類快捷鍵CTRL+O,彈出對話框,選中該類的直接父類除構造方法之外的兩個主要方法,然后重寫即可,
@Configuration//說明這是一個配置類
@EnableResourceServer//開啟資源服務器
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟方法權限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
}
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}
以下是這兩個方法的介紹,接下來的所有配置都是圍繞這兩個方法進行展開的,千萬不要死記硬背,要知道為什么用他以及怎么用他,
- ResourceServerSecurityConfigurer resources:專門用于配置資源服務,
- HttpSecurity http:這個方法跟
Spring Security的配置一樣,
4.2.4、配置資源的資訊
@Configuration//說明這是一個配置類
@EnableResourceServer//開啟資源服務器
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟方法權限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
@Bean
public ResourceServerTokenServices tokenService() {
//使用遠程服務請求授權服務器校驗token
RemoteTokenServices service = new RemoteTokenServices();
service.setCheckTokenEndpointUrl("http://localhost:1001/oauth/check_token");//認證服務檢查token地址
service.setClientId("myclient1");//客戶端id
service.setClientSecret("123456");//客戶端密碼
return service;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("RESOURCE-ORDER")//當前資源的標識
.tokenStore(tokenStore())//當前令牌的存盤
.tokenServices(tokenService())//令牌驗證服務
.stateless(true);//禁用當前session會話
}
...
...
}
4.2.5、配置資源的權限
@Configuration//說明這是一個配置類
@EnableResourceServer//開啟資源服務器
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟方法權限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
...
...
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//指定不同請求方式訪問資源所需要的權限,一般查詢是read,其余是write,
.antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
.antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
//允許cors
.and().cors()
//禁用csrf
.and().csrf().disable()
//禁用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
4.2.6、拷貝控制器代碼
把oauth2-server-auth1001的OrderController拷貝到oauth2-resource-order1002的controller中,
4.2.7、啟動訂單的資源
啟動專案:oauth2-resource-order1002
4.2.8、測驗權限的控制
首先來說下,這四種模式生成的token都可以用來訪問資源服務,這里以密碼模式為例進行演示,訪問一個資源的一個控制器方法,首先要看你有沒有當前這個資源的權限也就是我們之前配置的scope,如果你有當前資源的權限,我們再來看看你登錄的賬戶到底有沒有該方法的權限,這部分是根據資料庫用戶與角色與選單之間的關系得出的,
首先使用李四這個賬戶,myclient1擁有訂單資源和商品資源服務的權限,而李四賬戶本身有訂單資源的所有方法權限,登錄后獲取access_token,如下:

在訪問具體方法的時候,需要把token也傳遞過去,引數頭為Authorization,引數值為Bearer+空格+access_token,訪問地址是你正常業務的地址,如下:

其次使用王五這個賬戶,myclient1擁有訂單資源和商品資源服務的權限,而王五賬戶本身沒有訂單資源的所有方法權限,登錄后獲取access_token,如下:

在訪問具體方法的時候,需要把token也傳遞過去,引數頭為Authorization,引數值為Bearer+空格+access_token,訪問地址是你正常業務的地址,如下:

第五章 OAuth2.0對接資料庫
我們現在生成的token、重繪token、授權碼、授權的權限都是存放在記憶體中的,本章節將會介紹如何將這些資訊存放到資料庫中,
5.1、升級認證中心
5.1.1、匯入官方資料庫表
DROP TABLE IF EXISTS `oauth_client_details` ;
CREATE TABLE `oauth_client_details` (
`client_id` VARCHAR (255) PRIMARY KEY,
`resource_ids` VARCHAR (256),
`client_secret` VARCHAR (256),
`scope` VARCHAR (256),
`authorized_grant_types` VARCHAR (256),
`web_server_redirect_uri` VARCHAR (256),
`authorities` VARCHAR (256),
`access_token_validity` INTEGER,
`refresh_token_validity` INTEGER,
`additional_information` VARCHAR (4096),
`autoapprove` VARCHAR (256)
) ENGINE = INNODB DEFAULT CHARSET = utf8 ;
DROP TABLE IF EXISTS `oauth_client_token` ;
CREATE TABLE `oauth_client_token` (
`token_id` VARCHAR (256),
`token` BLOB,
`authentication_id` VARCHAR (255) PRIMARY KEY,
`user_name` VARCHAR (256),
`client_id` VARCHAR (256)
) ENGINE = INNODB DEFAULT CHARSET = utf8 ;
DROP TABLE IF EXISTS `oauth_access_token` ;
CREATE TABLE `oauth_access_token` (
`token_id` VARCHAR (256),
`token` BLOB,
`authentication_id` VARCHAR (255) PRIMARY KEY,
`user_name` VARCHAR (256),
`client_id` VARCHAR (256),
`authentication` BLOB,
`refresh_token` VARCHAR (256)
) ENGINE = INNODB DEFAULT CHARSET = utf8 ;
DROP TABLE IF EXISTS `oauth_refresh_token` ;
CREATE TABLE `oauth_refresh_token` (
`token_id` VARCHAR (256),
`token` BLOB,
`authentication` BLOB
) ENGINE = INNODB DEFAULT CHARSET = utf8 ;
DROP TABLE IF EXISTS `oauth_code` ;
CREATE TABLE `oauth_code` (
`code` VARCHAR (256),
`authentication` BLOB
) ENGINE = INNODB DEFAULT CHARSET = utf8 ;
DROP TABLE IF EXISTS `oauth_approvals` ;
CREATE TABLE `oauth_approvals` (
`userId` VARCHAR (256),
`clientId` VARCHAR (256),
`scope` VARCHAR (256),
`status` VARCHAR (10),
`expiresAt` TIMESTAMP,
`lastModifiedAt` TIMESTAMP
) ENGINE = INNODB DEFAULT CHARSET = utf8 ;
INSERT INTO `oauth_client_details` (
`client_id`,
`resource_ids`,
`client_secret`,
`scope`,
`authorized_grant_types`,
`web_server_redirect_uri`,
`authorities`,
`access_token_validity`,
`refresh_token_validity`,
`additional_information`,
`autoapprove`
)
VALUES
(
'myclient1',
'RESOURCE-ORDER,RESOURCE-GOODS',
'$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',
'read,write',
'authorization_code,implicit,password,client_credentials,refresh_token',
'http://www.baidu.com',
NULL,
3600,
7200,
NULL,
'false'
) ;
5.1.2、還原配置物件狀態
@Configuration//說明這是一個配置類
@EnableAuthorizationServer//開啟認證服務器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
}
5.1.3、配置客戶端的詳情
@Configuration//說明這是一個配置類
@EnableAuthorizationServer//開啟認證服務器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
//資料庫連接池物件
@Autowired
private DataSource dataSource;
//客戶端的資訊來源
@Bean
public JdbcClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}
...
...
}
5.1.4、配置令牌訪問端點
@Configuration//說明這是一個配置類
@EnableAuthorizationServer//開啟認證服務器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
...
...
//注入認證管理器
@Autowired
private AuthenticationManager authenticationManager;
//授權碼模式資料來源
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
//生成的token儲存在資料庫中
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
//授權資訊保存在資料庫中
@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}
@Autowired
private SysUserDetailsService userDetailsService;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)//使用認證管理器
.authorizationCodeServices(authorizationCodeServices())//授權碼服務
.tokenStore(tokenStore())//token存盤在哪里
.approvalStore(approvalStore())//授權資訊存盤在哪里
.userDetailsService(userDetailsService)//使用用戶詳情服務
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//只允許post請求訪問token端點
}
...
...
}
5.1.5、配置令牌端點安全
@Configuration//說明這是一個配置類
@EnableAuthorizationServer//開啟認證服務器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
...
...
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")//oauth/token_key公開
.checkTokenAccess("permitAll()")//oauth/check_token公開
.allowFormAuthenticationForClients();//允許表單認證
}
}
5.1.6、重啟認證的服務器
重新啟動專案:oauth2-server-auth1001
5.1.7、測驗四種模式使用
5.1.7.1、測驗授權碼模式
獲取授權碼:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=code
獲取token:http://localhost:1001/oauth/token

5.1.7.2、測驗簡化模式
訪問地址:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=token&scope=read%20write&redirect_uri=http://www.baidu.com

5.1.7.3、測驗密碼模式

5.1.7.4、測驗客戶端模式

5.1.7.5、測驗重繪token

5.2、升級訂單資源
5.2.1、修改配置的物件
@Configuration//說明這是一個配置類
@EnableResourceServer//開啟資源服務器
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟方法權限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Bean
public ResourceServerTokenServices tokenService() {
//使用遠程服務請求授權服務器校驗token
RemoteTokenServices service = new RemoteTokenServices();
service.setCheckTokenEndpointUrl("http://localhost:1001/oauth/check_token");//認證服務檢查token地址
service.setClientId("myclient1");//客戶端id
service.setClientSecret("123456");//客戶端密碼
return service;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("RESOURCE-ORDER")//當前資源的標識
.tokenStore(tokenStore())//當前令牌的存盤
.tokenServices(tokenService())//令牌驗證服務
.stateless(true);//禁用當前session會話
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//指定不同請求方式訪問資源所需要的權限,一般查詢是read,其余是write,
.antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
.antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
//允許cors
.and().cors()
//禁用csrf
.and().csrf().disable()
//禁用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
5.2.2、重啟訂單的資源
重啟訂單資源oauth2-resource-order1002
5.2.3、測驗權限的控制
首先使用李四這個賬戶,myclient1擁有訂單資源和商品資源服務的權限,而李四賬戶本身有訂單資源的所有方法權限,登錄后獲取access_token,如下:

在訪問具體方法的時候,需要把token也傳遞過去,引數頭為Authorization,引數值為Bearer+空格+access_token,訪問地址是你正常業務的地址,如下:

其次使用王五這個賬戶,myclient1擁有訂單資源和商品資源服務的權限,而王五賬戶本身沒有訂單資源的所有方法權限,登錄后獲取access_token,如下:

在訪問具體方法的時候,需要把token也傳遞過去,引數頭為Authorization,引數值為Bearer+空格+access_token,訪問地址是你正常業務的地址,如下:

第六章 OAuth2.0對接公私鑰
上邊的架構存在一種問題,每一次去訪問資源服務的時候,資源服務都需要登錄到認證服務,然后對token進行校驗,這樣無疑降低了系統的性能,能不能我不去認證服務器認證,我也知道你這個token到底合不合法,答案肯定是有的,
我們在這里采用JWT的方式,之前也介紹過JWT,他是可以生成token的,而且,為了安全,我們建議簽名演算法采用私鑰加密,而資源服務只需要使用公鑰就可以驗證這個token到底是不是合法的,這樣,我們就省去了一次訪問認證服務去校驗token的情況,同時,由于使用了RSA的公鑰和私鑰,在安全上也得到了保證,
6.1、創建工具類別庫
把以下這個類復制到工程oauth2-server-auth1001和oauth2-resource-order1002中,
com.caochenlei.utils.RsaUtils
package com.caochenlei.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.nio.file.Files;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* 對Rsa操作進行了簡單封裝
*
* @author CaoChenLei
*/
@Slf4j
public class RsaUtils {
private static final int DEFAULT_KEY_SIZE = 2048;
/**
* 從檔案中獲取RSA公鑰物件
*
* @param filename 公鑰保存路徑,相對于classpath
* @return RSA公鑰物件
*/
public static RSAPublicKey getRSAPublicKey(String filename) {
return (RSAPublicKey) getPublicKey(filename);
}
/**
* 從檔案中獲取RSA私鑰物件
*
* @param filename 公鑰保存路徑,相對于classpath
* @return RSA私鑰物件
*/
public static RSAPrivateKey getRSAPrivateKey(String filename) {
return (RSAPrivateKey) (getPrivateKey(filename));
}
/**
* 從檔案中獲取公鑰物件
*
* @param filename 公鑰保存路徑,相對于classpath
* @return 公鑰物件
*/
public static PublicKey getPublicKey(String filename) {
try {
byte[] bytes = readFile(filename);
return getPublicKey(bytes);
} catch (Exception e) {
log.error("獲取公鑰物件失敗", e);
return null;
}
}
/**
* 從檔案中獲取私鑰物件
*
* @param filename 私鑰保存路徑,相對于classpath
* @return 私鑰物件
*/
public static PrivateKey getPrivateKey(String filename) {
try {
byte[] bytes = readFile(filename);
return getPrivateKey(bytes);
} catch (Exception e) {
log.error("獲取私鑰物件失敗", e);
return null;
}
}
/**
* 從檔案中獲取公鑰物件
*
* @param bytes 公鑰的位元組陣列形式
* @return
* @throws Exception
*/
public static PublicKey getPublicKey(byte[] bytes) throws Exception {
bytes = Base64.getDecoder().decode(bytes);
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
/**
* 從檔案中獲取私鑰物件
*
* @param bytes 私鑰的位元組陣列形式
* @return
* @throws Exception
*/
public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
bytes = Base64.getDecoder().decode(bytes);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
}
/**
* 根據你指定的密文(鹽),生成rsa公鑰和私鑰,并寫入指定檔案
*
* @param publicKeyFilename 公鑰檔案的路徑
* @param privateKeyFilename 私鑰檔案的路徑
* @param secret 生成密鑰的密文
*/
public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret, int keySize) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);
KeyPair keyPair = keyPairGenerator.genKeyPair();
// 獲取公鑰并寫出
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
writeFile(publicKeyFilename, publicKeyBytes);
// 獲取私鑰并寫出
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
writeFile(privateKeyFilename, privateKeyBytes);
}
public static byte[] readFile(String fileName) throws Exception {
return Files.readAllBytes(new File(fileName).toPath());
}
public static void writeFile(String destPath, byte[] bytes) throws Exception {
File dest = new File(destPath);
File parentFile = dest.getParentFile();
if (!parentFile.exists()) {
parentFile.mkdirs();
}
if (!dest.exists()) {
dest.createNewFile();
}
Files.write(dest.toPath(), bytes);
}
public static void main(String[] args) throws Exception {
String publicFile = "D:\\auth_key\\rsa_key.pub";
String privateFile = "D:\\auth_key\\rsa_key";
String secret = "123456789";
//向指定路徑寫出公鑰和私鑰
generateKey(publicFile, privateFile, secret, 2048);
System.out.println("generateKey done ...");
}
}
6.2、生成公鑰私鑰
運行RsaUtils的main方法,生成公鑰和私鑰,這里建議你先別改路徑,這個secret也就是鹽或者說密碼,你可以隨便改,
注意:如果報錯誤: 找不到或無法加載主類 com.caochenlei.utils.RsaUtils,請按快捷鍵
CTRL+F9重新編譯當前專案,然后在運行就沒事了,
6.3、修改認證服務配置
com.caochenlei.config.AuthorizationServerConfig
@Configuration//說明這是一個配置類
@EnableAuthorizationServer//開啟認證服務器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
//資料庫連接池物件
@Autowired
private DataSource dataSource;
//客戶端的資訊來源
@Bean
public JdbcClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}
//注入認證管理器
@Autowired
private AuthenticationManager authenticationManager;
//JWT令牌轉換器
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//用于生成token
converter.setSigner(new RsaSigner(RsaUtils.getRSAPrivateKey("D:\\auth_key\\rsa_key")));
//用于校驗token(重繪token會用到,如果不設定,將會重繪失敗)
converter.setVerifier(new RsaVerifier(RsaUtils.getRSAPublicKey("D:\\auth_key\\rsa_key.pub")));
return converter;
}
//JWT令牌存盤策略
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
//授權資訊保存在資料庫中
@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}
@Autowired
private SysUserDetailsService userDetailsService;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)//使用認證管理器
.accessTokenConverter(jwtAccessTokenConverter())//令牌轉換器
.tokenStore(tokenStore())//token存盤在哪里
.approvalStore(approvalStore())//授權資訊存盤在哪里
.userDetailsService(userDetailsService)//使用用戶詳情服務
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//只允許post請求訪問token端點
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")//oauth/token_key公開
.checkTokenAccess("permitAll()")//oauth/check_token公開
.allowFormAuthenticationForClients();//允許表單認證
}
}
修改完畢以后,我們需要重新啟動oauth2-server-auth1001
6.4、修改資源服務配置
com.caochenlei.config.ResourceServerConfig
@Configuration//說明這是一個配置類
@EnableResourceServer//開啟資源服務器
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟方法權限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setVerifier(new RsaVerifier(RsaUtils.getRSAPublicKey("D:\\auth_key\\rsa_key.pub")));
return converter;
}
@Bean
public TokenStore tokenStore() throws Exception {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("RESOURCE-ORDER")//當前資源的標識
.tokenStore(tokenStore())//當前令牌的存盤
.stateless(true);//禁用當前session會話
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//指定不同請求方式訪問資源所需要的權限,一般查詢是read,其余是write,
.antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
.antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
//允許cors
.and().cors()
//禁用csrf
.and().csrf().disable()
//禁用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
修改完畢以后,我們需要重新啟動oauth2-resource-order1002
6.5、測驗四種模式使用
6.5.1、測驗授權碼模式
獲取授權碼:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=code
獲取token:http://localhost:1001/oauth/token

查看token:https://jwt.io/

6.5.2、測驗簡化模式
訪問地址:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=token&scope=read%20write&redirect_uri=http://www.baidu.com

6.5.3、測驗密碼模式

6.5.4、測驗客戶端模式

6.5.5、測驗重繪token

6.6、測驗資源權限控制
首先使用李四這個賬戶,myclient1擁有訂單資源和商品資源服務的權限,而李四賬戶本身有訂單資源的所有方法權限,登錄后獲取access_token,如下:

在訪問具體方法的時候,需要把token也傳遞過去,引數頭為Authorization,引數值為Bearer+空格+access_token,訪問地址是你正常業務的地址,如下:

其次使用王五這個賬戶,myclient1擁有訂單資源和商品資源服務的權限,而王五賬戶本身沒有訂單資源的所有方法權限,登錄后獲取access_token,如下:

在訪問具體方法的時候,需要把token也傳遞過去,引數頭為Authorization,引數值為Bearer+空格+access_token,訪問地址是你正常業務的地址,如下:

第七章 OAuth2.0實作單點登錄
在這里,我使用一種取巧的方式開進行單點登錄,既然OAuth2.0可以為第三方應用授權訪問系統資源,我給當前的系統申請一個擁有所有資源權限的客戶端id不就行了,這里為了省事,我還是用myclient1,在登錄的時候,我們使用密碼模式進行登錄,但是,你不能每次讓用戶都自己輸入客戶端id和客戶端密鑰,我們需要寫一個控制器,接收用戶傳遞過來的賬戶和密碼,我們內部自己發送請求,以此實作登錄效果,登錄成功以后回傳用戶資料,
7.1、注入Rest模板
com.caochenlei.OAuth2Server1001Application
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@EnableHystrix
public class OAuth2Server1001Application {
public static void main(String[] args) {
SpringApplication.run(OAuth2Server1001Application.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
7.2、創建控制器類
com.caochenlei.controller.AuthController
@RestController
public class AuthController {
//當前認證服務器的IP地址
@Value("${spring.cloud.client.ip-address}")
private String authIp;
//當前認證服務器的Port埠
@Value("${server.port}")
private String authPort;
@Autowired
private RestTemplate restTemplate;
//宣告客戶端的id和secret,這里應該用組態檔方式注入進來,為了方便,我就寫死了
private String clientId = "myclient1";
private String clientSecret = "123456";
@RequestMapping("/user/login")
public Map login(String username, String password) {
//1.定義申請token的認證服務地址
String url = "http://" + authIp + ":" + authPort + "/oauth/token";
//2.定義頭資訊 (有client id 和client secr)
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
String base64 = Base64.getEncoder().encodeToString(new String(clientId + ":" + clientSecret).getBytes());
headers.add("Authorization", "Basic " + base64);
//3.定義請求體、有授權模式、用戶的名稱和密碼
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("grant_type", "password");
formData.add("username", username);
formData.add("password", password);
//4.模擬瀏覽器發送POST請求,攜帶請求頭和請求體到認證服務器
/**
* 引數1 指定要發送的請求的url
* 引數2 指定要發送的請求的方法 PSOT
* 引數3 指定請求物體(包含請求頭和請求體資料)
*/
HttpEntity<MultiValueMap> requestentity = new HttpEntity<>(formData, headers);
ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestentity, Map.class);
//5.接收到回傳的回應,就是令牌的資訊
Map body = responseEntity.getBody();
//6.自己封裝資料物件,不能隨便改變欄位,否則重繪token會發生問題,丟失部分欄位
Map<String, Object> response = new LinkedHashMap();
response.put("access_token", (String) body.get("access_token"));
response.put("token_type", (String) body.get("token_type"));
response.put("refresh_token", (String) body.get("refresh_token"));
response.put("expires_in", (Integer) body.get("expires_in"));
response.put("scope", (String) body.get("scope"));
response.put("jti", (String) body.get("jti"));
//7.回傳資料
return response;
}
}
7.3、開放登錄埠
com.caochenlei.config.WebSecurityConfig
@Configuration//說明這是一個配置類
@EnableWebSecurity//開啟Web安全保護
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟方法權限控制
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
...
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/login").permitAll()//放行登錄請求
.anyRequest().authenticated()//所有請求都需要驗證
.and().formLogin().permitAll()//表單登錄我們要放行
.and().csrf().disable();//禁用csrf跨站保護
}
...
...
}
7.4、測驗登錄效果
重新啟動:oauth2-server-auth1001
訪問地址:http://localhost:1001/user/login

7.5、測驗權限控制
首先使用李四這個賬戶,myclient1擁有訂單資源和商品資源服務的權限,而李四賬戶本身有訂單資源的所有方法權限,登錄后獲取access_token,如下:

在訪問具體方法的時候,需要把token也傳遞過去,引數頭為Authorization,引數值為Bearer+空格+access_token,訪問地址是你正常業務的地址,如下:

其次使用王五這個賬戶,myclient1擁有訂單資源和商品資源服務的權限,而王五賬戶本身沒有訂單資源的所有方法權限,登錄后獲取access_token,如下:

在訪問具體方法的時候,需要把token也傳遞過去,引數頭為Authorization,引數值為Bearer+空格+access_token,訪問地址是你正常業務的地址,如下:

第八章 OAuth2.0服務呼叫問題
8.1、商品資源服務準備
(1)oauth2-resource-goods1003添加依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
(2)拷貝oauth2-resource-order1002的com.caochenlei.utils.RsaUtils到oauth2-resource-goods1003
(3)拷貝oauth2-resource-order1002的com.caochenlei.config.ResourceServerConfig到oauth2-resource-goods1003
修改資源名稱
@Configuration//說明這是一個配置類
@EnableResourceServer//開啟資源服務器
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟方法權限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
...
...
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("RESOURCE-GOODS")//當前資源的標識
.tokenStore(tokenStore())//當前令牌的存盤
.stateless(true);//禁用當前session會話
}
...
...
}
(4)拷貝oauth2-server-auth1001的com.caochenlei.controller.GoodstController到oauth2-resource-goods1003
(5)啟動oauth2-resource-goods1003
8.2、訂單資源服務準備
(1)在oauth2-resource-order1002增加com.caochenlei.service.FeginGoodsService
(2)在oauth2-resource-order1002修改com.caochenlei.controller.OrderController
@RestController
@PreAuthorize("hasAuthority('orderMgr')")
@RequestMapping("/order")
public class OrderController {
@Autowired
private FeginGoodsService feginGoodsService;
//查詢所有
@PreAuthorize("hasAuthority('orderMgr:find')")
@RequestMapping("/findAll")
public String findAll() {
return "order findAll ...";
}
//分頁查詢
@PreAuthorize("hasAuthority('orderMgr:find')")
@RequestMapping("/findPage")
public String findPage() {
return "order findPage ...";
}
//主鍵查詢
@PreAuthorize("hasAuthority('orderMgr:find')")
@RequestMapping("/findById")
public String findById() {
String feginGoodsServiceFindById = feginGoodsService.findById();
return "order findById ... " + feginGoodsServiceFindById;
}
//新增訂單
@PreAuthorize("hasAuthority('orderMgr:add')")
@RequestMapping("/add")
public String add() {
return "order add ...";
}
//洗掉訂單
@PreAuthorize("hasAuthority('orderMgr:delete')")
@RequestMapping("/delete")
public String delete() {
return "order delete ...";
}
//更新訂單
@PreAuthorize("hasAuthority('orderMgr:update')")
@RequestMapping("/update")
public String update() {
return "order update ...";
}
}
(3)重啟oauth2-resource-order1002
8.3、訂單呼叫商品問題
我用自定義單點登錄的方式進行登錄,這里使用zhangsan來訪問查找訂單服務,訂單服務里邊會有很多商品,所以,查找訂單服務又呼叫了根據主鍵查找商品服務,而且,張三擁有對訂單所有方法和商品所有方法訪問的權限,但是在實際呼叫程序中,就會報出錯誤,如下圖:

造成上述問題的根本原因就是,資源服務器在請求的時候,需要攜帶token,來驗證這次請求是不是合法,很顯然,我們直接呼叫肯定會報錯,我們并沒有攜帶token,要解決也很簡單,在每一次請求之前都攜帶上token即可,
8.4、解決服務呼叫問題
在Feign所有請求之前,使用攔截器進行攔截,將token頭欄位追加到頭上就行了,
com.caochenlei.interceptor.FeignInterceptor
@Component
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
try {
//使用RequestContextHolder工具獲取request相關變數
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
//取出request
HttpServletRequest request = attributes.getRequest();
//獲取所有頭檔案資訊的key
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
//頭檔案的key
String name = headerNames.nextElement();
//頭檔案的value
String values = request.getHeader(name);
//將令牌資料添加到頭檔案中
requestTemplate.header(name, values);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
重新啟動oauth2-resource-order1002
8.5、演示最終解決效果

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/259524.html
標籤:其他
上一篇:子類為什么先死后生
下一篇:什么是CDN?
