1. 程式人生 > >Spring Security Oauth2-授權碼模式(Finchley版本)

Spring Security Oauth2-授權碼模式(Finchley版本)

一、授權碼模式原理解析(來自理解OAuth 2.0)

授權碼模式(authorization code)是功能最完整、流程最嚴密的授權模式。它的特點就是通過客戶端的後臺伺服器,與"服務提供商"的認證伺服器進行互動。其具體的流程如下:
授權碼模式流程圖
具體步驟:

  • A:使用者訪問客戶端(client),客戶端告知瀏覽器(user-Agent)重定向到授權伺服器
  • B:呈現授權介面給使用者,使用者選擇是否給予客戶端授權
  • C:假設使用者給予授權,授權伺服器(Authorization Server)將使用者告知瀏覽器重定向(重定向地址為Redirection URI)到客戶端,同時附上授權碼(code)
  • D:客戶端收到授權碼,附上早先的重定向URL(Redirection URI),向授權伺服器申請令牌(access token),這一步在客戶端的後臺的伺服器上完成,對使用者不可見
  • E:授權伺服器核對授權碼(code)和重定向URI,確認無誤後,向客戶端發放(access token)和更新令牌(refresh token)

在步驟A中客戶端告知瀏覽器重定向到授權伺服器的URI包含以下引數:

  • response_type:表示授權型別,必選項,此處的值固定為"code"
  • client_id:表示客戶端的ID,必選項
  • client_secret:客戶端的密碼,可選
  • redirect_uri:表示重定向URI,可選項
  • scope:表示申請的許可權範圍,可選項
  • state:表示客戶端的當前狀態,可以指定任意值,認證伺服器會原封不動地返回這個值。
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com

在C步驟中授權伺服器迴應客戶端的URI,包含以下引數:

  • code:表示授權碼,該碼有效期應該很短,通常10分鐘,客戶端只能使用一次,否則會被授權伺服器拒絕,該碼與客戶端 ID 和 重定向 URI 是一一對應關係
  • state:如果客戶端請求中包含這個引數,授權伺服器的迴應也必須一模一樣包含這個引數
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
&state=xyz

在D步驟中客戶端向授權伺服器申請令牌的HTTP請求,包含以下引數:

  • grant_type:表示使用的授權模式,必選,此處固定值為“authorization_code”
  • code:表示上一步獲得的授權嗎,必選
  • redirect_uri:重定向URI,必選,與步驟 A 中保持一致
  • client_id:表示客戶端ID,必選
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&client_id=s6BhdRkqt3
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

在E步驟中授權伺服器傳送的HTTP回覆,包含以下引數:

  • access_token:表示訪問令牌,必選項。
  • token_type:表示令牌型別,該值大小寫不敏感,必選項,可以是bearer型別或mac型別。
  • expires_in:表示過期時間,單位為秒。如果省略該引數,必須其他方式設定過期時間。
  • refresh_token:表示更新令牌,用來獲取下一次的訪問令牌,可選項。
  • scope:表示許可權範圍,如果與客戶端申請的範圍一致,此項可省略。
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
    "access_token":"2YotnFZFEjr1zCsicMWpAA",
    "token_type":"example",
    "expires_in":3600,
    "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
    "example_parameter":"example_value"
}

更新令牌

如果使用者訪問的時候,客戶端的訪問令牌access_token已經過期,則需要使用更新令牌refresh_token申請一個新的訪問令牌。
客戶端發出更新令牌的HTTP請求,包含以下引數:

  • grant_type:表示使用的授權模式,此處的值固定為”refresh_token”,必選項。
  • refresh_token:表示早前收到的更新令牌,必選項。
  • scope:表示申請的授權範圍,不可以超出上一次申請的範圍,如果省略該引數,則表示與上一次一致。

二、授權碼示例

示例程式碼包含授權服務和資源服務

服務名 埠號 說明
auth-server 8080 授權伺服器
resource-server 8088 資源伺服器

2.1 授權伺服器

2.1.1 新增依賴

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-security</artifactId>
</dependency>

2.1.2 授權服務配置

/**
 * 授權伺服器配置
 *
 * @author simon
 * @create 2018-10-29 11:51
 **/
@Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

  @Autowired
  private AuthenticationManager authenticationManager;

  @Autowired
  UserDetailsService userDetailsService;

  // 使用最基本的InMemoryTokenStore生成token
  @Bean
  public TokenStore memoryTokenStore() {
    return new InMemoryTokenStore();
  }

  /**
   * 配置客戶端詳情服務
   * 客戶端詳細資訊在這裡進行初始化,你能夠把客戶端詳情資訊寫死在這裡或者是通過資料庫來儲存調取詳情資訊
   * @param clients
   * @throws Exception
   */
  @Override
  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
            .withClient("client1")//用於標識使用者ID
            .authorizedGrantTypes("authorization_code","refresh_token")//授權方式
            .scopes("test")//授權範圍
            .secret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"));//客戶端安全碼,secret密碼配置從 Spring Security 5.0開始必須以 {bcrypt}+加密後的密碼 這種格式填寫;
  }

  /**
   * 用來配置令牌端點(Token Endpoint)的安全約束.
   * @param security
   * @throws Exception
   */
  @Override
  public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    /* 配置token獲取合驗證時的策略 */
    security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
  }

  /**
   * 用來配置授權(authorization)以及令牌(token)的訪問端點和令牌服務(token services)
   * @param endpoints
   * @throws Exception
   */
  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    // 配置tokenStore,需要配置userDetailsService,否則refresh_token會報錯
    endpoints.authenticationManager(authenticationManager).tokenStore(memoryTokenStore()).userDetailsService(userDetailsService);
  }
}

2.1.3 spring security配置

/**
 * 配置spring security
 *
 * @author simon
 * @create 2018-10-29 16:25
 **/
@EnableWebSecurity//開啟許可權驗證
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  /**
   * 配置這個bean會在做AuthorizationServerConfigurer配置的時候使用
   * @return
   * @throws Exception
   */
  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }

  /**
   * 配置使用者
   * 使用記憶體中的使用者,實際專案中,一般使用的是資料庫儲存使用者,具體的實現類可以使用JdbcDaoImpl或者JdbcUserDetailsManager
   * @return
   */
  @Bean
  @Override
  protected UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("admin").password(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("admin")).authorities("USER").build());
    return manager;
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService());
  }
}

2.1.4 開啟授權服務

在啟動類上添加註解@EnableAuthorizationServer開啟授權服務

2.2 資源服務

2.2.1 新增依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>priv.simon.resource</groupId>
  <artifactId>resource-server</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>resource-server</name>
  <description>資源伺服器</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.6.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <spring-cloud.version>Finchley.SR2</spring-cloud.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>${spring-cloud.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

2.2.2 配置資源服務

auth-server-url: http://localhost:8080 # 授權服務地址

server:
  port: 8088
security:
  oauth2:
    client:
      client-id: client1
      client-secret: 123456
      scope: test
      access-token-uri: ${auth-server-url}/oauth/token
      user-authorization-uri: ${auth-server-url}/oauth/authorize
    resource:
      token-info-uri: ${auth-server-url}/oauth/check_token #檢查令牌

2.2.3 開啟資源服務

在啟動類上新增註解@EnableResourceServer開啟資源服務,並提供資源獲取介面


@EnableResourceServer
@RestController
@SpringBootApplication
public class ResourceServerApplication {

  private static final Logger log = LoggerFactory.getLogger(ResourceServerApplication.class);

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

  @GetMapping("/user")
  public Authentication getUser(Authentication authentication) {
    log.info("resource: user {}", authentication);
    return authentication;
  }
}

2.3 測試

  1. 獲取授權碼
    傳送GET請求獲取授權碼,回撥地址隨意寫就可以
http://localhost:8080/oauth/authorize?response_type=code&client_id=client1&redirect_uri=http://baidu.com

如果沒有登入,瀏覽器會重定向到登入介面
登入介面
輸入使用者名稱和密碼(admin/admin)點選登入,這時會進入授權頁
授權頁面
點選授權,瀏覽器會從定向到回撥地址上,攜帶code引數
授權成功

  1. 獲取令牌
    通過post請求獲取令牌
    postman請求
    請求失敗,報401 authentication is required的錯誤

經過研究發現:
/oauth/token端點:

  • 這個如果配置支援allowFormAuthenticationForClients的,且url中有client_id和client_secret的會走ClientCredentialsTokenEndpointFilter來保護
  • 如果沒有支援allowFormAuthenticationForClients或者有支援但是url中沒有client_id和client_secret的,走basic認證保護

那麼授權伺服器配置修改如下:

/**
   * 用來配置令牌端點(Token Endpoint)的安全約束.
   * @param security
   * @throws Exception
   */
  @Override
  public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    /* 配置token獲取合驗證時的策略 */
    security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()").allowFormAuthenticationForClients();
  }

重新發送請求
postman請求

返回資料如下:

{
    "access_token": "c0fa303f-77ad-427b-8f50-c5b3e031d109",
    "token_type": "bearer",
    "refresh_token": "a9b4a4c5-4e27-4328-ae1b-91cdd7c85f15",
    "expires_in": 43199,
    "scope": "test"
}
  1. 從資源服務獲取資源
    攜帶access_token引數請求資源
http://localhost:8088/user?access_token=c0fa303f-77ad-427b-8f50-c5b3e031d109

結果返回:

{
    "authorities": [
        {
            "authority": "ROLE_test"
        }
    ],
    "details": {
        "remoteAddress": "0:0:0:0:0:0:0:1",
        "sessionId": null,
        "tokenValue": "c0fa303f-77ad-427b-8f50-c5b3e031d109",
        "tokenType": "Bearer",
        "decodedDetails": null
    },
    "authenticated": true,
    "userAuthentication": {
        "authorities": [
            {
                "authority": "ROLE_test"
            }
        ],
        "details": null,
        "authenticated": true,
        "principal": "admin",
        "credentials": "N/A",
        "name": "admin"
    },
    "principal": "admin",
    "credentials": "",
    "oauth2Request": {
        "clientId": "client1",
        "scope": [
            "test"
        ],
        "requestParameters": {
            "client_id": "client1"
        },
        "resourceIds": [],
        "authorities": [],
        "approved": true,
        "refresh": false,
        "redirectUri": null,
        "responseTypes": [],
        "extensions": {},
        "refreshTokenRequest": null,
        "grantType": null
    },
    "clientOnly": false,
    "name": "admin"
}
  1. 重新整理token
    重新整理token

github下載原始碼