1.分布式系統認證方案
1.1什么是分布式系統
隨着軟件環境和需求的變化 ,軟件的架構由單體結構演變為分布式架構,具有分布式架構的系統叫分布式系統,分布式系統的運行通常依賴網絡,它將單體結構的系統分為若干服務,服務之間通過網絡交互來完成用戶的業務處理,當前流行的微服務架構就是分布式系統架構,如下圖:
分布式系統具體如下基本特點:
- 分布性:每個部分都可以獨立部署,服務之間交互通過網絡進行通信,比如:訂單服務、商品服務。
2.伸縮性:每個部分都可以集群方式部署,並可針對部分結點進行硬件及軟件擴容,具有一定的伸縮能力。
3.共享性:每個部分都可以作為共享資源對外提供服務,多個部分可能有操作共享資源的情況。
4.開放性:每個部分根據需求都可以對外發布共享資源的訪問接口,並可允許第三方系統訪問。
1.2 分布式認證需求
分布式系統的每個服務都會有認證、授權的需求,如果每個服務都實現一套認證授權邏輯會非常冗余,考慮分布式 系統共享性的特點,需要由獨立的認證服務處理系統認證授權的請求;考慮分布式系統開放性的特點,不僅對系統 內部服務提供認證,對第三方系統也要提供認證。分布式認證的需求總結如下:
統一認證授權
提供獨立的認證服務,統一處理認證授權。
無論是不同類型的用戶,還是不同種類的客戶端(web端,H5、APP),均采用一致的認證、權限、會話機制,實現 統一認證授權。
要實現統一則認證方式必須可擴展,支持各種認證需求,比如:用戶名密碼認證、短信驗證碼、二維碼、人臉識別 等認證方式,並可以非常靈活的切換。
應用接入認證
應提供擴展和開放能力,提供安全的系統對接機制,並可開放部分API給接入第三方使用,一方應用(內部 系統服 務)和三方應用(第三方應用)均采用統一機制接入。
1.3 分布式認證方案
1.3.1 選型分析
1、基於session的認證方式
在分布式的環境下,基於session的認證會出現一個問題,每個應用服務都需要在session中存儲用戶身份信息,通 過負載均衡將本地的請求分配到另一個應用服務需要將session信息帶過去,否則會重新認證。
這個時候,通常的做法有下面幾種:
Session復制:多台應用服務器之間同步session,使session保持一致,對外透明。
Session黏貼:當用戶訪問集群中某台服務器后,強制指定后續所有請求均落到此機器上。
Session集中存儲:將Session存入分布式緩存中,所有服務器應用實例統一從分布式緩存中存取Session。
總體來講,基於session認證的認證方式,可以更好的在服務端對會話進行控制,且安全性較高。但是,session機 制方式基於cookie,在復雜多樣的移動客戶端上不能有效的使用,並且無法跨域,另外隨着系統的擴展需提高 session的復制、黏貼及存儲的容錯性。
2、基於token的認證方式
基於token的認證方式,服務端不用存儲認證數據,易維護擴展性強, 客戶端可以把token 存在任意地方,並且可 以實現web和app統一認證機制。其缺點也很明顯,token由於自包含信息,因此一般數據量較大,而且每次請求 都需要傳遞,因此比較占帶寬。另外,token的簽名驗簽操作也會給cpu帶來額外的處理負擔。
1.3.2 技術方案
根據 選型的分析,決定采用基於token的認證方式,它的優點是:
1.適合統一認證的機制,客戶端、一方應用、三方應用都遵循一致的認證機制。
2.token認證方式對第三方應用接入更適合,因為它更開放,可使用當前有流行的開放協議Oauth2.0、JWT等。
3.一般情況服務端無需存儲會話信息,減輕了服務端的壓力。
分布式系統認證技術方案見下圖:
流程描述:
(1)用戶通過接入方(應用)登錄,接入方采取OAuth2.0方式在統一認證服務(UAA)中認證。
(2)認證服務(UAA)調用驗證該用戶的身份是否合法,並獲取用戶權限信息。
(3)認證服務(UAA)獲取接入方權限信息,並驗證接入方是否合法。
(4)若登錄用戶以及接入方都合法,認證服務生成jwt令牌返回給接入方,其中jwt中包含了用戶權限及接入方權 限。
(5)后續,接入方攜帶jwt令牌對API網關內的微服務資源進行訪問。
(6)API網關對令牌解析、並驗證接入方的權限是否能夠訪問本次請求的微服務。
(7)如果接入方的權限沒問題,API網關將原請求header中附加解析后的明文Token,並將請求轉發至微服務。
(8)微服務收到請求,明文token中包含登錄用戶的身份和權限信息。因此后續微服務自己可以干兩件事:1,用 戶授權攔截(看當前用戶是否有權訪問該資源)2,將用戶信息存儲進當前線程上下文(有利於后續業務邏輯隨時 獲取當前用戶信息)
流程所涉及到UAA服務、API網關這三個組件職責如下:
1)統一認證服務(UAA)
它承載了OAuth2.0接入方認證、登入用戶的認證、授權以及生成令牌的職責,完成實際的用戶認證、授權功能。
2)API網關
作為系統的唯一入口,API網關為接入方提供定制的API集合,它可能還具有其它職責,如身份驗證、監控、負載均 衡、緩存等。API網關方式的核心要點是,所有的接入方和消費端都通過統一的網關接入微服務,在網關層處理所 有的非業務功能。
2 .OAuth2.0
2.1 OAuth2.0介紹
OAuth(開放授權)是一個開放標准,允許用戶授權第三方應用訪問他們存儲在另外的服務提供者上的信息,而不 需要將用戶名和密碼提供給第三方應用或分享他們數據的所有內容。OAuth2.0是OAuth協議的延續版本,但不向 后兼容OAuth 1.0即完全廢止了OAuth1.0。很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH認證服 務,這些都足以說明OAUTH標准逐漸成為開放資源授權的標准。
Oauth協議目前發展到2.0版本,1.0版本過於復雜,2.0版本已得到廣泛應用。
參考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin
Oauth協議:https://tools.ietf.org/html/rfc6749
下邊分析一個Oauth2認證的例子,通過例子去理解OAuth2.0協議的認證流程,本例子是程序員網站使用微信 認證的過程,這個過程的簡要描述如下:
1、客戶端請求第三方授權 用戶進入程序的登錄頁面,點擊微信的圖標以微信賬號登錄系統,用戶是自己在微信里信息的資源擁有者
點擊“微信”出現一個二維碼,此時用戶掃描二維碼,開始給網站授權。
2、資源擁有者同意給客戶端授權
資源擁有者掃描二維碼表示資源擁有者同意給客戶端授權,微信會對資源擁有者的身份進行驗證, 驗證通過后,微 信會詢問用戶是否給授權網站訪問自己的微信數據,用戶點擊“確認登錄”表示同意授權,微信認證服務器會 頒發一個授權碼,並重定向到網站。
3、客戶端獲取到授權碼,請求認證服務器申請令牌 此過程用戶看不到,客戶端應用程序請求認證服務器,請求攜帶授權碼。
4、認證服務器向客戶端響應令牌 微信認證服務器驗證了客戶端請求的授權碼,如果合法則給客戶端頒發令牌,令牌是客戶端訪問資源的通行證。 此交互過程用戶看不到,當客戶端拿到令牌后,用戶在黑馬程序員看到已經登錄成功。
5、客戶端請求資源服務器的資源 客戶端攜帶令牌訪問資源服務器的資源。 黑馬程序員網站攜帶令牌請求訪問微信服務器獲取用戶的基本信息。
6、資源服務器返回受保護資源 資源服務器校驗令牌的合法性,如果合法則向用戶響應資源信息內容。
以上認證授權詳細的執行流程如下:
通過上邊的例子我們大概了解了OAauth2.0的認證過程,下邊我們看OAuth2.0認證流程:
引自OAauth2.0協議rfc6749 https://tools.ietf.org/html/rfc6749
OAauth2.0包括以下角色:
1、客戶端
本身不存儲資源,需要通過資源擁有者的授權去請求資源服務器的資源,比如:Android客戶端、Web客戶端(瀏 覽器端)、微信客戶端等。
2、資源擁有者
通常為用戶,也可以是應用程序,即該資源的擁有者。
3、授權服務器(也稱認證服務器)
用於服務提供商對資源擁有的身份進行認證、對訪問資源進行授權,認證成功后會給客戶端發放令牌 (access_token),作為客戶端訪問資源服務器的憑據。本例為微信的認證服務器。
4、資源服務器
存儲資源的服務器,本例子為微信存儲的用戶信息。
現在還有一個問題,服務提供商能允許隨便一個客戶端就接入到它的授權服務器嗎?答案是否定的,服務提供商會 給准入的接入方一個身份,用於接入時的憑據:
client_id:客戶端標識
client_secret:客戶端秘鑰
因此,准確來說,授權服務器對兩種OAuth2.0中的兩個角色進行認證授權,分別是資源擁有者、客戶端。
2.2 Spring Cloud Security OAuth2
2.2.1 環境介紹
Spring-Security-OAuth2是對OAuth2的一種實現,並且跟我們之前學習的Spring Security相輔相成,與Spring Cloud體系的集成也非常便利,接下來,我們需要對它進行學習,最終使用它來實現我們設計的分布式認證授權解決方案。
OAuth2.0的服務提供方涵蓋兩個服務,即授權服務 (Authorization Server,也叫認證服務) 和資源服務 (Resource Server),使用 Spring Security OAuth2 的時候你可以選擇把它們在同一個應用程序中實現,也可以選擇建立使用 同一個授權服務的多個資源服務。
授權服務 (Authorization Server)應包含對接入端以及登入用戶的合法性進行驗證並頒發token等功能,對令牌 的請求端點由 Spring MVC 控制器進行實現,下面是配置一個認證服務必須要實現的endpoints:
- AuthorizationEndpoint 服務於認證請求。默認 URL:
/oauth/authorize
。 - TokenEndpoint 服務於訪問令牌的請求。默認 URL:
/oauth/token
。 資源服務 (Resource Server),應包含對資源的保護功能,對非法請求進行攔截,對請求中token進行解析鑒 權等,下面的過濾器用於實現 OAuth 2.0 資源服務 - OAuth2AuthenticationProcessingFilter用來對請求給出的身份令牌解析鑒權。
分別創建uaa授權服務(也可叫認證服務)和order訂單資源服務。
認證流程如下:
1、客戶端請求UAA授權服務進行認證。
2、認證通過后由UAA頒發令牌。
3、客戶端攜帶令牌Token請求資源服務。
4、資源服務校驗令牌的合法性,合法即返回資源信息。
2.2.2 環境搭建
2.2.2.1 父工程
創建maven工程作為父工程,依賴如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
2.2.2.2 創建UAA授權服務工程
1、創建distributed-security-uaa
創建distributed-security-uaa作為授權服務工程,依賴如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</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-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2、啟動類
本工程采用SpringBoot開發,每個工程編寫一個啟動類:
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableFeignClients(basePackages = {"com.itheima.security.distributed.uaa"})
public class UAAServer {
public static void main(String[] args) {
SpringApplication.run(UAAServer.class, args);
}
}
3、配置文件
在resources下創建application.properties
spring.application.name=uaa-service
server.port=53020
spring.main.allow-bean-definition-overriding = true
logging.level.root = debug
logging.level.org.springframework.web = info
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF-8
spring.http.encoding.force = true
server.tomcat.remote_ip_header = x-forwarded-for
server.tomcat.protocol_header = x-forwarded-proto
server.use-forward-headers = true
server.servlet.context-path = /uaa
spring.freemarker.enabled = true
spring.freemarker.suffix = .html
spring.freemarker.request-context-attribute = rc
spring.freemarker.content-type = text/html
spring.freemarker.charset = UTF-8
spring.mvc.throw-exception-if-no-handler-found = true
spring.resources.add-mappings = false
spring.datasource.url = jdbc:mysql://localhost:3306/user_db?useUnicode=true
spring.datasource.username = root
spring.datasource.password = mysql
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/
eureka.instance.preferIpAddress = true
eureka.instance.instance-id = ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env
feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime-types[0] = text/xml
feign.compression.request.mime-types[1] = application/xml
feign.compression.request.mime-types[2] = application/json
feign.compression.request.min-request-size = 2048
feign.compression.response.enabled = true
2.2.2.3 創建Order資源服務工程
本工程為Order訂單服務工程,訪問本工程的資源需要認證通過。
本工程的目的主要是測試認證授權的功能,所以不涉及訂單管理相關業務。
創建Order工程
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</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-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
配置文件
在resources中創建application.properties
spring.application.name=order-service
server.port=53021
spring.main.allow-bean-definition-overriding = true
logging.level.root = debug
logging.level.org.springframework.web = info
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF-8
spring.http.encoding.force = true
server.tomcat.remote_ip_header = x-forwarded-for
server.tomcat.protocol_header = x-forwarded-proto
server.use-forward-headers = true
server.servlet.context-path = /order
spring.freemarker.enabled = true
spring.freemarker.suffix = .html
spring.freemarker.request-context-attribute = rc
spring.freemarker.content-type = text/html
spring.freemarker.charset = UTF-8
spring.mvc.throw-exception-if-no-handler-found = true
spring.resources.add-mappings = false
eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/
eureka.instance.preferIpAddress = true
eureka.instance.instance-id = ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env
feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime-types[0] = text/xml
feign.compression.request.mime-types[1] = application/xml
feign.compression.request.mime-types[2] = application/json
feign.compression.request.min-request-size = 2048
feign.compression.response.enabled = true
啟動類
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServer {
public static void main(String[] args) {
SpringApplication.run(OrderServer.class, args);
}
}
2.2.2.授權服務器配置
2.2.2.1 EnableAuthorizationServer
可以用 @EnableAuthorizationServer 注解並繼承AuthorizationServerConfigurerAdapter來配置OAuth2.0 授權 服務器。
在Config包下創建AuthorizationServer:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Autowired
PasswordEncoder passwordEncoder;
//將客戶端信息存儲到數據庫
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
//客戶端詳情服務
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.withClientDetails(clientDetailsService);
/* clients.inMemory()// 使用in-memory存儲
.withClient("c1")// client_id
.secret(new BCryptPasswordEncoder().encode("secret"))//客戶端密鑰
.resourceIds("res1")//資源列表
.authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 該client允許的授權類型authorization_code,password,refresh_token,implicit,client_credentials
.scopes("all")// 允許的授權范圍
.autoApprove(false)//false跳轉到授權頁面
//加上驗證回調地址
.redirectUris("http://www.baidu.com")*/
;
}
//令牌管理服務
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service=new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService);//客戶端詳情服務
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存儲策略
//令牌增強
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); // 令牌默認有效期2小時
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默認有效期3天
return service;
}
//設置授權碼模式的授權碼如何存取,暫時采用內存方式
/* @Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
return new JdbcAuthorizationCodeServices(dataSource);//設置授權碼模式的授權碼如何存取
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)//認證管理器
.authorizationCodeServices(authorizationCodeServices)//授權碼服務
.tokenServices(tokenService())//令牌管理服務
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security){
security
.tokenKeyAccess("permitAll()") //oauth/token_key是公開
.checkTokenAccess("permitAll()") //oauth/check_token公開
.allowFormAuthenticationForClients() //表單認證(申請令牌)
;
}
}
AuthorizationServerConfigurerAdapter要求配置以下幾個類,這幾個類是由Spring創建的獨立的配置對象,它們 會被Spring傳入AuthorizationServerConfigurer中進行配置。
- ClientDetailsServiceConfigurer:用來配置客戶端詳情服務(ClientDetailsService),客戶端詳情信息在 這里進行初始化,你能夠把客戶端詳情信息寫死在這里或者是通過數據庫來存儲調取詳情信息。
- AuthorizationServerEndpointsConfigurer:用來配置令牌(token)的訪問端點和令牌服務(token services)。
- AuthorizationServerSecurityConfigurer:用來配置令牌端點的安全約束.
2.2.2.1.配置客戶端詳細信息
ClientDetailsServiceConfigurer 能夠使用內存或者JDBC來實現客戶端詳情服務(ClientDetailsService), ClientDetailsService負責查找ClientDetails,而ClientDetails有幾個重要的屬性如下列表:
- clientId:(必須的)用來標識客戶的Id。
- secret:(需要值得信任的客戶端)客戶端安全碼,如果有的話。
- scope:用來限制客戶端的訪問范圍,如果為空(默認)的話,那么客戶端擁有全部的訪問范圍。
- authorizedGrantTypes:此客戶端可以使用的授權類型,默認為空。
- authorities:此客戶端可以使用的權限(基於Spring Security authorities)。
客戶端詳情(Client Details)能夠在應用程序運行的時候進行更新,可以通過訪問底層的存儲服務(例如將客戶 端詳情存儲在一個關系數據庫的表中,就可以使用 JdbcClientDetailsService)或者通過自己實現 ClientRegistrationService接口(同時你也可以實現 ClientDetailsService 接口)來進行管理。
我們暫時使用內存方式存儲客戶端詳情信息,配置如下
2.2.2.2.管理令牌
AuthorizationServerTokenServices 接口定義了一些操作使得你可以對令牌進行一些必要的管理,令牌可以被用來 加載身份信息,里面包含了這個令牌的相關權限。
自己可以創建 AuthorizationServerTokenServices 這個接口的實現,則需要繼承 DefaultTokenServices 這個類, 里面包含了一些有用實現,你可以使用它來修改令牌的格式和令牌的存儲。默認的,當它嘗試創建一個令牌的時 候,是使用隨機值來進行填充的,除了持久化令牌是委托一個 TokenStore 接口來實現以外,這個類幾乎幫你做了 所有的事情。並且 TokenStore 這個接口有一個默認的實現,它就是 InMemoryTokenStore ,如其命名,所有的 令牌是被保存在了內存中。除了使用這個類以外,你還可以使用一些其他的預定義實現,下面有幾個版本,它們都 實現了TokenStore接口:
- InMemoryTokenStore:這個版本的實現是被默認采用的,它可以完美的工作在單服務器上(即訪問並發量 壓力不大的情況下,並且它在失敗的時候不會進行備份),大多數的項目都可以使用這個版本的實現來進行 嘗試,你可以在開發的時候使用它來進行管理,因為不會被保存到磁盤中,所以更易於調試。
- JdbcTokenStore:這是一個基於JDBC的實現版本,令牌會被保存進關系型數據庫。使用這個版本的實現時, 你可以在不同的服務器之間共享令牌信息,使用這個版本的時候請注意把"spring-jdbc"這個依賴加入到你的 classpath當中。
- JwtTokenStore:這個版本的全稱是 JSON Web Token(JWT),它可以把令牌相關的數據進行編碼(因此對 於后端服務來說,它不需要進行存儲,這將是一個重大優勢),但是它有一個缺點,那就是撤銷一個已經授 權令牌將會非常困難,所以它通常用來處理一個生命周期較短的令牌以及撤銷刷新令牌(refresh_token)。 另外一個缺點就是這個令牌占用的空間會比較大,如果你加入了比較多用戶憑證信息。JwtTokenStore 不會保存任何數據,但是它在轉換令牌值以及授權信息方面與 DefaultTokenServices 所扮演的角色是一樣的。
定義TokenConfig
在config包下定義TokenConfig,我們暫時先使用InMemoryTokenStore,生成一個普通的令牌
代碼:
@Configuration
public class TokenConfig {
private String SIGNING_KEY = "uaa123";
@Bean
public TokenStore tokenStore() {
//JWT令牌存儲方案
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //對稱秘鑰,資源服務器使用該秘鑰來驗證
return converter;
}
/* @Bean
public TokenStore tokenStore() {
//使用內存存儲令牌(普通令牌)
return new InMemoryTokenStore();
}*/
}
定義AuthorizationServerTokenServices
在AuthorizationServer中定義AuthorizationServerTokenServices
2.2.2.3.令牌訪問端點配置
AuthorizationServerEndpointsConfigurer 這個對象的實例可以完成令牌服務以及令牌endpoint配置。
配置授權類型(Grant Types)
AuthorizationServerEndpointsConfigurer 通過設定以下屬性決定支持的授權類型(Grant Types):
- authenticationManager:認證管理器,當你選擇了資源所有者密碼(password)授權類型的時候,請設置 這個屬性注入一個 AuthenticationManager 對象。
- userDetailsService:如果你設置了這個屬性的話,那說明你有一個自己的 UserDetailsService 接口的實現, 或者你可以把這個東西設置到全局域上面去(例如 GlobalAuthenticationManagerConfigurer 這個配置對 象),當你設置了這個之后,那么 "refresh_token" 即刷新令牌授權類型模式的流程中就會包含一個檢查,用 來確保這個賬號是否仍然有效,假如說你禁用了這個賬戶的話。
- authorizationCodeServices:這個屬性是用來設置授權碼服務的(即 AuthorizationCodeServices 的實例對 象),主要用於 "authorization_code" 授權碼類型模式。
- implicitGrantService:這個屬性用於設置隱式授權模式,用來管理隱式授權模式的狀態。
- tokenGranter:當你設置了這個東西(即 TokenGranter 接口實現),那么授權將會交由你來完全掌控,並 且會忽略掉上面的這幾個屬性,這個屬性一般是用作拓展用途的,即標准的四種授權模式已經滿足不了你的 需求的時候,才會考慮使用這個。
配置授權端點的URL(Endpoint URLs):
AuthorizationServerEndpointsConfigurer 這個配置對象有一個叫做 pathMapping() 的方法用來配置端點URL鏈 接,它有兩個參數:
- 第一個參數:String 類型的,這個端點URL的默認鏈接。
- 第二個參數:String 類型的,你要進行替代的URL鏈接。
以上的參數都將以 "/" 字符為開始的字符串,框架的默認URL鏈接如下列表,可以作為這個 pathMapping() 方法的 第一個參數:
- /oauth/authorize:授權端點。
- /oauth/token:令牌端點。
- /oauth/confirm_access:用戶確認授權提交端點。
- /oauth/error:授權服務錯誤信息端點。
- /oauth/check_token:用於資源服務訪問的令牌解析端點。
- /oauth/token_key:提供公有密匙的端點,如果你使用JWT令牌的話。
需要注意的是授權端點這個URL應該被Spring Security保護起來只供授權用戶訪問.
在AuthorizationServer配置令牌訪問端點
2.2.2.4.令牌端點的安全約束
AuthorizationServerSecurityConfigurer:用來配置令牌端點(Token Endpoint)的安全約束,在 AuthorizationServer中配置如下
(1)tokenkey這個endpoint當使用JwtToken且使用非對稱加密時,資源服務用於獲取公鑰而開放的,這里指這個 endpoint完全公開。
(2)checkToken這個endpoint完全公開
(3) 允許表單認證
授權服務配置總結:授權服務配置分成三大塊,可以關聯記憶。
既然要完成認證,它首先得知道客戶端信息從哪兒讀取,因此要進行客戶端詳情配置。
既然要頒發token,那必須得定義token的相關endpoint,以及token如何存取,以及客戶端支持哪些類型的 token。
既然暴露除了一些endpoint,那對這些endpoint可以定義一些安全上的約束等。
2.2.2.5 web安全配置
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//認證管理器
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//密碼編碼器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//安全攔截機制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/r/r1").hasAnyAuthority("p1")
.antMatchers("/login*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
;
}
}
2.2.3.授權碼模式
2.2.3.1 授權碼模式介紹
下圖是授權碼模式交互圖
(1)資源擁有者打開客戶端,客戶端要求資源擁有者給予授權,它將瀏覽器被重定向到授權服務器,重定向時會 附加客戶端的身份信息。如:
/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
參數列表如下:
- client_id:客戶端准入標識。
- response_type:授權碼模式固定為code。
- scope:客戶端權限。
- redirect_uri:跳轉uri,當授權碼申請成功后會跳轉到此地址,並在后邊帶上code參數(授權碼)。
(2)瀏覽器出現向授權服務器授權頁面,之后將用戶同意授權。
(3)授權服務器將授權碼(AuthorizationCode)轉經瀏覽器發送給client(通過redirect_uri)。
(4)客戶端拿着授權碼向授權服務器索要訪問access_token,請求如下:
/uaa/oauth/token? client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://www.baidu.com
參數列表如下
- client_id:客戶端准入標識。
- client_secret:客戶端秘鑰。
- grant_type:授權類型,填寫authorization_code,表示授權碼模式
- code:授權碼,就是剛剛獲取的授權碼,注意:授權碼只使用一次就無效了,需要重新申請。
- redirect_uri:申請授權碼時的跳轉url,一定和申請授權碼時用的redirect_uri一致。
(5)授權服務器返回令牌(access_token)
這種模式是四種模式中最安全的一種模式。一般用於client是Web服務器端應用或第三方的原生App調用資源服務 的時候。因為在這種模式中access_token不會經過瀏覽器或移動端的App,而是直接從服務端去交換,這樣就最大 限度的減小了令牌泄漏的風險。
2.2.3.2 測試
瀏覽器訪問認證頁面:
http://localhost:53020/uaa/oauth/authorize? client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
然后輸入模擬的賬號和密碼點登陸之后進入授權頁面:
確認授權后,瀏覽器會重定向到指定路徑(oauth_client_details表中的web_server_redirect_uri)並附加驗證碼? code=DB2mFj(每次不一樣),最后使用該驗證碼獲取token。
POST請求 http://localhost:53020/uaa/oauth/token 參數的值配置好
2.2.4.簡化模式
2.2.4.1 簡化模式介紹
下圖是簡化模式交互圖:
(1)資源擁有者打開客戶端,客戶端要求資源擁有者給予授權,它將瀏覽器被重定向到授權服務器,重定向時會 附加客戶端的身份信息。如:
/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
參數描述同授權碼模式 ,注意response_type=token,說明是簡化模式。
(2)瀏覽器出現向授權服務器授權頁面,之后將用戶同意授權。
(3)授權服務器將授權碼將令牌(access_token)以Hash的形式存放在重定向uri的fargment中發送給瀏覽 器。
注:fragment 主要是用來標識 URI 所標識資源里的某個資源,在 URI 的末尾通過 (#)作為 fragment 的開頭, 其中 # 不屬於 fragment 的值。如https://domain/index#L18這個 URI 中 L18
就是 fragment 的值。大家只需要 知道js通過響應瀏覽器地址欄變化的方式能獲取到fragment 就行了。
一般來說,簡化模式用於沒有服務器端的第三方單頁面應用,因為沒有服務器端就無法接收授權碼。
2.2.4.2 測試
確認授權后,瀏覽器會重定向到指定路徑(oauth_client_details表中的web_server_redirect_uri)並以Hash的形 式存放在重定向uri的fargment中,如:
2.2.5.密碼模式
2.2.5.1 授權碼模式介紹
(1)資源擁有者將用戶名、密碼發送給客戶端
(2)客戶端拿着資源擁有者的用戶名、密碼向授權服務器請求令牌(access_token),請求如下:
/uaa/oauth/token? client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123
參數列表如下:
client_id:客戶端准入標識。
client_secret:客戶端秘鑰。
grant_type:授權類型,填寫password表示密碼模式
username:資源擁有者用戶名。
password:資源擁有者密碼。
(3)授權服務器將令牌(access_token)發送給client
這種模式十分簡單,但是卻意味着直接將用戶敏感信息泄漏給了client,因此這就說明這種模式只能用於client是我 們自己開發的情況下。因此密碼模式一般用於我們自己開發的,第一方原生App或第一方單頁面應用。
2.2.5.2 測試
POST請求 http://localhost:53020/uaa/oauth/token 加上那幾個參數
返回結果:
2.2.6.客戶端模式
2.2.6.1 客戶端模式介紹
(1)客戶端向授權服務器發送自己的身份信息,並請求令牌(access_token)
(2)確認客戶端身份無誤后,將令牌(access_token)發送給client,請求如下:
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
參數列表如下:
client_id:客戶端准入標識。
client_secret:客戶端秘鑰。
grant_type:授權類型,填寫client_credentials表示客戶端模式
這種模式是最方便但最不安全的模式。因此這就要求我們對client完全的信任,而client本身也是安全的。因 此這種模式一般用來提供給我們完全信任的服務器端服務。比如,合作方系統對接,拉取一組用戶信息。
2.2.6.2 客戶端模式測試
POST http://localhost:53020/uaa/oauth/token 加上參數
2.2.7.資源服務測試
2.2.7.1 資源服務器配置
@EnableResourceServer 注解到一個 @Configuration 配置類上,並且必須使用 ResourceServerConfigurer 這個 配置對象來進行配置(可以選擇繼承自 ResourceServerConfigurerAdapter 然后覆寫其中的方法,參數就是這個 對象的實例),下面是一些可以配置的屬性:
ResourceServerSecurityConfigurer中主要包括:
- tokenServices:ResourceServerTokenServices 類的實例,用來實現令牌服務。
- tokenStore:TokenStore類的實例,指定令牌如何訪問,與tokenServices配置可選
- resourceId:這個資源服務的ID,這個屬性是可選的,但是推薦設置並在授權服務中進行驗證。
- 其他的拓展屬性例如 tokenExtractor 令牌提取器用來提取請求中的令牌。
HttpSecurity配置這個與Spring Security類似:
- 請求匹配器,用來設置需要進行保護的資源路徑,默認的情況下是保護資源服務的全部路徑。
- 通過http.authorizeRequests()來設置受保護資源的訪問規則
- 其他的自定義權限保護規則通過 HttpSecurity 來進行配置。
@EnableResourceServer 注解自動增加了一個類型為 OAuth2AuthenticationProcessingFilter 的過濾器鏈
編寫ResouceServerConfig:
@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "res1";
@Autowired
TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)//資源 id
.tokenStore(tokenStore)
// .tokenServices(tokenService())//驗證令牌的服務
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").access("#oauth2.hasScope('all')")
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
2.2.7.2 驗證token
ResourceServerTokenServices 是組成授權服務的另一半,如果你的授權服務和資源服務在同一個應用程序上的 話,你可以使用 DefaultTokenServices ,這樣的話,你就不用考慮關於實現所有必要的接口的一致性問題。如果 你的資源服務器是分離開的,那么你就必須要確保能夠有匹配授權服務提供的 ResourceServerTokenServices,它 知道如何對令牌進行解碼。
令牌解析方法: 使用 DefaultTokenServices 在資源服務器本地配置令牌存儲、解碼、解析方式 使用 RemoteTokenServices 資源服務器通過 HTTP 請求來解碼令牌,每次都請求授權服務器端點 /oauth/check_token
使用授權服務的 /oauth/check_token 端點你需要在授權服務將這個端點暴露出去,以便資源服務可以進行訪問, 這在咱們授權服務配置中已經提到了,下面是一個例子,在這個例子中,我們在授權服務中配置了 /oauth/check_token 和 /oauth/token_key 這兩個端點:
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")// /oauth/token_key 安全配置
.checkTokenAccess("permitAll()") // /oauth/check_token 安全配置
}
在資源 服務配置RemoteTokenServices ,在ResouceServerConfig中配置:
2.2.7.3編寫資源
在controller包下編寫OrderController,此controller表示訂單資源的訪問類:
@RestController
public class OrderController {
@GetMapping(value = "/r1")
@PreAuthorize("hasAuthority('p1')")//擁有p1權限方可訪問此url
public String r1() {
//獲取用戶身份信息
UserDTO userDTO = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userDTO.getFullname() + "訪問資源1";
}
}
2.2.7.4 添加安全訪問控制
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//安全攔截機制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
// .antMatchers("/r/r1").hasAuthority("p2")
// .antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/**").authenticated()//所有/r/**的請求必須認證通過
.anyRequest().permitAll()//除了/r/**,其它的請求可以訪問
;
}
}
2.2.7.5 測試
1、申請令牌 這里我們使用密碼方式
2、請求資源
按照oauth2.0協議要求,請求資源需要攜帶token,如下:
token的參數名稱為:Authorization,值為:Bearer token值
如果token錯誤,則授權失敗,如下:
2.3 JWT令牌
2.3.1 JWT介紹
通過上邊的測試我們發現,當資源服務和授權服務不在一起時資源服務使用RemoteTokenServices 遠程請求授權 服務驗證token,如果訪問量較大將會影響系統的性能。
解決上邊問題:
令牌采用JWT格式即可解決上邊的問題,用戶認證通過會得到一個JWT令牌,JWT令牌中已經包括了用戶相關的信 息,客戶端只需要攜帶JWT訪問資源服務,資源服務根據事先約定的算法自行完成令牌校驗,無需每次都請求認證 服務完成授權。
1、什么是JWT?
JSON Web Token(JWT)是一個開放的行業標准(RFC 7519),它定義了一種簡介的、自包含的協議格式,用於 在通信雙方傳遞json對象,傳遞的信息經過數字簽名可以被驗證和信任。JWT可以使用HMAC算法或使用RSA的公 鑰/私鑰對來簽名,防止被篡改。
官網:https://jwt.io/
標准:https://tools.ietf.org/html/rfc7519
JWT令牌的優點:
1)jwt基於json,非常方便解析。
2)可以在令牌中自定義豐富的內容,易擴展。
3)通過非對稱加密算法及數字簽名技術,JWT防止篡改,安全性高。
4)資源服務使用JWT可不依賴認證服務即可完成授權。
缺點:
1)JWT令牌較長,占存儲空間比較大。
2、JWT令牌結構
通過學習JWT令牌結構為自定義jwt令牌打好基礎。
JWT令牌由三部分組成,每部分中間使用點(.)分隔,比如:xxxxx.yyyyy.zzzzz
- Header
頭部包括令牌的類型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)
一個例子如下:
下邊是Header部分的內容
{
"alg": "HS256",
"typ": "JWT"
}
將上邊的內容使用Base64Url編碼,得到一個字符串就是JWT令牌的第一部分。
- Payload
第二部分是負載,內容也是一個json對象,它是存放有效信息的地方,它可以存放jwt提供的現成字段,比 如:iss(簽發者),exp(過期時間戳), sub(面向的用戶)等,也可自定義字段。
此部分不建議存放敏感信息,因為此部分可以解碼還原原始內容。
最后將第二部分負載使用Base64Url編碼,得到一個字符串就是JWT令牌的第二部分。
一個例子:
{
"sub": "1234567890",
"name": "456",
"admin": true
}
- Signature
第三部分是簽名,此部分用於防止jwt內容被篡改。
這個部分使用base64url將前兩部分進行編碼,編碼后使用點(.)連接組成字符串,最后使用header中聲明 簽名算法進行簽名。
一個例子:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:簽名所使用的密鑰。
2.3.2 配置JWT令牌服務
在uaa中配置jwt令牌服務,即可實現生成jwt格式的令牌。
1、TokenConfig
@Configuration
public class TokenConfig {
private String SIGNING_KEY = "uaa123";
@Bean
public TokenStore tokenStore() {
//JWT令牌存儲方案
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //對稱秘鑰,資源服務器使用該秘鑰來驗證
return converter;
}
/* @Bean
public TokenStore tokenStore() {
//使用內存存儲令牌(普通令牌)
return new InMemoryTokenStore();
}*/
}
2、定義JWT令牌服務
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Autowired
PasswordEncoder passwordEncoder;
//將客戶端信息存儲到數據庫
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
//客戶端詳情服務
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.withClientDetails(clientDetailsService);
/* clients.inMemory()// 使用in-memory存儲
.withClient("c1")// client_id
.secret(new BCryptPasswordEncoder().encode("secret"))//客戶端密鑰
.resourceIds("res1")//資源列表
.authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 該client允許的授權類型authorization_code,password,refresh_token,implicit,client_credentials
.scopes("all")// 允許的授權范圍
.autoApprove(false)//false跳轉到授權頁面
//加上驗證回調地址
.redirectUris("http://www.baidu.com")*/
;
}
//令牌管理服務
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service=new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService);//客戶端詳情服務
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存儲策略
//令牌增強
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); // 令牌默認有效期2小時
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默認有效期3天
return service;
}
//設置授權碼模式的授權碼如何存取,暫時采用內存方式
/* @Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
return new JdbcAuthorizationCodeServices(dataSource);//設置授權碼模式的授權碼如何存取
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)//認證管理器
.authorizationCodeServices(authorizationCodeServices)//授權碼服務
.tokenServices(tokenService())//令牌管理服務
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security){
security
.tokenKeyAccess("permitAll()") //oauth/token_key是公開
.checkTokenAccess("permitAll()") //oauth/check_token公開
.allowFormAuthenticationForClients() //表單認證(申請令牌)
;
}
}
2.3.3 生成jwt令牌
2.3.4 校驗jwt令牌
資源服務需要和授權服務擁有一致的簽字、令牌服務等:
1、將授權服務中的TokenConfig類拷貝到資源 服務中
2、屏蔽資源 服務原來的令牌服務類
@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "res1";
@Autowired
TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)//資源 id
.tokenStore(tokenStore)
// .tokenServices(tokenService())//驗證令牌的服務
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").access("#oauth2.hasScope('ROLE_ADMIN')")
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
//資源服務令牌解析服務
/* @Bean
public ResourceServerTokenServices tokenService() {
//使用遠程服務請求授權服務器校驗token,必須指定校驗token 的url、client_id,client_secret
RemoteTokenServices service=new RemoteTokenServices();
service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
service.setClientId("c1");
service.setClientSecret("secret");
return service;
}*/
}
3、測試
1)申請jwt令牌
2)使用令牌請求資源
小技巧: 令牌申請成功可以使用/uaa/oauth/check_token校驗令牌的有效性,並查詢令牌的內容
2.4 完善環境配置
目前客戶端信息和授權碼仍然存儲在內存中,生產環境中通過會存儲在數據庫中,下邊完善環境的配置:
2.4.1 創建表
在user_db中創建如下表:
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '客戶端標 識',
`resource_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '接入資源列表',
`client_secret` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '客戶端秘鑰',
`scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorized_grant_types` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`web_server_redirect_uri` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorities` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`access_token_validity` int(11) NULL DEFAULT NULL,
`refresh_token_validity` int(11) NULL DEFAULT NULL,
`additional_information` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
`archived` tinyint(4) NULL DEFAULT NULL,
`trusted` tinyint(4) NULL DEFAULT NULL,
`autoapprove` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '接入客戶端信息' ROW_FORMAT = Dynamic;
INSERT INTO `oauth_client_details` VALUES ('c1', 'res1', '$2a$10$NlBC84MVb7F95EXYTXwLneXgCca6/GipyWR5NHm8K0203bSQMLpvm', 'ROLE_ADMIN,ROLE_USER,ROLE_API', 'client_credentials,password,authorization_code,implicit,refresh_token', 'http://www.baidu.com', NULL, 7200, 259200, NULL, '2019‐09‐09 16:04:28', 0, 0, 'false');
INSERT INTO `oauth_client_details` VALUES ('c2', 'res2', '$2a$10$NlBC84MVb7F95EXYTXwLneXgCca6/GipyWR5NHm8K0203bSQMLpvm', 'ROLE_API', 'client_credentials,password,authorization_code,implicit,refresh_token', 'http://www.baidu.com', NULL, 31536000, 2592000, NULL, '2019‐09‐09 21:48:51', 0, 0, 'false');
oauth_code表,Spring Security OAuth2使用,用來存儲授權碼:
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
`code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authentication` blob NULL,
INDEX `code_index`(`code`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
2.4.2 配置授權服務
(1)修改AuthorizationServer:
ClientDetailsService和AuthorizationCodeServices從數據庫讀取數據。
2.4.3測試
1、測試申請令牌
使用密碼模式申請令牌,客戶端信息需要和數據庫中的信息一致。
2、測試授權碼模式
生成的授權存儲到數據庫中。
3.Spring Security實現分布式系統授權
3.1 需求分析
回顧技術方案如下:
1、UAA認證服務負責認證授權。
2、所有請求經過 網關到達微服務
3、網關負責鑒權客戶端以及請求轉發
4、網關將token解析后傳給微服務,微服務進行授權。
3.2.注冊中心
所有微服務的請求都經過網關,網關從注冊中心讀取微服務的地址,將請求轉發至微服務。
完成注冊中心的搭建,注冊中心采用Eureka。
1、創建maven工程
2、pom.xml依賴如下
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
3、配置文件
在resources中配置application.yml
spring:
application:
name: distributed-discovery
server:
port: 53000 #啟動端口
eureka:
server:
enable-self-preservation: false #關閉服務器自我保護,客戶端心跳檢測15分鍾內錯誤達到80%服務會保護,導致別人還認為是好用的服務
eviction-interval-timer-in-ms: 10000 #清理間隔(單位毫秒,默認是60*1000)5秒將客戶端剔除的服務在服務注冊列表中剔除#
shouldUseReadOnlyResponseCache: true #eureka是CAP理論種基於AP策略,為了保證強一致性關閉此切換CP 默認不關閉 false關閉
client:
register-with-eureka: false #false:不作為一個客戶端注冊到注冊中心
fetch-registry: false #為true時,可以啟動,但報異常:Cannot execute request on any known server
instance-info-replication-interval-seconds: 10
serviceUrl:
defaultZone: http://localhost:${server.port}/eureka/
instance:
hostname: ${spring.cloud.client.ip-address}
prefer-ip-address: true
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
啟動類:
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServer {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServer.class,args);
}
}
3.3.網關
網關整合 OAuth2.0 有兩種思路,一種是認證服務器生成jwt令牌, 所有請求統一在網關層驗證,判斷權限等操作; 另一種是由各資源服務處理,網關只做請求轉發。
我們選用第一種。我們把API網關作為OAuth2.0的資源服務器角色,實現接入客戶端權限攔截、令牌解析並轉發當 前登錄用戶信息(jsonToken)給微服務,這樣下游微服務就不需要關心令牌格式解析以及OAuth2.0相關機制了。
API網關在認證授權體系里主要負責兩件事:
(1)作為OAuth2.0的資源服務器角色,實現接入方權限攔截。
(2)令牌解析並轉發當前登錄用戶信息(明文token)給微服務
微服務拿到明文token(明文token中包含登錄用戶的身份和權限信息)后也需要做兩件事:
(1)用戶授權攔截(看當前用戶是否有權訪問該資源)
(2)將用戶信息存儲進當前線程上下文(有利於后續業務邏輯隨時獲取當前用戶信息)
3.3.1 創建工程模塊
pom.xml:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</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-netflix-zuul</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-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
配置application.properties
spring.application.name=gateway-server
server.port=53010
spring.main.allow-bean-definition-overriding = true
logging.level.root = info
logging.level.org.springframework = info
zuul.retryable = true
zuul.ignoredServices = *
zuul.add-host-header = true
zuul.sensitiveHeaders = *
zuul.routes.uaa-service.stripPrefix = false
zuul.routes.uaa-service.path = /uaa/**
zuul.routes.order-service.stripPrefix = false
zuul.routes.order-service.path = /order/**
eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/
eureka.instance.preferIpAddress = true
eureka.instance.instance-id = ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env
feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime-types[0] = text/xml
feign.compression.request.mime-types[1] = application/xml
feign.compression.request.mime-types[2] = application/json
feign.compression.request.min-request-size = 2048
feign.compression.response.enabled = true
統一認證服務(UAA)與統一用戶服務都是網關下微服務,需要在網關上新增路由配置:
上面配置了網關接收的請求url若符合/order/**表達式,將被被轉發至order-service(統一用戶服務)。
啟動類:
@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
public class GatewayServer {
public static void main(String[] args) {
SpringApplication.run(GatewayServer.class, args);
}
}
3.3.2 token配置
前面也介紹了,資源服務器由於需要驗證並解析令牌,往往可以通過在授權服務器暴露check_token的Endpoint來 完成,而我們在授權服務器使用的是對稱加密的jwt,因此知道密鑰即可,資源服務與授權服務本就是對稱設計, 那我們把授權服務的TokenConfig兩個類拷貝過來就行 。
@Configuration
public class TokenConfig {
private String SIGNING_KEY = "uaa123";
@Bean
public TokenStore tokenStore() {
//JWT令牌存儲方案
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //對稱秘鑰,資源服務器使用該秘鑰來驗證
return converter;
}
/* @Bean
public TokenStore tokenStore() {
//使用內存存儲令牌(普通令牌)
return new InMemoryTokenStore();
}*/
}
3.3.3 配置資源服務
在ResouceServerConfig中定義資源服務配置,主要配置的內容就是定義一些匹配規則,描述某個接入客戶端需要 什么樣的權限才能訪問某個微服務,如:
@Configuration
public class ResouceServerConfig {
public static final String RESOURCE_ID = "res1";
//uaa資源服務配置
@Configuration
@EnableResourceServer
public class UAAServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/uaa/**").permitAll();
}
}
//order資源
//uaa資源服務配置
@Configuration
@EnableResourceServer
public class OrderServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/order/**").access("#oauth2.hasScope('ROLE_API')");
}
}
//配置其它的資源服務..
}
上面定義了兩個微服務的資源,其中:
UAAServerConfig指定了若請求匹配/uaa/網關不進行攔截。
OrderServerConfig指定了若請求匹配/order/,也就是訪問統一用戶服務,接入客戶端需要有scope中包含 read,並且authorities(權限)中需要包含ROLE_USER。
由於res1這個接入客戶端,read包括ROLE_ADMIN,ROLE_USER,ROLE_API三個權限。
3.3.4 安全配置
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.and().csrf().disable();
}
}
3.3.5轉發明文token給微服務
通過Zuul過濾器的方式實現,目的是讓下游微服務能夠很方便的獲取到當前的登錄用戶信息(明文token)
(1)實現Zuul前置過濾器,完成當前登錄用戶信息提取,並放入轉發微服務的request中
public class AuthFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
return true;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
//從安全上下文中拿 到用戶身份對象
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(!(authentication instanceof OAuth2Authentication)){
return null;
}
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
//取出用戶身份信息
String principal = userAuthentication.getName();
//取出用戶權限
List<String> authorities = new ArrayList<>();
//從userAuthentication取出權限,放在authorities
userAuthentication.getAuthorities().stream().forEach(c->authorities.add(((GrantedAuthority) c).getAuthority()));
OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
Map<String,Object> jsonToken = new HashMap<>(requestParameters);
if(userAuthentication!=null){
jsonToken.put("principal",principal);
jsonToken.put("authorities",authorities);
}
//把身份信息和權限信息放在json中,加入http的header中,轉發給微服務
ctx.addZuulRequestHeader("json-token", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));
return null;
}
}
工具類:
public class EncryptUtil {
private static final Logger logger = LoggerFactory.getLogger(EncryptUtil.class);
public static String encodeBase64(byte[] bytes){
String encoded = Base64.getEncoder().encodeToString(bytes);
return encoded;
}
public static byte[] decodeBase64(String str){
byte[] bytes = null;
bytes = Base64.getDecoder().decode(str);
return bytes;
}
public static String encodeUTF8StringBase64(String str){
String encoded = null;
try {
encoded = Base64.getEncoder().encodeToString(str.getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
logger.warn("不支持的編碼格式",e);
}
return encoded;
}
public static String decodeUTF8StringBase64(String str){
String decoded = null;
byte[] bytes = Base64.getDecoder().decode(str);
try {
decoded = new String(bytes,"utf-8");
}catch(UnsupportedEncodingException e){
logger.warn("不支持的編碼格式",e);
}
return decoded;
}
public static String encodeURL(String url) {
String encoded = null;
try {
encoded = URLEncoder.encode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.warn("URLEncode失敗", e);
}
return encoded;
}
public static String decodeURL(String url) {
String decoded = null;
try {
decoded = URLDecoder.decode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.warn("URLDecode失敗", e);
}
return decoded;
}
public static void main(String [] args){
String str = "abcd{'a':'b'}";
String encoded = EncryptUtil.encodeUTF8StringBase64(str);
String decoded = EncryptUtil.decodeUTF8StringBase64(encoded);
System.out.println(str);
System.out.println(encoded);
System.out.println(decoded);
String url = "== wo";
String urlEncoded = EncryptUtil.encodeURL(url);
String urlDecoded = EncryptUtil.decodeURL(urlEncoded);
System.out.println(url);
System.out.println(urlEncoded);
System.out.println(urlDecoded);
}
}
(2)將filter納入spring 容器:
配置AuthFilter
@Configuration
public class ZuulConfig {
@Bean
public AuthFilter preFileter() {
return new AuthFilter();
}
@Bean
public FilterRegistrationBean corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setMaxAge(18000L);
source.registerCorsConfiguration("/**", config);
CorsFilter corsFilter = new CorsFilter(source);
FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
3.4 微服務用戶鑒權攔截
當微服務收到明文token時,應該怎么鑒權攔截呢?自己實現一個filter?自己解析明文token,自己定義一套資源 訪問策略?能不能適配Spring Security呢,是不是突然想起了前面我們實現的Spring Security基於token認證例子。咱們還拿 統一用戶服務作為網關下游微服務,對它進行改造,增加微服務用戶鑒權攔截功
(1)增加測試資源
OrderController增加以下endpoint
@PreAuthorize("hasAuthority('p1')")
@GetMapping(value = "/r1")
public String r1() {
UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return user.getUsername() + "訪問資源1";
}
@PreAuthorize("hasAuthority('p2')")
@GetMapping(value = "/r2")
public String r2() {//通過Spring Security API獲取當前登錄用戶
UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return user.getUsername() + "訪問資源2";
}
(2)Spring Security配置
開啟方法保護,並增加Spring配置策略,除了/login方法不受保護(統一認證要調用),其他資源全部需要認證才能訪問。
在ResouceServerConfig 配置
綜合上面的配置,咱們共定義了三個資源了,擁有p1權限可以訪問r1資源,擁有p2權限可以訪問r2資源,只要認 證通過就能訪問r3資源。
(3)定義filter攔截token,並形成Spring Security的Authentication對象
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//解析出頭中的token
String token = httpServletRequest.getHeader("json-token");
if(token!=null){
String json = EncryptUtil.decodeUTF8StringBase64(token);
//將token轉成json對象
JSONObject jsonObject = JSON.parseObject(json);
//用戶身份信息
// UserDTO userDTO = new UserDTO();
// String principal = jsonObject.getString("principal");
// userDTO.setUsername(principal);
UserDTO userDTO = JSON.parseObject(jsonObject.getString("principal"), UserDTO.class);
//用戶權限
JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);
//將用戶信息和權限填充 到用戶身份token對象中
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
//將authenticationToken填充到安全上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
經過上邊的過慮 器,資源 服務中就可以方便到的獲取用戶的身份信息:
@Data
public class UserDTO {
/**
* 用戶id
*/
private String id;
/**
* 用戶名
*/
private String username;
/**
* 手機號
*/
private String mobile;
/**
* 姓名
*/
private String fullname;
}
@GetMapping(value = "/r1")
@PreAuthorize("hasAuthority('p1')")//擁有p1權限方可訪問此url
public String r1() {
//獲取用戶身份信息
UserDTO userDTO = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userDTO.getFullname() + "訪問資源1";
}
還是三個步驟:
1.解析token
2.新建並填充authentication
3.將authentication保存進安全上下文
剩下的事兒就交給Spring Security好了。
3.5.集成測試
測試過程描述:
1、采用OAuth2.0的密碼模式從UAA獲取token
2、使用該token通過網關訪問訂單服務的測試資源
(1)過網關訪問uaa的授權及獲取令牌,獲取token。注意端口是53010,網關的端口。
如授權endpoint:
http://localhost:53010/uaa/oauth/authorize?response_type=code&client_id=c1
令牌endpoint
http://localhost:53010/uaa/oauth/token
(2)使用Token過網關訪問訂單服務中的r1-r2測試資源進行測試
結果:
使用張三token訪問p1,訪問成功
使用張三token訪問p2,訪問失敗
使用李四token訪問p1,訪問失敗
使用李四token訪問p2,訪問成功
符合預期結果
(3)破壞token測試
無token測試返回內容:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
破壞token測試返回內容:
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
3.6 擴展用戶信息
3.6.1 需求分析
目前jwt令牌存儲了用戶的身份信息、權限信息,網關將token明文化轉發給微服務使用,目前用戶身份信息僅包括 了用戶的賬號,微服務還需要用戶的ID、手機號等重要信息
所以,本案例將提供擴展用戶信息的思路和方法,滿足微服務使用用戶信息的需求。
下邊分析JWT令牌中擴展用戶信息的方案:
在認證階段DaoAuthenticationProvider會調用UserDetailService查詢用戶的信息,這里是可以獲取到齊全的用戶 信息的。由於JWT令牌中用戶身份信息來源於UserDetails,UserDetails中僅定義了username為用戶的身份信息, 這里有兩個思路:第一是可以擴展UserDetails,使之包括更多的自定義屬性,第二也可以擴展username的內容 ,比如存入json數據內容作為username的內容。相比較而言,方案二比較簡單還不用破壞UserDetails的結構,我 們采用方案二。
3.6.2 修改UserDetailService
從數據庫查詢到user,將整體user轉成json存入userDetails對象
3.6.3 修改資源服務過慮器
資源服務中的過慮 器負責 從header中解析json-token,從中即可拿網關放入的用戶身份信息,部分關鍵代碼如下:
if(token!=null){
//1.解析token
String json=EncryptUtil.decodeUTF8StringBase64(token);
JSONObject userJson=JSON.parseObject(json);
//取出用戶身份信息
String principal=userJson.getString("principal");
//將json轉成對象
UserDTO userDTO=JSON.parseObject(principal,UserDTO.class);
SONArray authoritiesArray=userJson.getJSONArray("authorities");
...
以上過程就完成自定義用戶身份信息的方案。