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

歷史文章

Spring Security OAuth2.0認證授權一:框架搭建和認證測試
Spring Security OAuth2.0認證授權二:搭建資源服務
Spring Security OAuth2.0認證授權三:使用JWT令牌
Spring Security OAuth2.0認證授權四:分佈式系統認證授權
Spring Security OAuth2.0認證授權五:用戶信息擴展到jwt

本篇文章將會解決上一篇文章《Spring Security OAuth2.0認證授權五:用戶信息擴展到jwt 》中遺留的問題,並在原有的項目中新增模塊business-server用來充當前端頁面的web容器並轉發登錄請求和更換token的請求等,以模擬前後端分離下的登錄以及更換token操作。

一、jwt令牌在網關處的過期時間校驗

上一篇文章中講了在網關處解析token並轉發到目標服務的操作,因爲使用了jwt令牌的原因,所以省了一步到認證服務器認證的操作,只要驗籤成功,就認爲令牌有效。這實際上留下了一個bug:服務端無法主動取消jwt令牌,所以這個令牌只要客戶端保存下來,如果不調用認證服務器的令牌驗證接口,這個jwt令牌將永遠有效。因此需要在網關處加上對過期時間的校驗。

在TokenFilter中添加以下代碼邏輯

//取出exp字段,判斷token是否已經過期
try {
    Map<String, Object> map = objectMapper.readValue(payLoad, new TypeReference<Map<String, Object>>() {
    });
    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有關係,關於這個類的實例,當初創建的方法如下

@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

最終發現是如下代碼的問題org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter#extractAuthentication

public Authentication extractAuthentication(Map<String, ?> map) {
    if (map.containsKey(USERNAME)) {
        Object principal = map.get(USERNAME);
        Collection<? extends GrantedAuthority> 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創建的時候省略參數導致的,只需要如此做就可以解決問題了

@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_idclient_secret,這兩個參數無論如何也不應當泄露給前端,通常都是中間的真正的客戶端服務拼接這兩個參數再將請求轉發給認證服務

四、前後端分離

設計上想要實現以下功能

  1. 首頁未登錄則提示用戶登錄,已經登錄則展示用戶個人信息
  2. 用戶登錄之後將令牌保存到localStorage
  3. token過期之後用戶可以選擇使用refresh_token更換已經過期的令牌(access_token)
  4. 已經過期的refresh_token不能用於更換新的令牌

1、關閉認證服務表單登錄

以前請求認證服務的任意接口,如果沒有認證,則都會跳轉到系統自帶的登錄頁面,現在我們想要實現前後端分離了,原來系統自帶的登錄頁面就有些礙眼了,直接關閉就好。關閉方法如下,spring security的配置更改爲如下:

                .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-servergateway-serverauth-serverresource-serverbusiness-server 五個服務

啓動成功後打開瀏覽器,輸入http://127.0.0.1:30002/地址,就會看到以下頁面
2021-01-29_171442.jpg
點擊登錄之後,出現登錄框
2021-01-29_171532.jpg
輸入賬號密碼之後,登錄成功之後會跳轉首頁,就會看到個人信息
2021-01-29_171754.jpg
這裏設置的token有效期爲10秒,所以很快token就會失效,十秒鐘之後刷新頁面就會有新的提示
2021-01-29_171858.jpg
接下來可以有兩種選擇,一種是使用refresh-token更新失效的令牌,另外一種是重新登錄,這裏refresh_token的有效期也很短,只有30秒,如果超出30秒,則會更新失敗,提示如下
2021-01-29_172049.jpg
而如果在30秒內刷新令牌,則會重新獲取到令牌並刷新當前頁

六、源代碼地址

源代碼地址:https://gitee.com/kdyzm/spring-security-oauth-study/tree/v7.0.0

我的博客地址:https://blog.kdyzm.cn/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章