1. 程式人生 > >16、SpringBoot單點登入SSO資料庫實現

16、SpringBoot單點登入SSO資料庫實現

    首先以一張圖形式說明單點認證的整個流程:

   前一篇文章中,我們使用了記憶體寫死的模式實現使用者的授權和資源的保護,當然token以及clients資訊儲存有多種方式,有inmemory記憶體模式、redis儲存模式、jdbc儲存模式、jwt儲存模式、jwk儲存模式等等。是實際的生產環境,為了安全我們會將客戶資訊以及token資訊儲存在資料庫,以便伺服器之間共享和保證資料斷電安全。這篇文章中,我們將告知如何實現以jdbc(即資料)將授權資訊儲存到資料庫,將授權資訊儲存到資料庫並用資料庫中的使用者名稱和密碼驗證資料使用者許可權;

一、回顧以記憶體模式

@Order(1)          // 使用註解方式使bean的載入順序得到控制,配置改類被載入的順序,優先順序為1
@EnableWebSecurity // 包含@Configuration,@EnableWebSecurity註解以及WebSecurityConfigurerAdapter一起配合提供基於web的security
public class SecurityConfig extends WebSecurityConfigurerAdapter{
     /**
      * Override this method to configure the HttpSecurity. Typically subclasses should not invoke this method by calling super as it may override their configuration. The default configuration is:
      * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
      */
      @Override
    protected void configure(HttpSecurity http) throws Exception {
           http.requestMatchers()
               //.antMatchers("/login", "/oauth/authorize", "/oauth/accessToken", "/auth/oauth/token")                  // 這些頁面不需要授權
               .antMatchers("/login")                                                                                   // 這些頁面不需要授權
              .and()
              .authorizeRequests()
               .anyRequest().authenticated()                                                                            // 其他所有頁面必須授權
              .and()
               .formLogin()                                                                                             //定製登入表單
                        //.loginPage("/login")                                                                          //設定登入url
                        //.defaultSuccessUrl("/home")                                                                   //設定登入成功預設跳轉url
                        .permitAll();
    }
     /**
      * 1、建立使用者
      * 2、驗證使用者
      */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {     
     // 記憶體中建立[驗證]一個使用者名稱為john,密碼為123的使用者,第三方應用登入的時候必須指定改使用者名稱和密碼通過認證後才發放code授權碼
     // 第三方應用憑藉使用者授權(使用使用者名稱和密碼)獲取的授權碼換取access_token然後通過access_token獲取使用者資訊
     // 角色名會預設被新增上"ROLE_"字首,如下USER角色,實際的名稱為ROLE_USER
     auth.inMemoryAuthentication()                                 // 記憶體驗證模式
             .withUser("john")                                     // 建立的使用者名稱
             .password(passwordEncoder().encode("123"))          // 驗證的使用者密碼
             .roles("USER")                                        // 改使用者的角色
             .and()                                                // 級聯
             .withUser("lixx")                                     // 建立使用者lixx
             .password(passwordEncoder().encode("dw123456")) // 使用者密碼
             .roles("ADMIN", "USER");                          // 使用者角色為ROLE_ADMIN和ROLE_USER有兩種角色
    }
   
    /**
     * 攔截URL,設定忽略安全校驗的靜態資源攔截
     * Override this method to configure WebSecurity. For example, if you wish to ignore certain requests.
     */
    @Override
     public void configure(WebSecurity web) throws Exception {
     web
          .ignoring()                                              // 忽略如下請求
          .antMatchers("/resources/**");                       // 忽略以/resources/打頭的請求
     }
     /**
     * 建立加密物件,對密碼進行加密
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

這裡的重點是:

protected void configure(AuthenticationManagerBuilder auth) throws Exception 

使用記憶體授權模式並在記憶體中建立了兩個使用者join,密碼是123(儲存的時候需要加密),另一個使用者是lixx,密碼是dw123456,角色為ADMIN和USER,會自動新增上ROLE_字首;

並使用了

protected void configure(HttpSecurity http) throws Exception

做授權保護,指定哪些頁面可以訪問,哪些頁面需要授權;

二、以資料庫儲存模式

如果是以資料庫方式儲存先關的授權、token訪問、token期限等,必須實現建立起oauth2的相關表,

1、在資料庫中,建立相關表,建立語句如下:

mysql建表語句如下:

-- used in tests that use HSQL
create table oauth_client_details (
client_id VARCHAR(128) PRIMARY KEY,
resource_ids VARCHAR(128),
client_secret VARCHAR(128),
scope VARCHAR(128),
authorized_grant_types VARCHAR(128),
web_server_redirect_uri VARCHAR(128),
authorities VARCHAR(128),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(128)
);

create table oauth_client_token (
token_id VARCHAR(128),
token blob,
authentication_id VARCHAR(128) PRIMARY KEY,
user_name VARCHAR(128),
client_id VARCHAR(128)
);

create table oauth_access_token (
token_id VARCHAR(128),
token blob,
authentication_id VARCHAR(128) PRIMARY KEY,
user_name VARCHAR(128),
client_id VARCHAR(128),
authentication blob,
refresh_token VARCHAR(128)
);

create table oauth_refresh_token (
token_id VARCHAR(128),
token blob,
authentication blob
);

create table oauth_code (
code VARCHAR(128), authentication blob
);

create table oauth_approvals (
userId VARCHAR(128),
clientId VARCHAR(128),
scope VARCHAR(128),
status VARCHAR(10),
expiresAt TIMESTAMP,
lastModifiedAt TIMESTAMP
);

jdbc對應建表語句(根據不同資料庫修正LONGVARBINARY對應型別):

-- used in tests that use HSQL
create table oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY,
  resource_ids VARCHAR(256),
  client_secret VARCHAR(256),
  scope VARCHAR(256),
  authorized_grant_types VARCHAR(256),
  web_server_redirect_uri VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(256)
);

create table oauth_client_token (
  token_id VARCHAR(256),
  token LONGVARBINARY,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256)
);

create table oauth_access_token (
  token_id VARCHAR(256),
  token LONGVARBINARY,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication LONGVARBINARY,
  refresh_token VARCHAR(256)
);

create table oauth_refresh_token (
  token_id VARCHAR(256),
  token LONGVARBINARY,
  authentication LONGVARBINARY
);

create table oauth_code (
  code VARCHAR(256), authentication LONGVARBINARY
);

create table oauth_approvals (
    userId VARCHAR(256),
    clientId VARCHAR(256),
    scope VARCHAR(256),
    status VARCHAR(10),
    expiresAt TIMESTAMP,
    lastModifiedAt TIMESTAMP
);

表各個欄位含義如下:

表名

欄位名

欄位說明

oauth_client_details

client_id

主鍵,必須唯一,不能為空. 

用於唯一標識每一個客戶端(client); 在註冊時必須填寫(也可由服務端自動生成). 

對於不同的grant_type,該欄位都是必須的. 在實際應用中的另一個名稱叫appKey,與client_id是同一個概念.

resource_ids

客戶端所能訪問的資源id集合,多個資源時用逗號(,)分隔,如: "unity-resource,mobile-resource". 

該欄位的值必須來源於與security.xml中標籤‹oauth2:resource-server的屬性resource-id值一致. 在security.xml配置有幾個‹oauth2:resource-server標籤, 則該欄位可以使用幾個該值. 

在實際應用中, 我們一般將資源進行分類,並分別配置對應的‹oauth2:resource-server,如訂單資源配置一個‹oauth2:resource-server, 使用者資源又配置一個‹oauth2:resource-server. 當註冊客戶端時,根據實際需要可選擇資源id,也可根據不同的註冊流程,賦予對應的資源id.

client_secret

用於指定客戶端(client)的訪問密匙; 在註冊時必須填寫(也可由服務端自動生成). 

對於不同的grant_type,該欄位都是必須的. 在實際應用中的另一個名稱叫appSecret,與client_secret是同一個概念.

scope

指定客戶端申請的許可權範圍,可選值包括read,write,trust;若有多個許可權範圍用逗號(,)分隔,如: "read,write". 

scope的值與security.xml中配置的‹intercept-url的access屬性有關係. 如‹intercept-url的配置為

‹intercept-url pattern="/m/**" access="ROLE_MOBILE,SCOPE_READ"/>

則說明訪問該URL時的客戶端必須有read許可權範圍. write的配置值為SCOPE_WRITE, trust的配置值為SCOPE_TRUST. 

在實際應該中, 該值一般由服務端指定, 常用的值為read,write.

authorized_grant_types

指定客戶端支援的grant_type,可選值包括authorization_code,password,refresh_token,implicit,client_credentials, 若支援多個grant_type用逗號(,)分隔,如: "authorization_code,password". 

在實際應用中,當註冊時,該欄位是一般由伺服器端指定的,而不是由申請者去選擇的,最常用的grant_type組合有: "authorization_code,refresh_token"(針對通過瀏覽器訪問的客戶端); "password,refresh_token"(針對移動裝置的客戶端). 

implicit與client_credentials在實際中很少使用.

web_server_redirect_uri

客戶端的重定向URI,可為空, 當grant_type為authorization_code或implicit時, 在Oauth的流程中會使用並檢查與註冊時填寫的redirect_uri是否一致. 下面分別說明:

  • 當grant_type=authorization_code時, 第一步 從 spring-oauth-server獲取 'code'時客戶端發起請求時必須有redirect_uri引數, 該引數的值必須與web_server_redirect_uri的值一致. 第二步 用 'code' 換取 'access_token' 時客戶也必須傳遞相同的redirect_uri. 

    在實際應用中, web_server_redirect_uri在註冊時是必須填寫的, 一般用來處理伺服器返回的code, 驗證state是否合法與通過code去換取access_token值. 

    spring-oauth-client專案中, 可具體參考AuthorizationCodeController.java中的authorizationCodeCallback方法.

authorities

指定客戶端所擁有的Spring Security的許可權值,可選, 若有多個許可權值,用逗號(,)分隔, 如: "ROLE_UNITY,ROLE_USER". 

對於是否要設定該欄位的值,要根據不同的grant_type來判斷, 若客戶端在Oauth流程中需要使用者的使用者名稱(username)與密碼(password)的(authorization_code,password), 

則該欄位可以不需要設定值,因為服務端將根據使用者在服務端所擁有的許可權來判斷是否有許可權訪問對應的API. 

但如果客戶端在Oauth流程中不需要使用者資訊的(implicit,client_credentials), 

則該欄位必須要設定對應的許可權值, 因為服務端將根據該欄位值的許可權來判斷是否有許可權訪問對應的API. 

(請在spring-oauth-client專案中來測試不同grant_type時authorities的變化)

access_token_validity

設定客戶端的access_token的有效時間值(單位:秒),可選, 若不設定值則使用預設的有效時間值(60 * 60 * 12, 12小時). 

在服務端獲取的access_token JSON資料中的expires_in欄位的值即為當前access_token的有效時間值. 

在專案中, 可具體參考DefaultTokenServices.java中屬性accessTokenValiditySeconds. 

在實際應用中, 該值一般是由服務端處理的, 不需要客戶端自定義.

refresh_token_validity

設定客戶端的refresh_token的有效時間值(單位:秒),可選, 若不設定值則使用預設的有效時間值(60 * 60 * 24 * 30, 30天). 

若客戶端的grant_type不包括refresh_token,則不用關心該欄位 在專案中, 可具體參考DefaultTokenServices.java中屬性refreshTokenValiditySeconds. 

在實際應用中, 該值一般是由服務端處理的, 不需要客戶端自定義.

additional_information

這是一個預留的欄位,在Oauth的流程中沒有實際的使用,可選,但若設定值,必須是JSON格式的資料,如:

{"country":"CN","country_code":"086"}

按照spring-security-oauth專案中對該欄位的描述 

Additional information for this client, not need by the vanilla OAuth protocol but might be useful, for example,for storing descriptive information. 

(詳見ClientDetails.java的getAdditionalInformation()方法的註釋)

在實際應用中, 可以用該欄位來儲存關於客戶端的一些其他資訊,如客戶端的國家,地區,註冊時的IP地址等等.

create_time

資料的建立時間,精確到秒,由資料庫在插入資料時取當前系統時間自動生成(擴充套件欄位)

archived

用於標識客戶端是否已存檔(即實現邏輯刪除),預設值為'0'(即未存檔). 

對該欄位的具體使用請參考CustomJdbcClientDetailsService.java,在該類中,擴充套件了在查詢client_details的SQL加上archived = 0條件 (擴充套件欄位)

trusted

設定客戶端是否為受信任的,預設為'0'(即不受信任的,1為受信任的). 

該欄位只適用於grant_type="authorization_code"的情況,當用戶登入成功後,若該值為0,則會跳轉到讓使用者Approve的頁面讓使用者同意授權, 

若該欄位為1,則在登入後不需要再讓使用者Approve同意授權(因為是受信任的). 

對該欄位的具體使用請參考OauthUserApprovalHandler.java. (擴充套件欄位)

autoapprove

設定使用者是否自動Approval操作, 預設值為 'false', 可選值包括 'true','false', 'read','write'. 

該欄位只適用於grant_type="authorization_code"的情況,當用戶登入成功後,若該值為'true'或支援的scope值,則會跳過使用者Approve的頁面, 直接授權. 

該欄位與 trusted 有類似的功能, 是 spring-security-oauth2 的 2.0 版本後新增的新屬性.

 在專案中,主要操作oauth_client_details表的類是JdbcClientDetailsService.java, 更多的細節請參考該類. 

也可以根據實際的需要,去擴充套件或修改該類的實現.

oauth_client_token

create_time

資料的建立時間,精確到秒,由資料庫在插入資料時取當前系統時間自動生成(擴充套件欄位)

token_id

從伺服器端獲取到的access_token的值.

token

這是一個二進位制的欄位, 儲存的資料是OAuth2AccessToken.java物件序列化後的二進位制資料.

authentication_id

該欄位具有唯一性, 是根據當前的username(如果有),client_id與scope通過MD5加密生成的. 

具體實現請參考DefaultClientKeyGenerator.java類.

user_name

登入時的使用者名稱

client_id

 該表用於在客戶端系統中儲存從服務端獲取的token資料, 在spring-oauth-server專案中未使用到. 

對oauth_client_token表的主要操作在JdbcClientTokenServices.java類中, 更多的細節請參考該類.

oauth_access_token

create_time

資料的建立時間,精確到秒,由資料庫在插入資料時取當前系統時間自動生成(擴充套件欄位)

token_id

該欄位的值是將access_token的值通過MD5加密後儲存的.

token

儲存將OAuth2AccessToken.java物件序列化後的二進位制資料, 是真實的AccessToken的資料值.

authentication_id

該欄位具有唯一性, 其值是根據當前的username(如果有),client_id與scope通過MD5加密生成的. 具體實現請參考DefaultAuthenticationKeyGenerator.java類.

user_name

登入時的使用者名稱, 若客戶端沒有使用者名稱(如grant_type="client_credentials"),則該值等於client_id

client_id

authentication

儲存將OAuth2Authentication.java物件序列化後的二進位制資料.

refresh_token

該欄位的值是將refresh_token的值通過MD5加密後儲存的.

 在專案中,主要操作oauth_access_token表的物件是JdbcTokenStore.java. 更多的細節請參考該類.

oauth_refresh_token

create_time

資料的建立時間,精確到秒,由資料庫在插入資料時取當前系統時間自動生成(擴充套件欄位)

token_id

該欄位的值是將refresh_token的值通過MD5加密後儲存的.

token

儲存將OAuth2RefreshToken.java物件序列化後的二進位制資料.

authentication

儲存將OAuth2Authentication.java物件序列化後的二進位制資料.

 在專案中,主要操作oauth_refresh_token表的物件是JdbcTokenStore.java. (與操作oauth_access_token表的物件一樣);更多的細節請參考該類. 

如果客戶端的grant_type不支援refresh_token,則不會使用該表.

oauth_code

create_time

資料的建立時間,精確到秒,由資料庫在插入資料時取當前系統時間自動生成(擴充套件欄位)

code

儲存服務端系統生成的code的值(未加密).

authentication

儲存將AuthorizationRequestHolder.java物件序列化後的二進位制資料.

 在專案中,主要操作oauth_code表的物件是JdbcAuthorizationCodeServices.java. 更多的細節請參考該類. 

只有當grant_type為"authorization_code"時,該表中才會有資料產生; 其他的grant_type沒有使用該表.

2、引入對應的包

<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.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <!-- 基本資訊 -->
  <modelVersion>4.0.0</modelVersion>
  <artifactId>spring-auth-server</artifactId>
  <name>認證授權服務</name>
  <description>認證授權服務</description>
 
  <!-- 父專案 -->
  <parent>
    <groupId>com.easystudy</groupId>
    <artifactId>spring-cloud-oauther2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
 
  <!-- 強依賴 -->
  <dependencies>
     <!-- 公共資訊包 -->
     <dependency>
          <groupId>com.easystudy</groupId>
          <artifactId>spring-auth-common</artifactId>
          <version>0.0.1-SNAPSHOT</version>
     </dependency>
     <!-- spring cloud 客戶註冊 -->
     <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
     </dependency>
     <!-- oauth2認證 -->
     <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>
     <!-- 熔斷機制 -->
     <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-hystrix</artifactId>
          <version>1.4.0.RELEASE</version>
     </dependency>
     <!-- 遠端呼叫 -->
     <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-feign</artifactId>
          <version>1.4.0.RELEASE</version>
     </dependency>
     <!-- 負載 -->
     <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-ribbon</artifactId>
          <version>1.4.0.RELEASE</version>
     </dependency>
     <!-- redis -->
     <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
     </dependency>
     <!-- jpa註解 -->
     <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-jpa</artifactId>
     </dependency>
     <!-- mysql -->
    <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
     </dependency>
     <!-- 阿里巴巴druid資料庫連線池 -->
     <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid-spring-boot-starter</artifactId>
          <version>1.1.9</version>
     </dependency>
    <!-- 使用webjar管理前端資源,此處引入bootstrap和jquery方便演示 -->
     <dependency>
          <groupId>org.webjars</groupId>
          <artifactId>jquery</artifactId>
          <version>2.1.1</version>
     </dependency>
     <dependency>
          <groupId>org.webjars</groupId>
          <artifactId>bootstrap</artifactId>
          <version>3.3.7</version>
     </dependency>
     <!-- 輔助定位靜態資源,省略 webjar的版本:http://localhost:8080/webjars/jquery/3.1.0/jquery.js路徑中可以省略3.1.0 -->
     <dependency>
          <groupId>org.webjars</groupId>
          <artifactId>webjars-locator</artifactId>
          <version>0.32</version>
     </dependency>
  </dependencies>
</project>

3、資料來源配置

application.yml配置如下所示:

#服務配置
server:
  #監聽埠
  port: 8762
  servlet:
    context-path: /auth
spring:
  application:
    #服務名稱
    name: auth2.0-center
  #分散式系統跟蹤服務
  zipkin:
      base-url:http://localhost:8763
  #資料來源配置
  datasource:
    url: jdbc:mysql://localhost:3306/zuul-auth?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
    druid:
      driver-class-name: com.mysql.jdbc.Driver
      # 連線池配置----------------------
      #初始連線池大小
      initial-size: 5
      #最小閒置連線
      min-idle: 5
      #最大活動連線大小
      max-active: 20
      #配置獲取連線等待超時的時間
      max-wait: 60000
      # 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連線,單位是毫秒
      time-between-eviction-runs-millis: 60000
      # 配置一個連線在池中最小生存的時間,單位是毫秒
      min-evictable-idle-time-millis: 300000
      # 驗證連線有效性
      validation-query: SELECT 'x'
      validation-query-timeout: 6
      #申請連線的時候檢測,如果空閒時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測連線是否有效
      test-while-idle: true
      #這裡建議配置為TRUE,防止取到的連線不可用。獲取連結的時候,不校驗是否可用,開啟會有損效能
      test-on-borrow: false
      #歸還連結到連線池的時候校驗連結是否可用
      test-on-return: false
      # 開啟PSCache,並且指定每個連線上PSCache的大小
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      filters: stat,wall,log4j
      #慢SQL記錄:開啟合併sql、開啟慢查詢語句,5000毫秒
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
  #redis配置
  redis:
    #redis地址
    host: 127.0.0.1
    #redis埠,預設6379
    port: 6379
    #redis校驗密碼
    #password: 123456
   
#eureka叢集配置
eureka:
  instance:
    #將IP註冊到Eureka Server上,如果不配置就是機器的主機名
    prefer-ip-address: true
    #例項名定義為:"ip:port" 如果spring.cloud.client.ipAddress獲取不到則使用spring.cloud.client.ip_address
    #instance-id: ${spring.cloud.client.ip_address}:${server.port}
    #隔多久去拉取服務註冊資訊,m預設30s
    registry-fetch-interval-seconds: 30
    #client傳送心跳給server端的頻率,m預設30s,如果server端leaseExpirationDurationInSeconds
    #後沒有收到client的心跳,則將摘除該instance
    lease-renewal-interval-in-seconds: 10
    #表示eureka server至上一次收到client的心跳之後,等待下一次心跳的超時時間,
    #在這個時間內若沒收到下一次心跳,則將移除該instance,預設是90秒
    lease-expiration-duration-in-seconds: 30
  #eureka客戶端配置
  client:
      #註冊到eureka伺服器地址
      service-url:
        #可以配置多個
        #defaultZone: http://mss-eureka1:9010/eureka/,http://mss-eureka2:9011/eureka/
        defaultZone:http://localhost:8761/eureka/
# ----Spring Boot Actuator:監控系統配置
endpoints:
  health:
    sensitive: false
    enabled: false
  shutdown:
    #Spring Boot Actuator的shutdown endpoint預設是關閉的
    enabled: true
    #自定義api地址:host:port/shutdown就可以實現優雅停機
    path: /shutdown
    #使用密碼驗證-專案中新增的有Spring Security,所有通過HTTP暴露的敏感端點都會受到保護
    #預設情況下會使用基本認證(basic authentication,使用者名稱為user,密碼為應用啟動時在控制檯列印的密碼)
    sensitive: true
management:
  security:
    #重新整理時,關閉安全驗證
    enabled: false
# ----Spring Boot Actuator:監控系統配置
#安全校驗
security:
  oauth2:
    resource:
      #本來spring security的基礎上使用了spring security oauth2,控制/api下的請求
      #但是spring security的資源控制和spring securtiy oauth2的資源控制會互相覆蓋
      #如果配置添加了security.oauth2.resource.filter-order=3,則使用spring security的控制,反之則為oauth2的控制
      filter-order: 3
#系統日誌配置
logging:
  #日誌路徑
  config: classpath:logback.xml
  #不同元件的日誌顯示級別
  level:
    org:
      springframework:
        web: info
#feign 預設關閉熔斷,請看HystrixFeignConfiguration
feign:
  hystrix:
    #啟用熔斷機制
    enabled: true
#熔斷配置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            #設定API閘道器中路由轉發請求的HystrixCommand執行超時時間
            #在zuul配置了熔斷fallback的話,熔斷超時也要配置,不然如果你配置的ribbon超時時間大於熔斷的超時,那麼會先走熔斷,相當於你配的ribbon超時就不生效了
            #這裡面ribbon和hystrix是同時生效的,哪個值小哪個生效,另一個就看不到效果了
            timeoutInMilliseconds: 60000
           
#ribbon負載均衡配置
ribbon:
  #設定路由轉發請求的時候,建立請求連線的超時時間
  ReadTimeout: 30000
  #用來設定路由轉發請求的超時時間
  ConnectTimeout: 60000
  # 最大重試次數
  MaxAutoRetries: 2
  # 重試下一服務次數[排除第一臺伺服器]
  MaxAutoRetriesNextServer: 0

注意:server.servlet.context-path為配置的專案上下文路徑,可以理解為專案名稱(同tomcat的專案名,訪問指定的專案必須帶上改路徑:http://localhost:8080/專案名/login.html),當然這裡可以不用配置druid(因為沒有使用到)

3、修改http security配置

將記憶體模式修改為JDBC資料庫模式:

package com.easystudy.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import com.easystudy.service.impl.UserDetailsServiceImpl;

/**
* 安全服務配置(Spring Security http URL攔截保護)::
* URL強制攔截保護服務,可以配置哪些路徑不需要保護,哪些需要保護。預設全都保護
* 繼承了WebSecurityConfigurerAdapter之後,再加上幾行程式碼,我們就能實現以下的功能:
* 1、要求使用者在進入你的應用的任何URL之前都進行驗證
* 2、建立一個使用者名稱是“user”,密碼是“password”,角色是“ROLE_USER”的使用者
* 3、啟用HTTP Basic和基於表單的驗證
* 4、Spring Security將會自動生成一個登陸頁面和登出成功頁面
*
* @EnableWebSecurity註解以及WebSecurityConfigurerAdapter一起配合提供基於web的security。
* 繼承了WebSecurityConfigurerAdapter之後,再加上幾行程式碼,我們就能實現以下的功能:
* 1、要求使用者在進入你的應用的任何URL之前都進行驗證
* 2、建立一個使用者名稱是“user”,密碼是“password”,角色是“ROLE_USER”的使用者
* 3、啟用HTTP Basic和基於表單的驗證
* 4、Spring Security將會自動生成一個登陸頁面和登出成功頁面
* 預設頁面:
* 登入頁面:/login
* 登出頁面:/login?logout
* 錯誤頁面:/login?error
*
* 與ResourceServerConfigurerAdapter區別
* 1、ResourceServerConfigurerAdapter被配置為不同的端點(參見antMatchers),而WebSecurityConfigurerAdapter不是。
*   這兩個介面卡之間的區別在於,RealServServer配置介面卡使用一個特殊的過濾器來檢查請求中的承載令牌,以便通過OAuth2對請求進行認證。
*   WebSecurityConfigurerAdapter介面卡用於通過會話對使用者進行身份驗證(如表單登入)
* 2、WebSecurityConfigurerAdapter是預設情況下spring security的http配置,
*   ResourceServerConfigurerAdapter是預設情況下spring security oauth2的http配置
*   在ResourceServerProperties中,定義了它的order預設值為SecurityProperties.ACCESS_OVERRIDE_ORDER - 1;是大於1的,
*   即WebSecurityConfigurerAdapter的配置的攔截要優先於ResourceServerConfigurerAdapter,優先順序高的http配置是可以覆蓋優先順序低的配置的。
*   某些情況下如果需要ResourceServerConfigurerAdapter的攔截優先於WebSecurityConfigurerAdapter需要在配置檔案中新增
*   security.oauth2.resource.filter-order=99
*   
*   
重點:
@@如果使用jdbc儲存客戶資訊,必須做如下粗啊哦做:
(1)建立如下資料表oauth_client_details,auth2會從oauth_client_details查詢授權祕鑰資訊(在哪一個資料庫中建立,就看你配置的資料來源是哪一個)
(2)往對應表中插入記錄(一般是第三方app註冊之後返回給第三方app的clientid和祕鑰資訊
          這個是預設的類的表,一般用它預設的即可,我們這邊就需要根據以上的欄位配置相關的內容,如下:
insert into oauth_client_details(client_id,resource_ids,client_secret,scope,authorized_grant_types,
                                 web_server_redirect_uri,authorities,access_token_validity,
                                 refresh_token_validity,autoapprove)
                        values('test_client_id', 'test_resource_id', 'test_client_secret',
                               'user_info','authorization_code', 'http://localhost:8082/ui/login',
                               'ROLE_ADMIN', 3600, 7200, 'true');
注意:scope客戶受限的範圍。如果範圍未定義或為空(預設值),客戶端不受範圍限制。read write all
    authorities授予客戶的授權機構(普通的Spring Security權威機構)。
    authorized_grant_types:有:authorization_code,password,refresh_token,implicit,client_credentials, 若支援多個grant_type用逗號(,)分隔,如:  "authorization_code,refresh_token".
    additional_information預設為NULL,否則必須為json格式Map<String,Object>形式的字串,例如:{"systemInfo":"Atlas System"}
*/

@Configuration
@EnableWebSecurity          // 建立了一個WebSecurityConfigurerAdapter,且自帶了硬編碼的order=3,使用spring security而不是auth
//@Order(1)                 // 定義攔截器配置攔截次序,高於ResourceServerConfigurerAdapter
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    // 自定義使用者服務-校驗使用者是否合法,實現改介面,spring即可獲取對應使用者的角色、許可權等資訊,然後可以攔截URL判斷是否具有對應許可權
    // 具體是否可以訪問對應URL配置可以在HttpSecurity中配置
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    
    /**
     * 密碼加密器:將使用者密碼進行加密
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 使用BCrypt進行密碼的hash
        return new BCryptPasswordEncoder();
    }

    /**
     * 不定義沒有password grant_type即密碼授權模式(總共四種授權模式:授權碼、implicat精簡模式、密碼、client credentials)
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 如果有要忽略攔截校驗的靜態資源,在此處新增
     * 忽略任何以”/resources/”開頭的請求,這和在XML配置[email protected]=none的效果一樣
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        // TODO Auto-generated method stub
        web
        .ignoring()
        .antMatchers("/resources/**");    
    }

    /**
     * 允許對特定的http請求基於安全考慮進行配置,預設情況下,適用於所有的請求,
     * 但可以使用requestMatcher(RequestMatcher)或者其它相似的方法進行限制
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 如:將所有的url訪問許可權設定為角色名稱為"ROLE_USER"
        // http.authorizeRequests().antMatchers("/").hasRole("USER").and().formLogin();
        
        // 啟用HTTP Basic和基於表單的驗證
        http.authorizeRequests()                            // 定義許可權配置
                //.antMatchers("/login").permitAll()
                .anyRequest().authenticated()                // 任何請求都必須經過認證才能訪問--登入後可以訪問任意頁面
            .and()
                .formLogin()                                // 定製登入表單
                    //.loginPage("/login")                    // 設定登入url
                    //.failureUrl("/login?error")
                    //.defaultSuccessUrl("/home")                // 設定登入成功預設跳轉url
                    .permitAll()                            //允許任何人訪問登入url
            .and()
                    .logout().permitAll()
            .and()
                .csrf().disable()                            // 禁止跨域請求
                .httpBasic();                                // 進行http Basic認證                    
    }
    
    /**
     * 系統安全使用者驗證模式:
     * 1、使用記憶體模式建立驗證
     * 2、使用資料庫建立驗證,實現userDetailsService介面即可
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 將驗證過程交給自定義
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
        
        // 記憶體建立使用者:寫死不利於專案實際應用
        // 驗證的時候就是通過建立的使用者名稱、密碼、角色進行驗證的
        // 建立一個使用者名稱是“user”,密碼是“password”,角色是“ROLE_USER”的使用者
        // 建立一個使用者名稱是“admin”,密碼是“123456”,角色是“ROLE_ADMIN以及ROLE_USER”的使用者
        //auth
        //    .inMemoryAuthentication()
        //        .withUser("user").password("password").roles("USER")              // 在記憶體中的驗證(memory authentication)叫作”user”的使用者
        //    .and()
        //        .withUser("admin").password("123456").roles("ADMIN", "USER");    // 在記憶體中的驗證(memory authentication)叫作”admin”的管理員使用者
    }
}

4、自定義使用者資訊實現

    實際的專案中,我們的登入使用者資料可能存在資料庫中,也可能是存放在ladap或其他微服務介面中,springcloud oauth2給我們提供了一個UserDetailsService介面,在專案中,我們需要自行實現這個介面來獲取使用者資訊,Spring Auth2提供了獲取UserDetails的方式,只要實現UserDetailsService介面即可, 最終生成使用者和許可權共同組成的UserDetails,在這裡就可以實現從自定義的資料來源中獲取使用者資訊。這裡我使用的feign遠端介面呼叫的方式實現使用者資訊的獲取,真實的使用者服務有eureka中的user-service提供;

package com.easystudy.service.impl;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.easystudy.error.ReturnValue;
import com.easystudy.model.Rights;
import com.easystudy.model.Role;
import com.easystudy.model.RoleRights;
import com.easystudy.service.RightsService;
import com.easystudy.service.RoleRightsService;
import com.easystudy.service.RoleService;
import com.easystudy.service.UserService;

/**
* 實際的專案中,我們的登入使用者資料可能存在資料庫中,也可能是存放在ladap或其他微服務介面中,
* springcloud oauth2給我們提供了一個UserDetailsService介面,在專案中,
* 我們需要自行實現這個介面來獲取使用者資訊
* 提供了獲取UserDetails的方式,只要實現UserDetailsService介面即可,
* 最終生成使用者和許可權共同組成的UserDetails,在這裡就可以實現從自定義的資料來源
* 中獲取使用者資訊
* @author Administrator
*
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserService userService;                    // 使用者服務
    @Autowired
    private RoleService roleService;                    // 角色服務
    @Autowired
    private RoleRightsService roleRightsService;        // 許可權服務
    @Autowired
    private RightsService rightsService;                // 許可權服務


    /**
     * 通過使用者名稱獲取使用者資訊給oauth2
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查詢使用者
        ReturnValue<com.easystudy.model.User> userResult = userService.findByUsername(username);
        if (userResult.getError() != 0) {
            throw new UsernameNotFoundException("使用者:" + username + ",不存在!");
        }
        
        // 查詢的結果bean拷貝到userVo
        com.easystudy.model.User userVo = new com.easystudy.model.User();
        BeanUtils.copyProperties(userResult.getValue(),userVo);
        
        // 使用者使用者id查詢對應的角色
        ReturnValue<List<Role>> roleResult = roleService.getRolesByUserId(userVo.getId());
        
        // 設定使用者許可權
        Set<GrantedAuthority> grantedAuthorities = new HashSet<GrantedAuthority>();
        if (0 == roleResult.getError()){            
            // 獲取所有角色許可權
            List<Role> roleVoList = roleResult.getValue();
            for (Role role : roleVoList){
                
                // 角色必須是ROLE_開頭,可以在資料庫中設定---WebSecurityConfig也必須對應進行http驗證,即保持一致---
                GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + role.getName());
                grantedAuthorities.add(grantedAuthority);
                
                // 其他角色-每一個許可權
                ReturnValue<List<RoleRights>> perResult = roleRightsService.getRoleRights(role.getId());
                if (0 == perResult.getError()){
                    List<RoleRights> permissionList = perResult.getValue();
                    for (RoleRights roleRight : permissionList) {
                        ReturnValue<Rights> right = rightsService.findByRightId(roleRight.getRight_id());
                        if(0 == right.getError()){
                            GrantedAuthority authority = new SimpleGrantedAuthority(right.getValue().getName());
                            grantedAuthorities.add(authority);
                        }
                    }
                }
            }
        }
        
        // 標識位設定
        boolean enabled = true;                         // 可用性 :true:可用 false:不可用
        boolean accountNonExpired = true;                 // 過期性 :true:沒過期 false:過期
        boolean credentialsNonExpired = true;             // 有效性 :true:憑證有效 false:憑證無效
        boolean accountNonLocked = true;                 // 鎖定性 :true:未鎖定 false:已鎖定
        
        return new User(userVo.getUsername(), userVo.getPassword(), enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuthorities);
    }
}

這裡實現的功能是:

    通過使用者名稱獲取改使用者的所有角色,然後通過角色獲取改使用者的所有許可權(其實,改步驟可以通過user-service一步可以獲取:通過使用者名稱獲取使用者的許可權列表即可),然後根據使用者名稱、密碼(注意密碼必須使用BCrypt方式加密,否則驗證使用者失敗)、使用者具有的授權陣列生成spring security使用者,提供給Spring security進行使用者驗證和許可權訪問控制;

4、token授權認證服務配置

只要經過授權認證的使用者才能訪問對應的功能或頁面,所以必須配置授權資訊,授權相關的資訊以資料庫方式儲存,AuthorizationServerConfigurerAdapter實現如下並使用註解標註為授權伺服器:

package com.easystudy.config;

import java.util.concurrent.TimeUnit;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import com.easystudy.service.impl.UserDetailsServiceImpl;

/**
* 建立授權配置資訊:認證伺服器,進行認證和授權,宣告一個認證伺服器,當用此註解後,應用啟動後將自動生成幾個Endpoint
* AuthorizationServerConfigurer包含三種配置:
* ClientDetailsServiceConfigurer:client客戶端的資訊配置,
* client資訊包括:clientId、secret、scope、authorizedGrantTypes、authorities
* (1)scope:表示許可權範圍,可選項,使用者授權頁面時進行選擇
* (2)authorizedGrantTypes:有四種授權方式
*         Authorization Code:用驗證獲取code,再用code去獲取token(用的最多的方式,也是最安全的方式)
*         Implicit: 隱式授權模式
*         Client Credentials (用來取得 App Access Token)
*         Resource Owner Password Credentials
* (3)authorities:授予client的許可權
*/
@Configuration
@EnableAuthorizationServer            // oauth2分為資源服務和認證授權服務,可以分開,可以統一程序
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private DataSource dataSource;

    // token儲存資料庫-使用jdbc儲存客戶token資訊
    @Bean
    public JdbcTokenStore jdbcTokenStore(){
        return new JdbcTokenStore(dataSource);
    }

    // 宣告 ClientDetails實現:定義授權的請求的路徑的Bean--JDBC儲存客戶資訊
    @Bean
    public ClientDetailsService JdbcClientDetails() {
        // 使用JdbcClientDetailsService客戶端詳情服務
        // Jdbc實現客戶端詳情服務,資料來源dataSource不做敘述,使用框架預設的表
        return new JdbcClientDetailsService(dataSource);
    }

    /**
     * 授權認證端點攔截驗證:
     * 宣告授權和token的端點以及token的服務的一些配置資訊,比如採用什麼儲存方式、token的有效期等:
     * 用來配置授權(authorization)以及令牌(token)的訪問端點和令牌服務(token services)。
     * 包含以下的端點(以上這些endpoint都在原始碼裡的endpoint包裡面):
     * 1、AuthorizationEndpoint 根據使用者認證獲得授權碼,有下面兩個方法
     *   -/oauth/authorize - GET
     *   -/oauth/authorize - POST
     * 2、TokenEndpoint 客戶端根據授權碼獲取 token
     *   -/oauth/token - GET
     *   -/oauth/token - POST
     * 3、CheckTokenEndpoint 資源伺服器用來校驗token
     *   -/oauth/check_token
     * 4、WhitelabelApprovalEndpoint 顯示授權伺服器的確認頁
     *      -/oauth/confirm_access
     * 5、WhitelabelErrorEndpoint 顯示授權伺服器的錯誤頁
     *   -/oauth/error
     * 6、/oauth/token_key:如果jwt模式則可以用此來從認證伺服器獲取公鑰
     * 這些端點有個特點,如果你自己實現了上面的方法,他會優先使用你提供的方法,利用這個特點,通常都會根據自己的
     * 需要來設計自己的授權確認頁面,例如使用 QQ 登入微博的認證頁面
     * 在官方的示例中,通過下面程式碼直接指定了檢視:
     * registry.addViewController("/oauth/confirm_access").setViewName("authorize");
     *
     * 授權型別:
     * 通過AuthorizationServerEndpointsConfigurer來進行配置,預設情況下,支援除了密碼外的所有授權型別,相關授權型別的一些類:
     * (1)authenticationManager:直接注入一個AuthenticationManager,自動開啟密碼授權型別
     * (2)userDetailsService:如果注入UserDetailsService,那麼將會啟動重新整理token授權型別,會判斷使用者是否還是存活的
     * (3)authorizationCodeServices:AuthorizationCodeServices的例項,auth code 授權型別的服務
     * (4)implicitGrantService:imlpicit grant
     * (5)tokenGranter:
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)
                 // token儲存在資料庫中-生產環境使用以免伺服器崩潰
                 .tokenStore(jdbcTokenStore())
                 // 從資料查使用者授權資訊
                 .userDetailsService(userDetailsService);
                 
        // 配置TokenServices引數
        // Spring Cloud Security OAuth2通過DefaultTokenServices類來完成token生成、過期等 OAuth2 標準規定的業務邏輯
        // 而DefaultTokenServices又是通過TokenStore介面完成對生成資料的持久化        
        endpoints.tokenServices(defaultTokenServices());
    }
    
    /**
     * 用來配置令牌端點(Token Endpoint)的安全約束
     * 宣告安全約束,哪些允許訪問,哪些不允許訪問
     * /oauth/token
     * 1、這個如果配置支援allowFormAuthenticationForClients的,且url中有client_id和client_secret的會走ClientCredentialsTokenEndpointFilter來保護
     * 2、如果沒有支援allowFormAuthenticationForClients或者有支援但是url中沒有client_id和client_secret的,走basic認證保護
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 允許獲取token
        security.tokenKeyAccess("permitAll()");
        // isAuthenticated()檢查access_token需要進行授權
        security.checkTokenAccess("isAuthenticated()");
        // 允許以form形式提交表單資訊,否則向/oauth/token或/oauth/accessToken端點獲取access_token的時候報錯401
        security.allowFormAuthenticationForClients();
    }
    
    /**
     * AuthorizationServerConfigurer 的一個回撥配置項:
     * client的資訊的讀取:在ClientDetailsServiceConfigurer類裡面進行配置,
     * 可以有in-memory、jdbc等多種讀取方式,jdbc需要呼叫JdbcClientDetailsService類,
     * 此類需要傳入相應的DataSource.
     * 用來配置客戶端詳情服務(ClientDetailsService),客戶端詳情資訊在這裡進行初始化,
     * 你能夠把客戶端詳情資訊寫死在這裡或者是通過資料庫來儲存調取詳情資訊
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //clients.withClientDetails(clientDetails());
        // 這個地方指的是從jdbc查出資料來[儲存]
        clients.withClientDetails(JdbcClientDetails());
    }

    /**
     * Spring Cloud Security OAuth2通過DefaultTokenServices類來完成token生成、過期等 OAuth2 標準規定的業務邏輯,
     * 而DefaultTokenServices又是通過TokenStore介面完成對生成資料的持久化。在上面的demo中,TokenStore的預設實現為
     * InMemoryTokenStore,即記憶體儲存。 對於Client資訊,ClientDetailsService介面負責從儲存倉庫中讀取資料,在上面的
     * demo中預設使用的也是InMemoryClientDetialsService實現類。說到這裡就能看出,要想使用資料庫儲存,只需要提供這些介面的
     * 實現類即可。慶幸的是,框架已經為我們寫好JDBC實現了,即JdbcTokenStore和JdbcClientDetailsService
     * <p>注意,自定義TokenServices的時候,需要設定@Primary,否則報錯,</p>
     *
     * token儲存方式共有三種分別是:
     * (1)InMemoryTokenStore:存放記憶體中,不會持久化
     * (2)JdbcTokenStore:存放資料庫中
     * (3)Jwt: json web token
     */
    @Primary
    @Bean
    public DefaultTokenServices defaultTokenServices(){
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(jdbcTokenStore());                                            // 如果儲存在jdbc中就需要建立token儲存表
        tokenServices.setSupportRefreshToken(true);                                                // 支援更換token
        tokenServices.setClientDetailsService(JdbcClientDetails());                            // jdbc具體的祕鑰認證服務-如果儲存在jdbc中就需要建立oauth_client_details表
        //tokenServices.setClientDetailsService(InMemClientDetails());                    // 記憶體祕鑰認證服務
        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(15)); // token有效期自定義設定,30天
        tokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30));// 預設30天
        return tokenServices;
    }
}

5、Feign遠端介面呼叫

以獲取使用者資訊介面定義如下:

package com.easystudy.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import com.easystudy.error.ReturnValue;
import com.easystudy.model.User;
import com.easystudy.service.impl.UserServiceImpl;

/**
* @FeignClient指定呼叫user-service服務的對應介面
* @author Administrator
*/
@FeignClient(name = "user-service",fallback = UserServiceImpl.class)
public interface UserService {
    @GetMapping("user/findByUserName/{username}")
    public ReturnValue<User> findByUsername(@PathVariable("username") String username);
}

錯誤降級介面實現如下:

package com.easystudy.service.impl;
import org.springframework.stereotype.Service;

import com.easystudy.error.ErrorCode;
import com.easystudy.error.ReturnValue;
import com.easystudy.model.User;
import com.easystudy.service.UserService;

import lombok.extern.slf4j.Slf4j;


@Service
@Slf4j            // 不想每次都寫private final Logger logger = LoggerFactory.getLogger(XXX.class);
                // @Slf4j注入後找不到變數log,那就給IDE安裝lombok外掛
public class UserServiceImpl implements UserService {
    @Override
    public ReturnValue<User> findByUsername(String username) {
        log.info("呼叫{}失敗","findByUsername");
        return new ReturnValue<User>(ErrorCode.ERROR_SERVER_ERROR, "呼叫findByUsername介面失敗");
    }
}

6、啟動入口實現

/**
 * 簡單理解一下OAuth2,你要登入一個XX網站下片,但是下片需要註冊登入成為這個網站的會員,你不想註冊,
 * 還好網站提供了qq登入,你選擇qq登入。那麼問題來了,你選擇qq登入時,會跳轉到qq的登入頁面,輸入qq
 * 賬號密碼,注意,這時登入qq與這個XX網站是沒關係的,這是qq做的登入頁,登入時qq會問你是否允許該XXX
 * 網站獲取你的個人資訊如頭像、暱稱等,你勾選同意,登入成功後就又回到了XX網站的某個頁面了。這是一個什麼
 * 流程呢,其實是一個XX網站試圖獲取你個人資訊、然後問你是否同意的一個流程。你同意了,qq才會允許第三方
 * 網站獲取你的個人資訊
 * 使用:
 * 1、通過client_id請求授權碼
 * 請求:
 * http://localhost:8762/auth/oauth/authorize?response_type=code&client_id=test_client_id&redirect_uri=http://localhost:8082/ui/login
 * 響應:
 * http://localhost:8082/ui/login?code=uB0dHT
 * 2、通過授權碼換取token
 * http://localhost:8762/auth/oauth/token?client_id=test_client_id&client_secret=test_client_secret&scope=user_info&grant_type=authorization_code&code=uB0dHT&redirect_uri=http://localhost:8082/ui/login

   注意,改介面必須以post方式提交,測試可以用postman,將後面欄位逐一以鍵值對形式填寫並選擇post方式提交,否則報錯(不支援get方式)
 * 3、
 */
@SpringBootApplication       // SpringBoot應用
@EnableDiscoveryClient       // 開啟服務發現功能
@EnableFeignClients          // 開啟Feign
public class AuthServerApplication {
     public static void main(String[] args) {
          SpringApplication.run(AuthServerApplication.class, args);
     }
}

三、測試

    經過以上步驟,我們的認證伺服器已經搭建完成,啟動AuthServerApplication,然後根據如下測試流程測試,在測試前我們做一個假設:假如我要訪問你的網站(http://localhost:8082/ui/login),你要求必須經過授權中心(http://localhost:8762)統一認證才能訪問對應資源或介面,所以

(1)第一步,訪問你的網站你會跳轉至認證中心的授權連結,你提供你程式向授權中心註冊時候返回的client_id,redirect_uri獲取授權碼,注意改步驟是GET請求(這個過程需要你提供使用者名稱和密碼);

你網站的發起的請求:

驗證註冊資訊有效後響應(回覆授權碼):

(2)第二步、通過第一步獲取的授權碼以及client_id、client_secret、scope許可權範圍、redirect_uri換取token令牌

發起請求,注意這裡是POST請求(使用postman工具即可,注意資料部分放在body以鍵值對形式隔開,不是header部分):

回覆響應(json中的一個字串,這裡能配置refresh_token是因為表中設定了authorized_grant_types為“authorization_code,refresh_token”,也就是說可以獲取access_token並可以重新整理token):

access_token:2bccded3-1dd2-4f44-bb82-aea659c514ee

refresh_token: 2260389e-db44-4100-b05d-4f5af77e1802

(3)第三步、通過第二步獲取的access_token訪問你的網站獲取資訊(獲取的token在所有服務上都是可信的)

請求你的網站:

你的介面回覆:

該回復可以任意,根據你的需要設定;

(4)第四步、當access_token過期(如配置access_token過期1天,換取token的refresh_token則為30天,過期後29天之內都可以使用refresh_token換取access_token重新整理獲取新的令牌),此時必須帶上client_id、client_secret、refresh_token並指定grant_type為refresh_token, 重點中的重點(這裡坑了我3-4天時間):改請求必須以post請求傳送且在postman中放在body設定部分(header和body都可以設定鍵值對,沒看到,只設置了post方式,把給body的鍵值對寫在了header部分,慚愧!)

請求:

{
    "access_token": "6fc18d5d-4978-4c3d-8f9b-e2416b0e123c",
    "token_type": "bearer",
    "refresh_token": "2260389e-db44-4100-b05d-4f5af77e1802",
    "expires_in": 3599,
    "scope": "app"
}

回覆了一個新的token和以及沒有過期的refresh_token,過期應該會返回新的refresh_token;

四、問題

1、獲取授