參考: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
微服務架構下,咱們的系統根據業務被拆分紅了多個職責單一的微服務。
每一個服務都有本身的一套API提供給別的服務調用,那麼如何保證安全性呢?
不是說你想調用就能夠調用,必定要有認證機制,是咱們內部服務發出的請求,才能夠調用咱們的接口。
須要注意的是咱們這邊講的是微服務之間調用的安全認證,不是統一的在API官網認證,需求不同,API網關處的統一認證是和業務掛鉤的,咱們這邊是爲了防止接口被別人隨便調用。
Spring Cloud可使用OAUTH2來實現多個微服務的統一認證受權
經過向OAUTH2服務進行集中認證和受權,得到access_token
而這個token是受其餘微服務信任的,在後續的訪問中都把access_token帶過去,從而實現了微服務的統一認證受權。
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