參考: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服務器再響應給瀏覽中器)
這里與<使用OAuth2的SSO分析>主要不同的是,accessToken是jwt,經過解碼,轉化就可成為認證授權信息,無需再向授權服務器協助獲得認證授權信息,關於jwt可參看前面提供的鏈接.本文還修改了自定義登錄頁和授權頁,這種方案開始接近於生產了.
一.先創建OAuth2授權服務器
1.因為使用了自定義頁面,添加了wro4j-maven-plugin插件和以下依賴到pom.xml
<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一樣是放在主類上.
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替換上步獲取的授權許可碼.返回結果類似如下:
{
"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,繼續:
[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.
最后運行主類的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
一.背景
微服務架構下,我們的系統根據業務被拆分成了多個職責單一的微服務。
每個服務都有自己的一套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:
http://www.spring4all.com/article/356
