1. 程式人生 > >Spring Security OAuth2.0認證授權六:前後端分離下的登入授權

Spring Security OAuth2.0認證授權六:前後端分離下的登入授權

歷史文章 > [Spring Security OAuth2.0認證授權一:框架搭建和認證測試](https://blog.kdyzm.cn/post/24) > [Spring Security OAuth2.0認證授權二:搭建資源服務](https://blog.kdyzm.cn/post/25) > [Spring Security OAuth2.0認證授權三:使用JWT令牌](https://blog.kdyzm.cn/post/26) > [Spring Security OAuth2.0認證授權四:分散式系統認證授權](https://blog.kdyzm.cn/post/30) > [Spring Security OAuth2.0認證授權五:使用者資訊擴充套件到jwt](https://blog.kdyzm.cn/post/31) > 本篇文章將會解決上一篇文章《[Spring Security OAuth2.0認證授權五:使用者資訊擴充套件到jwt](https://blog.kdyzm.cn/post/31) 》中遺留的問題,並在原有的專案中新增模組`business-server`用來充當前端頁面的web容器並轉發登入請求和更換token的請求等,以模擬前後端分離下的登入以及更換token操作。 ## 一、jwt令牌在閘道器處的過期時間校驗 上一篇文章中講了在閘道器處解析token並轉發到目標服務的操作,因為使用了jwt令牌的原因,所以省了一步到認證伺服器認證的操作,只要驗籤成功,就認為令牌有效。這實際上留下了一個bug:服務端無法主動取消jwt令牌,所以這個令牌只要客戶端儲存下來,如果不呼叫認證伺服器的令牌驗證介面,這個jwt令牌將永遠有效。因此需要在閘道器處加上對過期時間的校驗。 在TokenFilter中新增以下程式碼邏輯 ``` java //取出exp欄位,判斷token是否已經過期 try { Map map = objectMapper.readValue(payLoad, new TypeReference>() { }); long expiration = ((Integer) map.get("exp")) * 1000L; if (expiration < new Date().getTime()) { return unAuthorized(exchange, "未認證的請求:token存在,但是已經失效",WrapperResult.TOKEN_EXPIRE); } } catch (IOException e) { log.error("", e); return unAuthorized(exchange, "未認證的請求:錯誤的token",null); } ``` ## 二、refresh-token介面缺少使用者資訊 refresh-token在access_token過期,但是refresh-token未過期的時候使用,目的是使用refresh_token更新已經過期的access_token,這樣理論上來說,客戶端只要能在refresh_token過期之前進行任意操作,就可以避免重新登入了。 上一篇文章中將使用者資訊放到了jwt token中並返回給客戶端,但是如果使用refresh_token更新token,後端會報錯,前端取到的token中則缺少了使用者資訊。究其原因,和`JwtAccessTokenConverter`有關係,關於這個類的例項,當初建立的方法如下 ``` java @Bean public JwtAccessTokenConverter accessTokenConverter(){ JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);//對稱祕鑰,資源伺服器使用該祕鑰來驗證 return jwtAccessTokenConverter; } ``` 這裡的`new`操作省了很多預設引數的指定,且先看下為啥會缺少使用者資訊,擴充套件使用者資訊的關鍵在於方法`com.kdyzm.spring.security.auth.center.service.MyUserDetailsServiceImpl#loadUserByUsername`,這裡擴充套件了使用者資訊,使其從單純的username字串變成了`UserDetailsExpand`物件,然後在增強方法`com.kdyzm.spring.security.auth.center.enhancer.CustomTokenEnhancer#enhance`中將擴充套件資訊取出來放到Token中。 經過debug,發現 ![2021-01-29_162556.jpg](https://img2020.cnblogs.com/blog/516671/202101/516671-20210129174601097-1511538924.jpeg;%20charset=UTF-8) 最終發現是如下程式碼的問題`org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter#extractAuthentication` ``` java public Authentication extractAuthentication(Map map) { if (map.containsKey(USERNAME)) { Object principal = map.get(USERNAME); Collection authorities = getAuthorities(map); //執行到這裡的時候userDetailsService為空,所以並沒有執行自定義的loadUserByUsername方法 if (userDetailsService != null) { UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME)); authorities = user.getAuthorities(); principal = user; } return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities); } return null; } ``` 層層網上追尋呼叫鏈,竟然是`JwtAccessTokenConverter`建立的時候省略引數導致的,只需要如此做就可以解決問題了 ``` java @Bean public JwtAccessTokenConverter accessTokenConverter(){ JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); DefaultAccessTokenConverter tokenConverter = new DefaultAccessTokenConverter(); DefaultUserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter(); userTokenConverter.setUserDetailsService(userDetailsService); tokenConverter.setUserTokenConverter(userTokenConverter); jwtAccessTokenConverter.setAccessTokenConverter(tokenConverter); jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);//對稱祕鑰,資源伺服器使用該祕鑰來驗證 return jwtAccessTokenConverter; } ``` JwtAccessTokenConverter物件建立的時候指定DefaultUserAuthenticationConverter使用的userDetailsService即可。 ## 三、新建business-server模組作為web容器 這裡新建的business-server模組有兩個功能 1. 充當web容器,該服務並沒有使用模板化技術,使用的是純html、css實現前端 2. 轉發前端登入、更換token請求 可能會有人對第二條有疑問,為什麼要這麼做?之前測試的時候基本上都是使用postman發起的請求,請求的方式是這樣的`http://127.0.0.1:30000/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123`可以看到這裡傳遞了很重要的引數`client_id`和`client_secret`,這兩個引數無論如何也不應當洩露給前端,通常都是中間的真正的客戶端服務拼接這兩個引數再將請求轉發給認證服務 ## 四、前後端分離 設計上想要實現以下功能 1. 首頁未登入則提示使用者登入,已經登入則展示使用者個人資訊 2. 使用者登入之後將令牌儲存到localStorage 3. token過期之後使用者可以選擇使用refresh_token更換已經過期的令牌(access_token) 4. 已經過期的refresh_token不能用於更換新的令牌 ### 1、關閉認證服務表單登入 以前請求認證服務的任意介面,如果沒有認證,則都會跳轉到系統自帶的登入頁面,現在我們想要實現前後端分離了,原來系統自帶的登入頁面就有些礙眼了,直接關閉就好。關閉方法如下,spring security的配置更改為如下: ``` java .formLogin() .disable(); ``` ### 2、前後端程式碼 前端程式碼在`business-server/src/main/resources/static`目錄下,只有兩個頁面,一個首頁,一個登陸頁面 後端只有兩個介面 - 登入介面:com.kdyzm.spring.security.oauth.study.business.server.controller.LoginController#login - 更新token介面:com.kdyzm.spring.security.oauth.study.business.server.controller.TokenController#refreshToken 其它不做贅述,不過前端頁面寫起來挺麻煩的。。難是不難的 ## 五、測試 原始碼: 測試前首先需要重新執行初始化sql(auth-server/docs/sql/init.sql),然後依次啟動` register-server`、`gateway-server`、`auth-server`、`resource-server`、`business-server` 五個服務 啟動成功後開啟瀏覽器,輸入`http://127.0.0.1:30002/`地址,就會看到以下頁面 ![2021-01-29_171442.jpg](https://img2020.cnblogs.com/news/516671/202101/516671-20210129174601977-538921902.jpeg;%20charset=UTF-8) 點選登入之後,出現登入框 ![2021-01-29_171532.jpg](https://img2020.cnblogs.com/blog/516671/202101/516671-20210129174602236-2139991608.jpeg;%20charset=UTF-8) 輸入賬號密碼之後,登入成功之後會跳轉首頁,就會看到個人資訊 ![2021-01-29_171754.jpg](https://img2020.cnblogs.com/blog/516671/202101/516671-20210129174602421-491103744.jpeg;%20charset=UTF-8) 這裡設定的token有效期為10秒,所以很快token就會失效,十秒鐘之後重新整理頁面就會有新的提示 ![2021-01-29_171858.jpg](https://img2020.cnblogs.com/news/516671/202101/516671-20210129174602662-99006757.jpeg;%20charset=UTF-8) 接下來可以有兩種選擇,一種是使用refresh-token更新失效的令牌,另外一種是重新登入,這裡refresh_token的有效期也很短,只有30秒,如果超出30秒,則會更新失敗,提示如下 ![2021-01-29_172049.jpg](https://img2020.cnblogs.com/blog/516671/202101/516671-20210129174602802-136932163.jpeg;%20charset=UTF-8) 而如果在30秒內重新整理令牌,則會重新獲取到令牌並重新整理當前頁 ## 六、原始碼地址 原始碼地址:[https://gitee.com/kdyzm/spring-security-oauth-study/tree/v7.0.0](https://gitee.com/kdyzm/spring-security-oauth-study/tree/v7.0.0) 我的部落格地址:https://blog.k