Spring Cloud實戰 | 第六篇:Spring Cloud Gateway + Spring Security OAuth2 + JWT實現微服務統一認證授權鑒權


歡迎大家加入開源項目有來項目交流群,一起學習Spring Cloud微服務生態組件、分布式、Docker、K8S、Vue、element-ui、uni-app等主流全棧技術。

一. 前言

有來】開源全棧項目版本更新,本文部分內容和項目源碼有出入,建議移步至 【Spring Cloud & Alibaba 實戰 | 總結篇】Spring Cloud Gateway + Spring Security OAuth2 + JWT 實現微服務統一認證授權和鑒權,基於RBAC設計的適配微服務開發模式權限框架

線上地址:www.youlai.tech

往期系列文章

后台微服務

  1. Spring Cloud實戰 | 第一篇:Windows搭建Nacos服務
  2. Spring Cloud實戰 | 第二篇:Spring Cloud整合Nacos實現注冊中心
  3. Spring Cloud實戰 | 第三篇:Spring Cloud整合Nacos實現配置中心
  4. Spring Cloud實戰 | 第四篇:Spring Cloud整合Gateway實現API網關
  5. Spring Cloud實戰 | 第五篇:Spring Cloud整合OpenFeign實現微服務之間的調用
  6. Spring Cloud實戰 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT實現微服務統一認證授權
  7. Spring Cloud實戰 | 最七篇:Spring Cloud Gateway+Spring Security OAuth2集成統一認證授權平台下實現注銷使JWT失效方案
  8. Spring Cloud實戰 | 最八篇:Spring Cloud +Spring Security OAuth2+ Vue前后端分離模式下無感知刷新實現JWT續期
  9. Spring Cloud實戰 | 最九篇:Spring Security OAuth2認證服務器統一認證自定義異常處理
  10. Spring Cloud實戰 | 第十篇 :Spring Cloud + Nacos整合Seata 1.4.1最新版本實現微服務架構中的分布式事務,進階之路必須要邁過的檻
  11. Spring Cloud實戰 | 第十一篇 :Spring Cloud Gateway網關實現對RESTful接口權限和按鈕權限細粒度控制

后台管理前端

  1. vue-element-admin實戰 | 第一篇: 移除mock接入微服務接口,搭建SpringCloud+Vue前后端分離管理平台
  2. vue-element-admin實戰 | 第二篇: 最小改動接入后台實現根據權限動態加載菜單

微信小程序

  1. vue+uni-app商城實戰 | 第一篇:從0到1快捷開發一個商城微信小程序,無縫接入Spring Cloud OAuth2實現一鍵授權登錄

應用部署

  1. Docker實戰 | 第一篇:Linux 安裝 Docker
  2. Docker實戰 | 第二篇:Docker部署nacos-server:1.4.0
  3. Docker實戰 | 第三篇:IDEA集成Docker插件實現一鍵自動打包部署微服務項目,一勞永逸的技術手段值得一試
  4. Docker實戰 | 第四篇:Docker安裝Nginx,實現基於vue-element-admin框架構建的項目線上部署
  5. Docker實戰 | 第五篇:Docker啟用TLS加密解決暴露2375端口引發的安全漏洞,被黑掉三台雲主機的教訓總結

二. OAuth2和JWT的關系

1. 什么是OAuth2?

OAUth2就是一套廣泛流行的認證授權協議,大白話說呢OAuth2這套協議中有兩個核心的角色,認證服務器和資源服務器。

兩個角色和 youlai-mall 模塊對應關系如下:

模塊名稱 youlai-mall模塊 OAuth2角色 服務地址
認證中心 youlai-auth 認證服務器 localhost:8000
網關 youlai-gateway 資源服務器 localhost:9999

用戶不能直接去訪問資源服務器(網關),必須先到認證服務器認證,通過后頒發一個token令牌給你,你只有拿着token訪問資源服務器才能通過,令牌token是有時間限制的,到時間了就無效。

這個模式相信經常到甲方爸爸的地方做駐場的小伙伴深有體會,一般人家可不會給你一個正式員工工牌,要么拿身份證抵押換個臨時訪問牌,隔天就失效,這樣人家才有安全感嘛~

其中網關為什么能作為“資源服務器”呢? 網關是作為各個微服務(會員服務、商品服務、訂單服務等)統一入口,也就是這些資源服務的統一門面,在這里可以對JWT驗簽、JWT有效期判斷、JWT攜帶角色權限判斷。

2. 什么是JWT?

JWT(JSON Web Token)它沒啥懸乎的,就是一個特殊的token,最大的特性就是無狀態,因為它本身可以攜帶用戶的信息(用戶ID、用戶名、用戶的角色集合等),我們先看下一個解析過后的JWT是什么樣子的。

JWT字符串由Header(頭部)、Payload(負載)、Signature(簽名)三部分組成。

Header: JSON對象,用來描述JWT的元數據,alg屬性表示簽名的算法,typ標識token的類型

Payload: JSON對象,重要部分,除了默認的字段,還可以擴展自定義字段,比如用戶ID、姓名、角色等等

Signature: 對Header、Payload這兩部分進行簽名,認證服務器使用私鑰簽名,然后在資源服務器使用公鑰驗簽,防止數據被人動了手腳

JWT和傳統的Cookie/Session會話管理相比較有着多方面的優勢,因為Cookie/Session需要在服務器Session存用戶信息,然后拿客戶端Cookie存儲的SessionId獲取用戶信息,這個過程需要消耗服務器的內存和對客戶端的要求比較嚴格(需支持Cookie),而JWT最大的特性在於就是無狀態、去中心化,所以JWT更適用分布式的場景,不需要在多台服務器做會話同步這種消耗服務器性能的操作。

另外JWT和Redis+Token這兩種會話管理小伙伴們看項目情況選擇,別有用了JWT還使用Redis存儲的,因為你這種做法對JWT來說就是“傷害不大,但侮辱性極強”的做法,就當着它的面說我就看不上你的最自以為是的“無狀態”特性。

3. OAuth2和JWT關系?

  • OAuth2是一種認證授權的協議規范。
  • JWT是基於token的安全認證協議的實現。

OAuth2的認證服務器簽發的token可以使用JWT實現,JWT輕量且安全。

三. 認證服務器

認證服務器落地 youlai-mall 的youlai-auth認證中心模塊,完整代碼地址: github | 碼雲

1. pom依賴

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

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

2. 認證服務配置(AuthorizationServerConfig)

/**
 * 認證服務配置
 */
@Configuration
@EnableAuthorizationServer
@AllArgsConstructor
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

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

    /**
     * 客戶端信息配置
     */
    @Override
    @SneakyThrows
    public void configure(ClientDetailsServiceConfigurer clients) {
        JdbcClientDetailsServiceImpl jdbcClientDetailsService = new JdbcClientDetailsServiceImpl(dataSource);
        jdbcClientDetailsService.setFindClientDetailsSql(AuthConstants.FIND_CLIENT_DETAILS_SQL);
        jdbcClientDetailsService.setSelectClientDetailsSql(AuthConstants.SELECT_CLIENT_DETAILS_SQL);
        clients.withClientDetails(jdbcClientDetailsService);
    }

    /**
     * 配置授權(authorization)以及令牌(token)的訪問端點和令牌服務(token services)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

        endpoints.authenticationManager(authenticationManager)
                .accessTokenConverter(jwtAccessTokenConverter())
                .tokenEnhancer(tokenEnhancerChain)
                .userDetailsService(userDetailsService)
                // refresh_token有兩種使用方式:重復使用(true)、非重復使用(false),默認為true
                //      1.重復使用:access_token過期刷新時, refresh token過期時間未改變,仍以初次生成的時間為准
                //      2.非重復使用:access_token過期刷新時, refresh_token過期時間延續,在refresh_token有效期內刷新而無需失效再次登錄
                .reuseRefreshTokens(false);
    }

    /**
     * 允許表單認證
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security.allowFormAuthenticationForClients();
    }

    /**
     * 使用非對稱加密算法對token簽名
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyPair());
        return converter;
    }

    /**
     * 從classpath下的密鑰庫中獲取密鑰對(公鑰+私鑰)
     */
    @Bean
    public KeyPair keyPair() {
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(
                new ClassPathResource("youlai.jks"), "123456".toCharArray());
        KeyPair keyPair = factory.getKeyPair(
                "youlai", "123456".toCharArray());
        return keyPair;
    }

    /**
     * JWT內容增強
     */
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> {
            Map<String, Object> map = new HashMap<>(2);
            User user = (User) authentication.getUserAuthentication().getPrincipal();
            map.put(AuthConstants.JWT_USER_ID_KEY, user.getId());
            map.put(AuthConstants.JWT_CLIENT_ID_KEY, user.getClientId());
            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
            return accessToken;
        };
    }
}

AuthorizationServerConfig這個配置類是整個認證服務實現的核心。總結下來就是兩個關鍵點,客戶端信息配置和access_token生成配置。

2.1 客戶端信息配置

配置OAuth2認證允許接入的客戶端的信息,因為接入OAuth2認證服務器首先人家得認可你這個客戶端吧,就比如上面案例中的QQ的OAuth2認證服務器認可“有道雲筆記”客戶端。

同理,我們需要把客戶端信息配置在認證服務器上來表示認證服務器所認可的客戶端。一般可配置在認證服務器的內存中,但是這樣很不方便管理擴展。所以實際最好配置在數據庫中的,提供可視化界面對其進行管理,方便以后像PC端、APP端、小程序端等多端靈活接入。

Spring Security OAuth2官方提供的客戶端信息表oauth_client_details

CREATE TABLE `oauth_client_details`  (
  `client_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authorities` varchar(256) 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` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `autoapprove` varchar(256) 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 ROW_FORMAT = Dynamic;

添加一條客戶端信息

INSERT INTO `oauth_client_details` VALUES ('client', NULL, '123456', 'all', 'password,refresh_token', '', NULL, NULL, NULL, NULL, NULL);

2.2 token生成配置

項目使用JWT實現access_token,關於access_token生成步驟的配置如下:

1. 生成密鑰庫

使用JDK工具的keytool生成JKS密鑰庫(Java Key Store),並將youlai.jks放到resources目錄

keytool -genkey -alias youlai -keyalg RSA -keypass 123456 -keystore youlai.jks -storepass 123456

-genkey 生成密鑰

-alias 別名

-keyalg 密鑰算法

-keypass 密鑰口令

-keystore 生成密鑰庫的存儲路徑和名稱

-storepass 密鑰庫口令

2. JWT內容增強

JWT負載信息默認是固定的,如果想自定義添加一些額外信息,需要實現TokenEnhancer的enhance方法將附加信息添加到access_token中。

3. JWT簽名

JwtAccessTokenConverter是生成token的轉換器,可以實現指定token的生成方式(JWT)和對JWT進行簽名。

簽名實際上是生成一段標識(JWT的Signature部分)作為接收方驗證信息是否被篡改的依據。原理部分請參考這篇的文章:RSA加密、解密、簽名、驗簽的原理及方法

其中對JWT簽名有對稱和非對稱兩種方式:

對稱方式:認證服務器和資源服務器使用同一個密鑰進行加簽和驗簽 ,默認算法HMAC

非對稱方式:認證服務器使用私鑰加簽,資源服務器使用公鑰驗簽,默認算法RSA

非對稱方式相較於對稱方式更為安全,因為私鑰只有認證服務器知道。

項目中使用RSA非對稱簽名方式,具體實現步驟如下:

(1). 從密鑰庫獲取密鑰對(密鑰+私鑰)
(2). 認證服務器私鑰對token簽名
(3). 提供公鑰獲取接口供資源服務器驗簽使用

公鑰獲取接口

/**
 * RSA公鑰開放接口
 */
@RestController
@AllArgsConstructor
public class PublicKeyController {

    private KeyPair keyPair;

    @GetMapping("/getPublicKey")
    public Map<String, Object> getPublicKey() {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAKey key = new RSAKey.Builder(publicKey).build();
        return new JWKSet(key).toJSONObject();
    }

}

3. 安全配置(WebSecurityConfig)

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
         http
            .authorizeRequests().requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
        .and()
            .authorizeRequests().antMatchers("/getPublicKey").permitAll().anyRequest().authenticated()
        .and()
            .csrf().disable();
    }

    /**
     *  如果不配置SpringBoot會自動配置一個AuthenticationManager,覆蓋掉內存中的用戶
     */
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder()  {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

安全配置主要是配置請求訪問權限、定義認證管理器、密碼加密配置。

四. 資源服務器

資源服務器落地 youlai-mall 的youlai-gateway微服務網關模塊,完整代碼地址: github | 碼雲

上文有提到過網關這里是擔任資源服務器的角色,因為網關是微服務資源訪問的統一入口,所以在這里做資源訪問的統一鑒權是再合適不過。

1. pom依賴

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-resource-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-jose</artifactId>
    </dependency>

2. 配置文件(youlai-gateway.yaml)

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          # 獲取JWT驗簽公鑰請求路徑
          jwk-set-uri: 'http://localhost:8000/getPublicKey'
  redis:
    database: 0
    host: localhost
    port: 6379
    password:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 啟用服務發現
          lower-case-service-id: true
      routes:
        - id: youlai-auth
          uri: lb://youlai-auth
          predicates:
            - Path=/youlai-auth/**
          filters:
            - StripPrefix=1
        - id: youlai-admin
          uri: lb://youlai-admin
          predicates:
            - Path=/youlai-admin/**
          filters:
            - StripPrefix=1

# 配置白名單路徑
white-list:
    urls:
      - "/youlai-auth/oauth/token"

3. 鑒權管理器

鑒權管理器是作為資源服務器驗證是否有權訪問資源的裁決者,核心部分的功能先已通過注釋形式進行說明,后面再對具體形式補充。

/**
 * 鑒權管理器
 */
@Component
@AllArgsConstructor
@Slf4j
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    private RedisTemplate redisTemplate;
    private WhiteListConfig whiteListConfig;

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        ServerHttpRequest request = authorizationContext.getExchange().getRequest();
        String path = request.getURI().getPath();
        PathMatcher pathMatcher = new AntPathMatcher();
        
        // 1. 對應跨域的預檢請求直接放行
        if (request.getMethod() == HttpMethod.OPTIONS) {
            return Mono.just(new AuthorizationDecision(true));
        }

        // 2. token為空拒絕訪問
        String token = request.getHeaders().getFirst(AuthConstants.JWT_TOKEN_HEADER);
        if (StrUtil.isBlank(token)) {
            return Mono.just(new AuthorizationDecision(false));
        }

        // 3.緩存取資源權限角色關系列表
        Map<Object, Object> resourceRolesMap = redisTemplate.opsForHash().entries(AuthConstants.RESOURCE_ROLES_KEY);
        Iterator<Object> iterator = resourceRolesMap.keySet().iterator();

        // 4.請求路徑匹配到的資源需要的角色權限集合authorities
        List<String> authorities = new ArrayList<>();
        while (iterator.hasNext()) {
            String pattern = (String) iterator.next();
            if (pathMatcher.match(pattern, path)) {
                authorities.addAll(Convert.toList(String.class, resourceRolesMap.get(pattern)));
            }
        }
        Mono<AuthorizationDecision> authorizationDecisionMono = mono
                .filter(Authentication::isAuthenticated)
                .flatMapIterable(Authentication::getAuthorities)
                .map(GrantedAuthority::getAuthority)
                .any(roleId -> {
                    // 5. roleId是請求用戶的角色(格式:ROLE_{roleId}),authorities是請求資源所需要角色的集合
                    log.info("訪問路徑:{}", path);
                    log.info("用戶角色roleId:{}", roleId);
                    log.info("資源需要權限authorities:{}", authorities);
                    return authorities.contains(roleId);
                })
                .map(AuthorizationDecision::new)
                .defaultIfEmpty(new AuthorizationDecision(false));
        return authorizationDecisionMono;
    }
}

第1、2處只是做些基礎訪問判斷,不做過多的說明

第3處從Redis緩存獲取資源權限數據。首先我們會關注兩個問題:

a. 資源權限數據是什么樣格式數據?
b. 數據什么時候初始化到緩存中?

以下就帶着這兩個問題來分析要完成第4步從緩存獲取資源權限數據需要提前做哪些工作吧。

a. 資源權限數據格式

需要把url和role_ids的映射關系緩存到redis,大致意思的意思可以理解擁有url訪問權限的角色ID有哪些。

b. 初始化緩存時機

SpringBoot提供兩個接口CommandLineRunner和ApplicationRunner用於容器啟動后執行一些業務邏輯,比如數據初始化和預加載、MQ監聽啟動等。兩個接口執行時機無差,唯一區別在於接口的參數不同。有興趣的朋友可以了解一下這兩位朋友,以后會經常再見的哈~

那么這里的業務邏輯是在容器初始化完成之后將從MySQL讀取到資源權限數據加載到Redis緩存中,正中下懷,來看下具體實現吧。

Redis緩存中的資源權限數據

至此從緩存數據可以看到擁有資源url訪問權限的角色信息,從緩存獲取賦值給resourceRolesMap。

第4處根據請求路徑去匹配resourceRolesMap的資url(Ant Path匹配規則),得到對應資源所需角色信息添加到authorities。

第5處就是判斷用戶是否有權訪問資源的最終一步了,只要用戶的角色中匹配到authorities中的任何一個,就說明該用戶擁有訪問權限,允許通過。

4. 資源服務器配置

這里做的工作是將鑒權管理器AuthorizationManager配置到資源服務器、請求白名單放行、無權訪問和無效token的自定義異常響應。配置類基本上都是約定俗成那一套,核心功能和注意的細節點通過注釋說明。

/**
 * 資源服務器配置
 */
@AllArgsConstructor
@Configuration
// 注解需要使用@EnableWebFluxSecurity而非@EnableWebSecurity,因為SpringCloud Gateway基於WebFlux
@EnableWebFluxSecurity
public class ResourceServerConfig {

    private AuthorizationManager authorizationManager;
    private CustomServerAccessDeniedHandler customServerAccessDeniedHandler;
    private CustomServerAuthenticationEntryPoint customServerAuthenticationEntryPoint;
    private WhiteListConfig whiteListConfig;
    
    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http.oauth2ResourceServer().jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
        // 自定義處理JWT請求頭過期或簽名錯誤的結果
        http.oauth2ResourceServer().authenticationEntryPoint(customServerAuthenticationEntryPoint);
        http.authorizeExchange()
                .pathMatchers(ArrayUtil.toArray(whiteListConfig.getUrls(),String.class)).permitAll()
                .anyExchange().access(authorizationManager)
                .and()
                .exceptionHandling()
                .accessDeniedHandler(customServerAccessDeniedHandler) // 處理未授權
                .authenticationEntryPoint(customServerAuthenticationEntryPoint) //處理未認證
                .and().csrf().disable();

        return http.build();
    }

    /**
     * @linkhttps://blog.csdn.net/qq_24230139/article/details/105091273
     * ServerHttpSecurity沒有將jwt中authorities的負載部分當做Authentication
     * 需要把jwt的Claim中的authorities加入
     * 方案:重新定義ReactiveAuthenticationManager權限管理器,默認轉換器JwtGrantedAuthoritiesConverter
     */
    @Bean
    public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstants.AUTHORITY_PREFIX);
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstants.AUTHORITY_CLAIM_NAME);

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
    }
    
}
/**
 * 無權訪問自定義響應
 */
@Component
public class CustomServerAccessDeniedHandler implements ServerAccessDeniedHandler {

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException e) {
        ServerHttpResponse response=exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        response.getHeaders().set("Access-Control-Allow-Origin","*");
        response.getHeaders().set("Cache-Control","no-cache");
        String body= JSONUtil.toJsonStr(Result.custom(ResultCodeEnum.USER_ACCESS_UNAUTHORIZED));
        DataBuffer buffer =  response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
        return response.writeWith(Mono.just(buffer));
    }
}
/**
 * 無效token/token過期 自定義響應
 */
@Component
public class CustomServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {

    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        response.getHeaders().set("Access-Control-Allow-Origin", "*");
        response.getHeaders().set("Cache-Control", "no-cache");
        String body = JSONUtil.toJsonStr(Result.custom(ResultCodeEnum.USER_ACCOUNT_UNAUTHENTICATED));
        DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
        return response.writeWith(Mono.just(buffer));
    }

}

5. 網關鑒權測試

模擬數據說明,admin用戶擁有角色2,角色2有菜單管理、用戶管理、部門管理的資源權限,無其他權限

用戶 角色ID 角色名稱
admin 2 系統管理員
資源名稱 資源路徑 要求角色權限
系統管理 /youlai-admin/** [1]
菜單管理 /youlai-admin/menus/** [1,2]
用戶管理 /youlai-admin/users/** [1,2]
部門管理 /youlai-admin/depts/** [1,2]
字典管理 /youlai-admin/dictionaries/** [1]
角色管理 /youlai-admin/roles/** [1]
資源管理 /youlai-admin/resources/** [1]

啟動管理平台前端工程 youlai-mall-admin 完整代碼地址: github | 碼雲

訪問除了菜單管理、用戶管理、部門管理這三個系統管理員擁有訪問權限的資源之外,頁面都會提示“訪問未授權”,直接的說明了網關服務器實現了請求鑒權的目的。

五. 結語

至此,Spring Cloud的統一認證授權就實現了。其實還有很多可以擴展的點,文章中把客戶端信息存儲在數據庫中,那么可以添加一個管理界面來維護這些客戶端信息,這樣便可靈活配置客戶端接入認證平台、認證有效期等等。同時也還有未完成的事項,我們知道JWT是無狀態的,那用戶在登出、修改密碼、注銷的時候怎么能把JWT置為無效呢?因為不可能像cookie/session機制把用戶信息從服務器刪除。所以這些都是值得思考的東西,我會在下篇文章提供對應的解決方案。

大家對文章或項目有好的建議,歡迎留言,感謝~


免責聲明!

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



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