使用OAuth2的SSO分析


參考:https://github.com/spring-guides/tut-spring-security-and-angular-js/blob/master/oauth2-vanilla/README.adoc 
原理圖
1.瀏覽器向UI服務器點擊觸發要求安全認證 
2.跳轉到授權服務器獲取授權許可碼 
3.從授權服務器帶授權許可碼跳回來 
4.UI服務器向授權服務器獲取AccessToken 
5.返回AccessToken到UI服務器 
6.發出/resource請求到UI服務器 
7.UI服務器將/resource請求轉發到Resource服務器 
8.Resource服務器要求安全驗證,於是直接從授權服務器獲取認證授權信息進行判斷后(最后會響應給UI服務器,UI服務器再響應給瀏覽中器)

一.先創建OAuth2授權服務器 
1.使用spring Initializrt生成初始項目,選使用spring boot 1.3.3生成maven項目,根據需要填寫group,artifact,依賴選Web和Security兩塊,點生成按鈕即可. 
2.加入OAuth2依賴到pom.xml

<dependency>
   <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>

修改主類(這里同時也作為資源服務器)

@SpringBootApplication
@RestController
@EnableAuthorizationServer
@EnableResourceServer
public class AuthserverApplication {

    @RequestMapping("/user")
    public Principal user(Principal user) {
        return user;
    }

    public static void main(String[] args) {
        SpringApplication.run(AuthserverApplication.class, args);
    }

}

同時修改servlet容器的port,contextPath,注冊一個測試用戶與客戶端,加入配置:application.properties

server.port: 9999
server.contextPath: /uaa
security.user.password: password
security.sessions: if-required
security.oauth2.client.clientId: acme
security.oauth2.client.clientSecret: acmesecret
security.oauth2.client.authorized-grant-types: authorization_code,refresh_token,password
security.oauth2.client.scope: openid

基於spring boot的security的session創建策略默認是STATELESS,至於幾個選項意義,可看

org.springframework.security.config.http.SessionCreationPolicy

啟動授權服務器后,可測試了: 

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": "8eded27d-b849-4473-8b2d-49ae49e17943",
  "token_type": "bearer",
  "refresh_token": "5e9af75c-c442-433f-81ba-996eb2c00f53",
  "expires_in": 43199,
  "scope": "openid"
}

 

從返回結果復制access_token,繼續: 

[root@dev ~]# TOKEN=8eded27d-b849-4473-8b2d-49ae49e17943 
[root@dev ~]# curl -H “Authorization: Bearer $TOKEN” 192.168.1.115:9999/uaa/user 

其中上面的8eded27d-b849-4473-8b2d-49ae49e17943是access_token,根據實際情況替換,第二個命令返回結果類似如下: 

{
  "details": {
    "remoteAddress": "192.168.1.194",
    "sessionId": null,
    "tokenValue": "8eded27d-b849-4473-8b2d-49ae49e17943",
    "tokenType": "Bearer",
    "decodedDetails": null
  },
  "authorities": [
    {
      "authority": "ROLE_USER"
    }
  ],
  "authenticated": true,
  "userAuthentication": {
    "details": {
      "remoteAddress": "0:0:0:0:0:0:0:1",
      "sessionId": "3943F6861E0FE31C29568542730342F6"
    },
    "authorities": [
      {
        "authority": "ROLE_USER"
      }
    ],
    "authenticated": true,
    "principal": {
      "password": null,
      "username": "user",
      "authorities": [
        {
          "authority": "ROLE_USER"
        }
      ],
      "accountNonExpired": true,
      "accountNonLocked": true,
      "credentialsNonExpired": true,
      "enabled": true
    },
    "credentials": null,
    "name": "user"
  },
  "oauth2Request": {
    "clientId": "acme",
    "scope": [
      "openid"
    ],
    "requestParameters": {
      "response_type": "code",
      "redirect_uri": "http://example.com",
      "code": "QzbdLe",
      "grant_type": "authorization_code",
      "client_id": "acme"
    },
    "resourceIds": [],
    "authorities": [
      {
        "authority": "ROLE_USER"
      }
    ],
    "approved": true,
    "refresh": false,
    "redirectUri": "http://example.com",
    "responseTypes": [
      "code"
    ],
    "extensions": {},
    "grantType": "authorization_code",
    "refreshTokenRequest": null
  },
  "credentials": "",
  "principal": {
    "password": null,
    "username": "user",
    "authorities": [
      {
        "authority": "ROLE_USER"
      }
    ],
    "accountNonExpired": true,
    "accountNonLocked": true,
    "credentialsNonExpired": true,
    "enabled": true
  },
  "clientOnly": false,
  "name": "user"
}

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

二.再看分離的資源服務器(改動也不少) 

不再使用Spring Session從Redis抽取認證授權信息,而是使用ResourceServerTokenServices向授權服務器發送請求獲取認證授權信息.
因些沒用到Spring Session時可移除,同時application.properties
配置
security.oauth2.resource.userInfoUri

security.oauth2.resource.tokenInfoUri
中的一個,
主類修改如下:

@SpringBootApplication
@RestController
@EnableResourceServer
public class ResourceApplication {
    @RequestMapping("/")
    public Message home() {
        return new Message("Hello World");
    }
    public static void main(String[] args) {
        SpringApplication.run(ResourceApplication.class, args);
    }
}

最后運行主類的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"
} 

可借鑒的經驗,我在windows上開發,啟動資源服務器,然后資源服務器有配置

server.address: 127.0.0.1

,這里限制容器只能是本機訪問,
如果使用局域網IP是不可以訪問的,比如你在別人的機器或在一台虛擬的linux上使用curl都是不是訪問的,注釋這行配置,這限制就解除. 

跟蹤下獲取認證授權的信息過程: 
1.userInfoRestTemplate Bean的聲明在

org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerTokenServicesConfiguration.
UserInfoRestTemplateConfiguration#userInfoRestTemplate 

2.使用前面配置的userInfoUri和上面的userInfoRestTemplate Bean在org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerTokenServicesConfiguration.
RemoteTokenServicesConfiguration.
UserInfoTokenServicesConfiguration#userInfoTokenServices
創建UserInfoTokenServices Bean. 

3.在org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer#configure添加了org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter 

4.當使用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就是上面的UserInfoTokenServices Bean,就在這里向授權服務器發出請求. 


三.UI服務器作為SSO的客戶端. 

1.同樣UI服務器不需要Spring Session,認證如我們所期望的,交給授權服務器,所以使用Spring Security OAuth2依賴替換Spring Session和Redis依賴
2.當然UI服務器還是API網關的角色,所以不要移除@EnableZuulProxy
在UI服務器主類加上@EnableOAuth2Sso,這個注解會幫我們完成跳轉到授權服務器,當然要些配置application.yml

zuul:
  routes:
    resource:
      path: /resource/**
      url: http://localhost:9000
    user:
      path: /user/**
      url: http://localhost:9999/uaa/user

這里將”/user”請求代理到授權服務器 

3.繼續修改UI主類繼承WebSecurityConfigurerAdapter,重寫org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
#configure(org.springframework.security.config.annotation.web.builders.HttpSecurity) 
目的是為了修改@EnableOAuth2Sso引起的默認Filter鏈,默認是org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2SsoDefaultConfiguration
#configure,
這個類上面有@Conditional(NeedsWebSecurityCondition.class)意思應該是,沒有WebSecurityConfigurerAdapter才會去執行這個config,
因為繼承了這個類,所以此config不再執行. 

4.作為oauth2的客戶端,application.yml下面這幾項是少不了的

security:
  oauth2:
    client:
      accessTokenUri: http://localhost:9999/uaa/oauth/token
      userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize
      clientId: acme
      clientSecret: acmesecret
    resource:
      userInfoUri: http://localhost:9999/uaa/user

最后一項,因為也作為資源服務器,所以也加上吧

spring:
  aop:
    proxy-target-class: true

spring aop默認一般都是使用jdk生成代理,前提是要有接口,cglib生成代理,目標類不能是final類,這是最基本的條件.
估計是那些restTemplate沒有實現接口,所以不得不在這里使用cglib生成代理. 

5.其它的前端微小改變,這里不贅述.把授權服務器,分離的資源服務器和這個UI服務器都啟動.准備測試:http://localhost:8080/login 
a.經過security的攔截鏈接中的
org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter.doFilter攔截,
觸發了attemptAuthentication方法

    public OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException {
        OAuth2AccessToken accessToken = context.getAccessToken();
        if (accessToken == null || accessToken.isExpired()) {
            try {
                accessToken = acquireAccessToken(context);
            } catch (UserRedirectRequiredException e) {
                context.setAccessToken(null); // No point hanging onto it now
                accessToken = null;
                String stateKey = e.getStateKey();
                if (stateKey != null) {
                    Object stateToPreserve = e.getStateToPreserve();
                    if (stateToPreserve == null) {
                        stateToPreserve = "NONE";
                    }
                    context.setPreservedState(stateKey, stateToPreserve);
                }
                throw e;
            }
        }
        return accessToken;
    }

acquireAccessToken(context)去獲取token的時候觸發拋異常.
在org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider
#getRedirectForAuthorization處理發送的url,
最后這個UserRedirectRequiredException往上拋,
一直往上拋到org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter#doFilter

    catch (Exception ex) {
        // Try to extract a SpringSecurityException from the stacktrace
        Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
        UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer
                .getFirstThrowableOfType(
                        UserRedirectRequiredException.class, causeChain);
        if (redirect != null) {
            redirectUser(redirect, request, response);
        } else {
            if (ex instanceof ServletException) {
                throw (ServletException) ex;
            }
            if (ex instanceof RuntimeException) {
                throw (RuntimeException) ex;
            }
            throw new NestedServletException("Unhandled exception", ex);
        }
    }

終於看到redirectUser(redirect, request, response);進行跳轉到授權服務器去了.

授權服務器跳回到UI服務器原來的地址(帶回來授權許可碼),再次被OAuth2ClientAuthenticationProcessingFilter攔截發送獲取accessToken,
經org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport
#retrieveToken提交POST請求,獲取到返回原來發請求處得到OAuth2AccessToken對象. 

在org.springframework.security.oauth2.client.OAuth2RestTemplate#acquireAccessToken使用oauth2Context.setAccessToken(accessToken);
對token進行保存.有了accessToken,就可以從授權服務器獲取用戶信息了.

最后,當用戶點logout的時候,授權服務器根本沒有退出(銷毀認證授權信息)

http://blog.csdn.net/xiejx618/article/details/51039653

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM