使用JWT的OAuth2的SSO分析

參考:https://github.com/spring-guides/tut-spring-security-and-angular-js/blob/master/oauth2/README.adoc 
http://jwt.io/introduction/ 
本文在<使用OAuth2的SSO分析>文章的基礎上擴展,使用jwt可減小了向認證服務器的請求,但jwt比swt(Simple Web Tokens)要長很多,還要依賴公鑰解密. 
這裏寫圖片描述
1.瀏覽器向UI服務器點擊觸發要求安全認證 
2.跳轉到受權服務器獲取受權許可碼 
3.從受權服務器帶受權許可碼跳回來 
4.UI服務器向受權服務器獲取AccessToken 
5.返回AccessToken到UI服務器 
6.發出/resource/請求到UI服務器 
7.UI服務器將/resource/請求轉發到Resource服務器 
Resource服務器從請求取出accessToken,解碼,直接轉化爲認證受權信息進行判斷後(最後會響應給UI服務器,UI服務器再響應給瀏覽中器)java

這裏與<使用OAuth2的SSO分析>主要不一樣的是,accessToken是jwt,通過解碼,轉化就可成爲認證受權信息,無需再向受權服務器協助得到認證受權信息,關於jwt可參看前面提供的連接.本文還修改了自定義登陸頁和受權頁,這種方案開始接近於生產了.git

一.先建立OAuth2受權服務器 
1.由於使用了自定義頁面,添加了wro4j-maven-plugin插件和如下依賴到pom.xmlgithub

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2.主類修改比較大,主類繼承WebMvcConfigurerAdapter主要是註冊視圖控制器;
繼承WebSecurityConfigurerAdapter的內部類主要修改自定義權限控制;
關鍵是繼承AuthorizationServerConfigurerAdapter的受權服務器配置,裏面配置了JwtAccessTokenConverter(密鑰就在這裏使用),並使用這個Bean;
@EnableResourceServer同樣是放在主類上. web

3.application配置將oauth的配置移到了OAuth2AuthorizationConfig內部類內部.增長了一個密鑰庫文件和兩個freemarker頁面 
啓動受權服務器後,可測試了: 
a.打開瀏覽器輸入地址http://localhost:9999/uaa/oauth/authorize?response_type=code&client_id=acme&redirect_uri=http://example.com發出請求,而後根據以上配置,輸入用戶名/密碼,點贊成,獲取返回的受權許可碼 
b.在Linux的bash或mac的terminal輸入 算法

[root@dev ~]#curl acme:acmesecret@192.168.1.115:9999/uaa/oauth/token \ 
-d grant_type=authorization_code -d client_id=acme \ 
-d redirect_uri=http://example.com -d code=fjRdsL 


回車獲取access token,其中fjRdsL替換上步獲取的受權許可碼.返回結果相似以下: spring

{
  "access_token": "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0NTk1NTUxNTYsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIiwiUk9MRV9VU0VSIl0sImp0aSI6IjI5MjcyYWJiLTQ4MjUtNGYwMS1hZjllLTg5ZGE1ZDE1MDBiNyIsImNsaWVudF9pZCI6ImFjbWUiLCJzY29wZSI6WyJvcGVuaWQiXX0.cQd88GYItHUDJuwkd_Rd0Yo8QM1R0dccuK0-xZ4OynC7EnqClLunaNOZ9jXwtilIFJNxbkbhQ8ymXdvlAF5Zjo8lpRGotdVo9rgQc39BDse7hGy1EfA9ZADQmJ-EuwkTNo0IBEXYC33XxQNK_3I_E92cnIPXq-FZHuZMRzpr-SlriwLa3aZVidmeyXK2U5dsjViWoHHKhcg-9c-VBPtyTJfPZOvj3s7DrbfCgOAGOhHkd_MBCdLDFb7QFhzIRsMfcD9rOAGTqk-hU2pHkkakKQ7_vL604UU7Qh3Zzkn6VbHPy0HAAiB9cnUhkQxK3Qb-wbHG-l3FC2pDlhtlhMHNfg",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsib3BlbmlkIl0sImF0aSI6IjI5MjcyYWJiLTQ4MjUtNGYwMS1hZjllLTg5ZGE1ZDE1MDBiNyIsImV4cCI6MTQ2MjEwMzk1NiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJqdGkiOiIzNWM5OWY0Yy0xMGM0LTQ5ZTAtODAwYi1lZTc5ZTQ3ODNkNmUiLCJjbGllbnRfaWQiOiJhY21lIn0.bUvJ9HmrFU92euLzd5eesJKFlav5v1WyfBEgd3pO6I2D2yYy98oPwfNwCrbP44M2ilO48LJEovLLoZFYvjfA8xe6XO1Fx55Tik5SrWfizAEsNFsFg25zE92T3YNocStxuJWFSVBLlwjtxpVmnHOgPefku2G6N5seziX0SOBJleHSUObNAYtiBVQjKWXA3jGnMoZSP0dMbgtrWinwRJLwvaMgMDNnxYFSdvSW99XKjCyQNVmbGa4aRyy-xblTr7qlSqdcZIdRBfKkHM5S9jaenNVc85vGAYQFPrdkRWhk4v-8nlHJiYdBa6ZspgbVWw_oPLgP8cbuzJev86q55p1gAw",
  "expires_in": 43199,
  "scope": "openid",
  "jti": "29272abb-4825-4f01-af9e-89da5d1500b7"
}

 


從返回結果複製access_token,繼續: json

[root@dev ~]# TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0NTk1NTUxNTYsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIiwiUk9MRV9VU0VSSI6IjI5MjcyYWJiLTQ4MjUtNGYwMS1hZjllLTg5ZGE1ZDE1MDBiNyIsImNsaWVudF9pZCI6ImFjbWUiLCJzY29wZSI6WyJvcGVuaWQiXX0.cQd88GYItHUDJuwkd_Rd0Yo8QM1R0dccuK0-xZ4OynC7EnqClLunaNOZ9jXwtilIFJNxbkbhQ8ymXdvlAF5Zjo8lpRGotdVo9rgQc39BDse7hGy1EfA9ZADQmJ-EuwkTNo0IBEXYC33XxQNK_3I_E92cnIPXq-FZHuZMRzpr-SlriwLa3aZVidmeyXK2U5dsjViWoHHKhcg-9c-VBPtyTJfPZOvj3s7DrbfCgOAGOhHkd_MBCdLDFb7QFhzIRsMfcD9rOAGTqk-hU2pHkkakKQ7_vL604UU7Qh3Zzkn6VbHPy0HAAiB9cnUhkQxK3Qb-wbHG-l3FC2pDlhtlhMHNfg 
[root@dev ~]# curl -H 「Authorization: Bearer $TOKEN」 192.168.1.115:9999/uaa/user 

第二個命令返回結果相似以下: 瀏覽器

{
  "details": {
    "remoteAddress": "192.168.1.194",
    "sessionId": null,
    "tokenValue": "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0NTk1NTUxNTYsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIiwiUk9MRV9VU0VSIl0sImp0aSI6IjI5MjcyYWJiLTQ4MjUtNGYwMS1hZjllLTg5ZGE1ZDE1MDBiNyIsImNsaWVudF9pZCI6ImFjbWUiLCJzY29wZSI6WyJvcGVuaWQiXX0.cQd88GYItHUDJuwkd_Rd0Yo8QM1R0dccuK0-xZ4OynC7EnqClLunaNOZ9jXwtilIFJNxbkbhQ8ymXdvlAF5Zjo8lpRGotdVo9rgQc39BDse7hGy1EfA9ZADQmJ-EuwkTNo0IBEXYC33XxQNK_3I_E92cnIPXq-FZHuZMRzpr-SlriwLa3aZVidmeyXK2U5dsjViWoHHKhcg-9c-VBPtyTJfPZOvj3s7DrbfCgOAGOhHkd_MBCdLDFb7QFhzIRsMfcD9rOAGTqk-hU2pHkkakKQ7_vL604UU7Qh3Zzkn6VbHPy0HAAiB9cnUhkQxK3Qb-wbHG-l3FC2pDlhtlhMHNfg",
    "tokenType": "Bearer",
    "decodedDetails": null
  },
  "authorities": [
    {
      "authority": "ROLE_ADMIN"
    },
    {
      "authority": "ROLE_USER"
    }
  ],
  "authenticated": true,
  "userAuthentication": {
    "details": null,
    "authorities": [
      {
        "authority": "ROLE_ADMIN"
      },
      {
        "authority": "ROLE_USER"
      }
    ],
    "authenticated": true,
    "principal": "user",
    "credentials": "N/A",
    "name": "user"
  },
  "credentials": "",
  "principal": "user",
  "oauth2Request": {
    "clientId": "acme",
    "scope": [
      "openid"
    ],
    "requestParameters": {
      "client_id": "acme"
    },
    "resourceIds": [],
    "authorities": [],
    "approved": true,
    "refresh": false,
    "redirectUri": null,
    "responseTypes": [],
    "extensions": {},
    "grantType": null,
    "refreshTokenRequest": null
  },
  "clientOnly": false,
  "name": "user"
}

從結果來看,使用access token訪問資源一切正常,說明受權服務器沒問題.安全

二.再看分離的資源服務器 
spring-security-jwt依賴也要加入pom.xml;
主類沒改動;
application配置文件使用security.oauth2.resource.jwt.keyValue替換security.oauth2.resource.userInfoUri選項,使用這個公鑰來解密jwt.bash

最後運行主類的main方法測試(受權服務器前面啓動了,access_token也獲得了),因而在使用curl命令: 

[root@dev ~]# curl -H 「Authorization: Bearer $TOKEN」 192.168.1.115:9000 

返回結果相似以下: 

{
  "id": "03af8be3-2fc3-4d75-acf7-c484d9cf32b1",
  "content": "Hello World"
} 

跟蹤下獲取認證受權的信息過程: 

當使用curl -H 「Authorization: Bearer $TOKEN」 192.168.1.115:9000發出請求時,直到被OAuth2AuthenticationProcessingFilter攔截器處理, 

org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter
#doFilter{ 
Authentication authentication = tokenExtractor.extract(request);//抽取Token 
Authentication authResult = authenticationManager.authenticate(authentication);//還原解碼認證受權信息 
} 
org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager
#authenticate{ 
OAuth2Authentication auth = tokenServices.loadAuthentication(token);//這裏的tokenServices是DefaultTokenServices 
} 
org.springframework.security.oauth2.provider.token.DefaultTokenServices
#loadAuthentication{ 
OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);//tokenStore是JwtTokenStore 
OAuth2Authentication result = tokenStore.readAuthentication(accessToken); 
} 
org.springframework.security.oauth2.provider.token.store.JwtTokenStore
#readAccessToken{ 
OAuth2AccessToken accessToken = convertAccessToken(tokenValue); 
} 
org.springframework.security.oauth2.provider.token.store.JwtTokenStore
#convertAccessToken{ 
return jwtTokenEnhancer.extractAccessToken(tokenValue, jwtTokenEnhancer.decode(tokenValue)); 
} 
org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter
#extractAccessToken 

通過上面這個過程,用到jwt的公鑰對jwt進行解碼,從中抽取OAuth2Authentication,這個Authentication自己就包含了用戶認證的信息.
無需再向受權服務器發請求解碼

三.UI服務器做爲SSO的客戶端. 
一樣UI服務器也要添加spring-security-jwt依賴到pom.xml;
主類也基本不改動;
和資源服務器同樣,使用security.oauth2.resource.jwt.keyValue替換security.oauth2.resource.userInfoUri選項.
其它的分析與<使用OAuth2的SSO分析>相似.
能夠三臺服務器都啓動測試了.
http://blog.csdn.net/xiejx618/article/details/51039683


 

Spring Cloud中如何保證各個微服務之間調用的安全性

一.背景

微服務架構下,咱們的系統根據業務被拆分紅了多個職責單一的微服務。

每一個服務都有本身的一套API提供給別的服務調用,那麼如何保證安全性呢?

不是說你想調用就能夠調用,必定要有認證機制,是咱們內部服務發出的請求,才能夠調用咱們的接口。

須要注意的是咱們這邊講的是微服務之間調用的安全認證,不是統一的在API官網認證,需求不同,API網關處的統一認證是和業務掛鉤的,咱們這邊是爲了防止接口被別人隨便調用。

二.方案

OAUTH2

Spring Cloud可使用OAUTH2來實現多個微服務的統一認證受權

經過向OAUTH2服務進行集中認證和受權,得到access_token

而這個token是受其餘微服務信任的,在後續的訪問中都把access_token帶過去,從而實現了微服務的統一認證受權。

JWT

JWT是一種安全標準。基本思路就是用戶提供用戶名和密碼給認證服務器,服務器驗證用戶提交信息信息的合法性;若是驗證成功,會產生並返回一個Token,用戶可使用這個token訪問服務器上受保護的資源。

感受這2種好像沒多大區別呀,實際上是有區別的:OAuth2是一種受權框架 ,JWT是一種認證協議

不管使用哪一種方式切記用HTTPS來保證數據的安全性。

三.用哪一種

我我的建議用JWT,輕量級,簡單,適合分佈式無狀態的應用

用OAUTH2的話就麻煩點,各類角色,認證類型,客戶端等等一大堆概念

四.怎麼用

首先呢建立一個通用的認證服務,提供認證操做,認證成功後返回一個token

@RestController
@RequestMapping(value="/oauth")
public class AuthController {

    @Autowired
    private AuthService authService;

    @PostMapping("/token")
    public ResponseData auth(@RequestBody AuthQuery query) throws Exception {
        if (StringUtils.isBlank(query.getAccessKey()) || StringUtils.isBlank(query.getSecretKey())) {
            return ResponseData.failByParam("accessKey and secretKey not null");
        }

        User user = authService.auth(query);
        if (user == null) {
            return ResponseData.failByParam("認證失敗");
        }

        JWTUtils jwt = JWTUtils.getInstance();
        return ResponseData.ok(jwt.getToken(user.getId().toString()));
    }

    @GetMapping("/token")
    public ResponseData oauth(AuthQuery query) throws Exception {
        if (StringUtils.isBlank(query.getAccessKey()) || StringUtils.isBlank(query.getSecretKey())) {
            return ResponseData.failByParam("accessKey and secretKey not null");
        }

        User user = authService.auth(query);
        if (user == null) {
            return ResponseData.failByParam("認證失敗");
        }

        JWTUtils jwt = JWTUtils.getInstance();
        return ResponseData.ok(jwt.getToken(user.getId().toString()));
    }

}

 

JWT能夠加入依賴,而後寫個工具類便可,建議寫在全局的包中,全部的服務都要用,具體代碼請參考:JWTUtils

GITHUB地址:https://github.com/jwtk/jjwt

JWT提供了不少加密的算法,我這邊用的是RSA,目前是用的一套公鑰以及私鑰,這種作法目前來講是很差的,由於萬一祕鑰泄露了,那就談不上安全了,因此後面會採用配置中心的方式來動態管理祕鑰。

類裏主要邏輯是生成token,而後提供一個檢查token是否合法的方法,以及是否過時等等判斷。

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>

 

 
 

統一認證的服務有了,咱們只須要將認證服務註冊到註冊中心便可給別的服務消費。

那麼咱們如何使用剛剛的認證服務來作認證呢,最簡單的辦法就是用Filter來處理

好比說我如今有一個服務fangjia-fsh-house-service,以前是隨便誰都能調用我提供的接口,如今我想加入驗證,只有驗證經過的纔可讓它調用個人接口

那就在fangjia-fsh-house-service中加一個過濾器來判斷是否有權限調用接口,咱們從請求頭中獲取認證的token信息,不須要依賴Cookie

這個過濾器我也建議寫在全局的項目中,由於也是全部服務都要用,代碼請參考:HttpBasicAuthorizeFilter

主要邏輯就是獲取token而後經過JWTUtils來驗證是否合法,不合法給提示,合法則放過

這邊須要注意的地方是解密的祕鑰必須跟加密時是相同的,否則解密必然失敗,就是bug了

//驗證TOKEN
if (!StringUtils.hasText(auth)) {
    PrintWriter print = httpResponse.getWriter();
    print.write(JsonUtils.toJson(ResponseData.fail("非法請求【缺乏Authorization信息】", 
                 ResponseCode.NO_AUTH_CODE.getCode())));
    return;
}
JWTUtils.JWTResult jwt = jwtUtils.checkToken(auth);
if (!jwt.isStatus()) {
    PrintWriter print = httpResponse.getWriter();
    print.write(JsonUtils.toJson(ResponseData.fail(jwt.getMsg(), jwt.getCode())));
    return;
}
chain.doFilter(httpRequest, response);

到這步爲止,只要調用方在認證經過以後,經過認證服務返回的token,而後塞到請求頭Authorization中,就能夠調用其餘須要認證的服務了。

這樣看起來貌似很完美,可是用起來不方便呀,每次調用前都須要去認證,而後塞請求頭,如何作到通用呢,不須要具體的開發人員去關心,對使用者透明,下篇文章,咱們繼續探討如何實現方便的調用。

具體代碼能夠參考個人github:

https://github.com/yinjihuan/spring-cloud

http://www.spring4all.com/article/356

相關文章
相關標籤/搜索