好好學習,天天向上
本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star,更多文章請前往:目錄導航
- 暢購商城(一):環境搭建
- 暢購商城(二):分散式檔案系統FastDFS
- 暢購商城(三):商品管理
- 暢購商城(四):Lua、OpenResty、Canal實現廣告快取與同步
- 暢購商城(五):Elasticsearch實現商品搜尋
- 暢購商城(六):商品搜尋
- 暢購商城(七):Thymeleaf實現靜態頁
- 暢購商城(八):微服務閘道器和JWT令牌
- 暢購商城(九):Spring Security Oauth2
前言
之前因為沒學過Spring Security和OAuth2.0,所以看這一章的視訊的時候看的一頭霧水。所以花了幾天時間惡補了一下這方面的知識,並且寫了兩篇文章,把這兩部分內容詳細說明了一下。
下面的內容只是針對於這個專案的,前兩篇文章中說過的內容就不再說了。
認證服務介紹
怎麼搭建OAuth2.0我之前的文章已經詳細說過了,這裡直接將資料裡提供的程式碼匯入即可。
這裡簡單總結一下每個檔案的作用:
-
AuthorizationServerConfig
這個是OAuth2.0的認證服務配置。主要有三點,第一是客戶端資訊配置,也就是客戶端需要有哪些條件才可以訪問伺服器,比如客戶端id和客戶端金鑰等,可以直接配置到記憶體中,也可以配置從資料庫中讀取;第二是授權伺服器端點配置,就是配置認證管理器,令牌儲存方式等;第三個是授權伺服器的安全配置,就是配置訪問的限制,比如限制校驗令牌的配置等。
-
CustomUserAuthenticationConverter
自定義的UserAuthenticationConverter,繼承自DefaultUserAuthenticationConverter,重寫了convertUserAuthentication方法。預設該方法是獲取authentication中的username和許可權資訊。而我們重寫的方法裡面還獲取了authentication中的principal,判斷是不是我們自定義的UserJwt,不是的話就呼叫userDetailsService.loadUserByUsername去獲取,然後將userJwt中的name和id獲取出來,新增到返回的map中。
-
UserDetailsServiceImpl
這個是自定義的認證授權類,實現了UserDetailsService介面,並實現了裡面的loadUserByUsername()方法。這個方法是根據前端傳進來的使用者名稱去查出對應的使用者資訊。然後交給後續的過濾器去進行使用者身份的驗證。一般這個方法是從資料庫中查詢使用者,但是這裡為了測試就直接new了一個臨時使用者,密碼是 "robod666" ,所以只要前端傳過來的密碼是 “robod666” 就可以正常登入。
-
WebSecurityConfig
這個是Spring Security的安全配置類。主要配置了某些對於某些請求的限制。在這個類中,還往Spring容器中注入了passwordEncoder和authenticationManagerBean供其他類使用。
-
UserLoginController
這個是自定義的一個只使用使用者名稱和密碼進行登入的簡化的登入方式。
-
LoginService
和LoginServiceImpl
UserLoginController的Service層。負責新增一些必要的資訊後然後通過RestTemplate模擬瀏覽器向伺服器傳送請求獲取令牌資訊。
-
AuthToken
封裝了Token的相關資訊。令牌資訊,重新整理token,jwt短令牌。
-
CookieUtil
Cookie的工具類。設定Cookie以及根據名稱獲取Cookie資訊。
-
UserJwt
使用者資訊。實現了UserDetails介面。驗證使用者時用的就是這個類的物件。
-
changgou.jks
金鑰證照,可以使用keytool工具生成。
-
application.yml
認證服務的配置檔案
………… # 配置資訊,給UserLoginController用的 auth: ttl: 3600 #token儲存到redis的過期時間 clientId: changgou clientSecret: changgou cookieDomain: localhost cookieMaxAge: -1 # 因為採用了非對稱加密,所以這裡配置了金鑰的相關資訊 encrypt: key-store: location: classpath:/robod666.jks secret: robod666 alias: robod666 password: robod666 …………
這幾個檔案的作用到這裡就介紹完了。
非對稱加密認證
認證流程分析
這個是傳統的認證流程,當我們攜帶令牌去訪問資源伺服器的時候,資源服務會將令牌傳送到授權服務中驗證令牌時候合法。這樣做的話無形之中增加的伺服器的壓力,因為多了一次伺服器之間互動的行為,效率低下。
為了提高效率,採用了公鑰私鑰驗證的方式。
授權服務採用私鑰去生成令牌,然後客戶端攜帶令牌向資源伺服器傳送請求。資源伺服器採用公鑰對令牌進行校驗,校驗通過後再進行下一步操作。減少了和授權服務的互動。
公鑰可以儲存在任意的伺服器中,但是私鑰只能儲存在授權服務中。因為有了私鑰後就可以去偽造令牌,降低了安全性。所以採用非對稱加密也是為了提高安全性。
生成金鑰證照
我們可以使用keytool工具來生成金鑰對。在準備好的資料夾下,開啟命令列視窗,執行以下內容:
keytool -genkeypair -alias robod666 -keyalg RSA -keypass robod666 -keystore robod666.jks -storepass robod666
# -alias:金鑰的別名
# -keyalg:使用的hash演算法
# -keypass:金鑰的訪問密碼
# -keystore:金鑰庫檔名,xc.keystore儲存了生成的證照
# -storepass:金鑰庫的訪問密碼
然後介面上會出現幾個問題,答案隨便輸,最後輸入 “y” 即可生成金鑰。
把生成的金鑰證照放在認證服務的resources目錄下即可。
提取公鑰
在安裝好openssl後,在金鑰證照所在的目錄下開啟命令列視窗,執行
keytool -list -rfc --keystore robod666.jks | openssl x509 -inform pem -pubkey
這樣就可以將公鑰提取出來。在需要使用到公鑰的微服務的resources目錄下建立一個public.key的檔案,把這段內容合併為一行貼上進去。
建立及解析令牌
public class JWTTest {
//建立令牌
@Test
public void createJWT() {
ClassPathResource classPathResource = new ClassPathResource("robod666.jks");
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(classPathResource, "robod666".toCharArray());
KeyPair keyPair = keyStoreKeyFactory.getKeyPair("robod666");
PrivateKey privateKey = keyPair.getPrivate();
Map<String, Object> tokenMap = new HashMap<>();
tokenMap.put("id", "1");
tokenMap.put("name", "robod");
tokenMap.put("roles", "ROLE_VIP,ROLE_USER");
Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner((RSAPrivateKey) privateKey));
System.out.println(jwt.getEncoded());
}
//解析令牌
@Test
public void parseJWT() {
String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJyb2JvZCIsImlkIjoiMSJ9.KkPXXlYTkDCWq2VN5qy6w6FI5TgCbIy-GkQShaYGbfvcZsj0165XsgXSx7Sf5aUpGB-494ds-ZnLxs3oVMZ7_tbu-is1-gZOeQ0G1GLla0ytNkImXabnujgWH2B4bmX4lBLK7d8xTEQ4WoAnydWUusCmPjQDgdFZGHmccJLuYKqQPzru-4go1mFgjEeB7gNu6cLYyQc79bZdF2Mk2OX1Nxpb88sux0QkNAlb1-JuUhmjbUYwMK5l5W5zeNckRJtGy_Zy7OTwXviuRp6uISmWD7p1HYbkKH-ROKCgSu1cqnok0645Uou7Y54Nd8NosIqShuNYbBBo_BHWuyx_lKdk1g";
String publicKey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiq6KfbXc/viuB6oQ/80cfLSFIr7pX3PmteAQ2/dA+ReMLgULJb+U8Dax3xNpBgLAp+Ei2IMkBFJlJRn/iaYi5eMnCY2vyfHkC69x6OhhCtzWBRxGJkPRjLDU+Obhak2MrDI4zIpzQs2/phjqWXuEPMz7KMd5UhoAFZWLTW1Ih3CP962fuJdV83hj/2uWN/yaAgaLRxRlTw7HHoIEy1dX9prAnqQ/rOl2Igvwi23GNnzMrqlvR9qt1gBI+noHtMv07hkavUT1nmoYnt/pw2+FLMLFEun2gR3DUmqu79QC6trDf3cVfKyRP9A7TBjUEv+Ecrh8JQosQa8GongTzHhmOwIDAQAB-----END PUBLIC KEY-----";
Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publicKey));
String claims = jwt.getClaims();
System.out.println(claims);
}
}
建立令牌的時候,可能會出現以下 java.lang.IllegalStateException 錯誤,
把idea重啟一下即可。
執行createJWT()成功的話,就會出現以下內容。
這就是我們建立的令牌了。
執行parseJWT(),執行成功的話,就可以使用公鑰去解析出令牌中的內容。
賬號密碼登入
現在我們登入的時候,除了需要使用者名稱密碼之外,還需要指定clientId等其它資訊。那我們可不可以只使用賬號密碼就能登入呢?只需要在伺服器中將其它的資訊事先指定好即可。思路就是,前端將使用者名稱密碼傳入進來,然後伺服器端新增好其它的資訊,傳送給認證服務去登入,然後拿到token再返回給前端。
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@Override
public Map login(String username, String password, String clientId, String clientSecret, String grantType) {
String url = loadBalancerClient.choose("user-auth").getUri() +
"/oauth/token";
//請求體
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("username", username);
body.add("password", password);
body.add("grant_type", grantType);
//請求頭
MultiValueMap<String, String> header = new LinkedMultiValueMap<>();
String authorization = "Basic " +
new String(Base64.getEncoder().encode((clientId + ":" + clientSecret).getBytes()));
header.add("Authorization", authorization);
HttpEntity httpEntity = new HttpEntity(body, header);
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, httpEntity, Map.class);
return response.getBody();
}
}
@Value("${auth.clientId}")
private String clientId;
@Value("${auth.clientSecret}")
private String clientSecret;
//密碼模式 認證.
@RequestMapping("/login")
public Result<Map> login(String username, String password) throws Exception {
String grantType = "password";
Map token = loginService.login(username, password, clientId, clientSecret, grantType);
return new Result<>(true, StatusCode.OK,"令牌生成成功",token);
}
好了,現在我們只用賬號密碼就可以申請到令牌了。
總結
這篇文章主要是說了如何匯入OAuth2.0認證服務,如果不瞭解的話,理解起來還是很困難的,所以可以先去看看我之前的文章,匯入之後對每個檔案的作用進行了一個簡單的介紹。然後又講了一下采用非對稱加密的時候如何生成公鑰私鑰。最後說了如何實現只用賬號密碼進行登入。
如果我的文章對你有些幫助,不要忘了點贊,收藏,轉發,關注。要是有什麼好的意見歡迎在下方留言。讓我們下期再見!