SpringCloud+Spring Security OAuth2 實現微服務統一認證授權

目前正在做了一個基於Spring Cloud的微服務項目,現在的好多項目都是基於APP移動端以及前後端分離的項目,之前基於Session的前後端放到一起的項目已經慢慢失寵並淡出我們視線,尤其是當基於SpringCloud的微服務架構以及Vue、React單頁面應用流行起來,爲此基於前後端分離的項目用戶認證也受到衆人關注的一個焦點,我們先來聊一聊在分佈式項目認證需求以及解決方案。

分佈式認證需求

​ 分佈式系統的每個服務都會有認證、授權的需求,如果每個服務都實現一套認證授權邏輯會非常冗餘,考慮分佈式系統共享性的特點,需要由獨立的認證服務處理系統認證授權的請求;考慮分佈式系統開放性的特點,不僅對系統內部服務提供認證,對第三方系統也要提供認證。分佈式認證的需求總結如下:

統一認證授權

​ 提供獨立的認證服務,統一處理認證授權。

​ 無論是不同類型的用戶,還是不同種類的客戶端(web端,H5、APP),均採用一致的認證、權限、會話機制,實現統一認證授權。

​ 要實現統一則認證方式必須可擴展,支持各種認證需求,比如:用戶名密碼認證、短信驗證碼、二維碼、人臉識別等認證方式,並可以非常靈活的切換

應用接入認證

​ 應提供擴展和開放能力,提供安全的系統對接機制,並可開放部分API給接入第三方使用,一方應用(內部 系統服務)和三方應用(第三方應用)均採用統一機制接入。

分佈式認證方案

基於session的認證方式

​ 在分佈式的環境下,基於session的認證會出現一個問題,每個應用服務都需要在session中存儲用戶身份信息,通過負載均衡將本地的請求分配到另一個應用服務需要將session信息帶過去,否則會重新認證。

這個時候,通常的做法有下面幾種:

  • Session複製:多臺應用服務器之間同步session,使session保持一致,對外透明
  • Session黏貼:當用戶訪問集羣中某臺服務器後,強制指定後續所有請求均落到此機器上。
  • Session集中存儲:將Session存入分佈式緩存中,所有服務器應用實例統一從分佈式緩存中存取Session。

總體來講,基於session認證的認證方式,可以更好的在服務端對會話進行控制,且安全性較高。但是,session機制方式基於cookie,在複雜多樣的移動客戶端上不能有效的使用,並且無法跨域,另外隨着系統的擴展需提高session的複製、黏貼及存儲的容錯性。

基於token的認證方式

​ 基於token的認證方式,服務端不用存儲認證數據,易維護擴展性強, 客戶端可以把token 存在任意地方,並且可以實現web和app統一認證機制。其缺點也很明顯,token由於自包含信息,因此一般數據量較大,而且每次請求都需要傳遞,因此比較佔帶寬。另外,token的簽名驗籤操作也會給cpu帶來額外的處理負擔。

在分析完這兩種認證方式之後,決定採用基於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網關方式的核心要點是,所有的接入方和消費端都通過統一的網關接入微服務,在網關層處理所有的非業務功能。

OAuth2.0介紹

OAuth(開放授權)是一個開放標準,允許用戶授權第三方應用訪問他們存儲在另外的服務提供者上的信息,而不需要將用戶名和密碼提供給第三方應用或分享他們數據的所有內容。OAuth2.0是OAuth協議的延續版本,但不向後兼容OAuth 1.0即完全廢止了OAuth1.0。很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH認證服務,這些都足以說明OAUTH標準逐漸成爲開放資源授權的標準。

Oauth協議:https://tools.ietf.org/html/rfc6749

關於OAuth2.0的詳細介紹,可以去看 阮一峯老師的理解OAuth 2.0

學習中參考的資料:深入理解Spring Cloud Security OAuth2及JWT

準備工作:

項目結構
在這裏插入圖片描述

  • distributed-security -------父工程 (控制版本)

    • distributed-security-order ------訂單服務 (本工程的目的主要是測試認證授權的功能,不涉及訂單管理相關業務。)

    • distributed-security-uaa ------授權服務

    • distributed-security-discovery ------Eureka服務註冊中心

    • distributed-security-gateway ------Zuul網關

首先我們來編寫授權服務

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>distributed-security</artifactId>
        <groupId>com.example</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>distributed-security-uaa</artifactId>


    <dependencies>
        <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>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        </dependency>
    </dependencies>

</project>

接下來配置認證服務器AuthorizationServer,並添加@Configuration和@EnableAuthorizationServer註解並繼承AuthorizationServerConfigurerAdapter來配置OAuth2.0 授權服務器。

@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;

    //密碼編碼器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
	
    //ClientDetailsService和AuthorizationCodeServices從數據庫讀取數據。
    //將客戶端信息存儲到數據庫
    @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")
                ;*/
    }


    //令牌管理服務
    //AuthorizationServerTokenServices 接口定義了一些操作使得你可以對令牌進行一些必要的管理,令牌可以被用來加載身份信息,裏面包含了這個令牌的相關權限。
    @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(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中進行配置。

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
    public AuthorizationServerConfigurerAdapter() {
    }

    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    }

    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    }

    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    }
}

  • ClientDetailsServiceConfigurer:用來配置客戶端詳情服務(ClientDetailsService),客戶端詳情信息在這裏進行初始化,你能夠把客戶端詳情信息寫死在這裏或者是通過數據庫來存儲調取詳情信息。
  • AuthorizationServerEndpointsConfigurer:用來配置令牌(token)的訪問端點和令牌服務(token
    services)。
  • AuthorizationServerSecurityConfigurer:用來配置令牌端點的安全約束

授權服務配置總結:授權服務配置分成三大塊,可以關聯記憶。

  • 既然要完成認證,它首先得知道客戶端信息從哪兒讀取,因此要進行客戶端詳情配置。

  • 既然要頒發token,那必須得定義token的相關endpoint,以及token如何存取,以及客戶端支持哪些類型的token。

  • 既然暴露除了一些endpoint,那對這些endpoint可以定義一些安全上的約束等。

web安全配置

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //認證管理器
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //安全攔截機制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .antMatchers("/login*").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()	//設置SpringSecurity默認Form登錄
        ;

    }
}

一些關鍵代碼設置好之後我們來測試一下申請令牌
在這裏插入圖片描述
下面是框架提供的URL路徑:

  • /oauth/authorize:授權端點。
  • /oauth/token:令牌端點。
  • /oauth/confirm_access:用戶確認授權提交端點。
  • /oauth/error:授權服務錯誤信息端點。
  • /oauth/check_token:用於資源服務訪問的令牌解析端點。
  • /oauth/token_key:提供公有密匙的端點,如果你使用JWT令牌的話。

參數列表介紹:

  • client_id:客戶端准入標識。
  • client_secret:客戶端祕鑰。
  • grant_type:授權類型,填寫password表示密碼模式
  • username:資源擁有者用戶名。
  • password:資源擁有者密碼。
  • redirect_uri:申請授權碼時的跳轉url

測試完成之後我們搭建網關服務

網關:

​ 網關整合 OAuth2.0 有兩種思路,一種是認證服務器生成jwt令牌, 所有請求統一在網關層驗證,判斷權限等操作;另一種是由各資源服務處理,網關只做請求轉發。
​ 我們選用第一種。我們把API網關作爲OAuth2.0的資源服務器角色,實現接入客戶端權限攔截、令牌解析並轉發當前登錄用戶信息(jsonToken)給微服務,這樣下游微服務就不需要關心令牌格式解析以及OAuth2.0相關機制了。

API網關在認證授權體系裏主要負責兩件事:
(1)作爲OAuth2.0的資源服務器角色,實現接入方權限攔截。
(2)令牌解析並轉發當前登錄用戶信息(明文token)給微服務

微服務拿到明文token(明文token中包含登錄用戶的身份和權限信息)後也需要做兩件事:
(1)用戶授權攔截(看當前用戶是否有權訪問該資源)
(2)將用戶信息存儲進當前線程上下文(有利於後續業務邏輯隨時獲取當前用戶信息)

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>distributed-security</artifactId>
        <groupId>com.example</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>distributed-security-gateway</artifactId>

    <dependencies>

        <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>

    </dependencies>
</project>

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;
    }
}

配置資源服務

在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三個權限。

轉發明文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;
        }
    }
    
    
  2. 將filter納入spring 容器

    @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;
        }
    }
    
    

微服務用戶鑑權攔截

當微服務收到明文token,對該token解析並驗證是否有權限訪問資源,增加微服務用戶鑑權攔截功能。

  1. distributed-security-order服務中添加測試資源
@RestController
public class OrderController {

    @GetMapping(value = "/r1")
    @PreAuthorize("hasAuthority('p1')")//擁有p1權限方可訪問此url
    public String r1(){
        //獲取用戶身份信息
        UserDTO userDTO = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return userDTO.getUsername()+"訪問資源1";
    }

}
  1. Spring Security配置

開啓方法保護,並增加Spring配置策略,除了/login方法不受保護(統一認證要調用),其他資源全部需要認證才能訪問。

@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                .antMatchers("/**").access("#oauth2.hasScope('ROLE_ADMIN')")
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

}
  1. 定義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);

    }
}

經過上邊的過慮 器,資源服務中就可以方便到的獲取用戶的身份信息

UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

三個步驟:

  1. 解析token
  2. 新建並填充authentication
  3. 將authentication保存進安全上下文
    剩下的事兒就交給Spring Security好了

集成測試

測試過程描述:

  1. 採用OAuth2.0的密碼模式從UAA獲取token
  2. 使用該token通過網關訪問訂單服務的測試資源

1、通過網關訪問uaa的授權及獲取令牌,獲取token。注意端口是53010,網關的端口。(上面已經測試過了,參考上面圖片)

2、拿到token之後使用Token過網關訪問訂單服務中的r1-r2測試資源進行測試

在這裏插入圖片描述

訪問沒權限的r2
在這裏插入圖片描述

總結

​ 以上就是簡單的實現Spring cloud Security OAuth2統一認證授權的demo,總的說還有很多地方不足,有時間改進一下,項目以及sql上傳GitHub地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章