SpringCloud Alibaba 微服务实战十四 - SpringCloud Gateway 集成 Oauth2.0

本贴最后更新于 1472 天前,其中的信息可能已经水流花落

导读:上篇文章我们已经抽取出了单独的认证服务,本章主要内容是让 SpringCloud Gateway 集成 Oauth2。

概念部分

image.png

在网关集成 Oauth2.0 后,我们的流程架构如上。主要逻辑如下:
1、客户端应用通过 api 网关请求认证服务器获取 access_token http://localhost:8090/auth-service/oauth/token
2、认证服务器返回 access_token

{
  "access_token": "f938d0c1-9633-460d-acdd-f0693a6b5f4c",
  "token_type": "bearer",
  "refresh_token": "4baea735-3c0d-4dfd-b826-91c6772a0962",
  "expires_in": 43199,
  "scope": "web"
}

3、客户端携带 access_token 通过 API 网关访问后端服务
image.png

4、API 网关收到 access_token 后通过 AuthenticationWebFilter 对 access_token 认证

5、API 网关转发后端请求,后端服务请求 Oauth2 认证服务器获取当前用户

在前面文章中我们搭建好了单独的 Oauth2 认证授权服务,基本功能框架都实现了,这次主要是来实现第四条,SpringCloud 整合 Oauth2 后如何进行 access_token 过滤校验。

代码示例

引入组件

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

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

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

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>

主要引入跟 oauth2 相关的 jar 包,这里还需要引入数据库相关的 jar 包,因为我们的 token 是存在数据库中,要想在网关层校验 token 的有效性必须先从数据库取出 token。

bootstrap.yml 配置修改

spring:
  application:
    name: cloud-gateway
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://xx.0.xx.xx:3306/oauth2_config?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
    username: xxxxx
    password: xxxxxxx
    driver-class-name: com.mysql.jdbc.Driver

主要配置 oauth2 的数据库连接地址

自定义认证接口管理类

在 webFlux 环境下通过实现 ReactiveAuthenticationManager 接口 自定义认证接口管理,由于我们的 token 是存在 jdbc 中所以命名上就叫 ReactiveJdbcAuthenticationManager

@Slf4j
public class ReactiveJdbcAuthenticationManager implements ReactiveAuthenticationManager {

    private TokenStore tokenStore;

    public JdbcAuthenticationManager(TokenStore tokenStore){
        this.tokenStore = tokenStore;
    }

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        return Mono.justOrEmpty(authentication)
                .filter(a -> a instanceof BearerTokenAuthenticationToken)
                .cast(BearerTokenAuthenticationToken.class)
                .map(BearerTokenAuthenticationToken::getToken)
                .flatMap((accessToken ->{
                    log.info("accessToken is :{}",accessToken);
                    OAuth2AccessToken oAuth2AccessToken = this.tokenStore.readAccessToken(accessToken);
                    //根据access_token从数据库获取不到OAuth2AccessToken
                    if(oAuth2AccessToken == null){
                        return Mono.error(new InvalidTokenException("invalid access token,please check"));
                    }else if(oAuth2AccessToken.isExpired()){
                        return Mono.error(new InvalidTokenException("access token has expired,please reacquire token"));
                    }

                    OAuth2Authentication oAuth2Authentication =this.tokenStore.readAuthentication(accessToken);
                    if(oAuth2Authentication == null){
                        return Mono.error(new InvalidTokenException("Access Token 无效!"));
                    }else {
                        return Mono.just(oAuth2Authentication);
                    }
                })).cast(Authentication.class);
    }
}

网关层的安全配置

@Configuration
public class SecurityConfig {
    private static final String MAX_AGE = "18000L";
    @Autowired
    private DataSource dataSource;
    @Autowired
    private AccessManager accessManager;

    /**
     * 跨域配置
     */
    public WebFilter corsFilter() {
        return (ServerWebExchange ctx, WebFilterChain chain) -> {
            ServerHttpRequest request = ctx.getRequest();
            if (CorsUtils.isCorsRequest(request)) {
                HttpHeaders requestHeaders = request.getHeaders();
                ServerHttpResponse response = ctx.getResponse();
                HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
                HttpHeaders headers = response.getHeaders();
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
                headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
                if (requestMethod != null) {
                    headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
                }
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
                headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
                headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);
                if (request.getMethod() == HttpMethod.OPTIONS) {
                    response.setStatusCode(HttpStatus.OK);
                    return Mono.empty();
                }
            }
            return chain.filter(ctx);
        };
    }

    @Bean
    SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception{
        //token管理器
        ReactiveAuthenticationManager tokenAuthenticationManager = new ReactiveJdbcAuthenticationManager(new JdbcTokenStore(dataSource));
        //认证过滤器
        AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(tokenAuthenticationManager);
        authenticationWebFilter.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());

        http
                .httpBasic().disable()
                .csrf().disable()
                .authorizeExchange()
                .pathMatchers(HttpMethod.OPTIONS).permitAll()
                .anyExchange().access(accessManager)
                .and()
                // 跨域过滤器
                .addFilterAt(corsFilter(), SecurityWebFiltersOrder.CORS)
                //oauth2认证过滤器
                .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
        return http.build();
    }
}

这个类是 SpringCloug Gateway 与 Oauth2 整合的关键,通过构建认证过滤器 AuthenticationWebFilter 完成 Oauth2.0 的 token 校验。
AuthenticationWebFilter 通过我们自定义的 ReactiveJdbcAuthenticationManager 完成 token 校验。
我们在这里还加入了 CORS 过滤器,以及权限管理器 AccessManager

权限管理器

@Slf4j
@Component
public class AccessManager implements ReactiveAuthorizationManager<AuthorizationContext> {
    private Set<String> permitAll = new ConcurrentHashSet<>();
    private static final AntPathMatcher antPathMatcher = new AntPathMatcher();


    public AccessManager (){
        permitAll.add("/");
        permitAll.add("/error");
        permitAll.add("/favicon.ico");
        permitAll.add("/**/v2/api-docs/**");
        permitAll.add("/**/swagger-resources/**");
        permitAll.add("/webjars/**");
        permitAll.add("/doc.html");
        permitAll.add("/swagger-ui.html");
        permitAll.add("/**/oauth/**");
        permitAll.add("/**/current/get");
    }

    /**
     * 实现权限验证判断
     */
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) {
        ServerWebExchange exchange = authorizationContext.getExchange();
        //请求资源
        String requestPath = exchange.getRequest().getURI().getPath();
        // 是否直接放行
        if (permitAll(requestPath)) {
            return Mono.just(new AuthorizationDecision(true));
        }

        return authenticationMono.map(auth -> {
            return new AuthorizationDecision(checkAuthorities(exchange, auth, requestPath));
        }).defaultIfEmpty(new AuthorizationDecision(false));

    }

    /**
     * 校验是否属于静态资源
     * @param requestPath 请求路径
     * @return
     */
    private boolean permitAll(String requestPath) {
        return permitAll.stream()
                .filter(r -> antPathMatcher.match(r, requestPath)).findFirst().isPresent();
    }

    //权限校验
    private boolean checkAuthorities(ServerWebExchange exchange, Authentication auth, String requestPath) {
        if(auth instanceof OAuth2Authentication){
            OAuth2Authentication athentication = (OAuth2Authentication) auth;
            String clientId = athentication.getOAuth2Request().getClientId();
            log.info("clientId is {}",clientId);
        }

        Object principal = auth.getPrincipal();
        log.info("用户信息:{}",principal.toString());
        return true;
    }
}

主要是过滤掉静态资源,将来一些接口权限校验也可以放在这里。

测试

  • 通过网关调用 auth-service 获取 access_token
    image.png

  • 在 Header 上添加认证访问后端服务
    image.png

  • 网关过滤器进行 token 校验
    image.png

  • 权限管理器校验
    image.png

  • 去认证服务器校验当前用户
    image.png

  • 返回正常结果
    image.png

  • 故意写错 access_token,返回错误响应
    image.png

  • 请求头上去掉 access_token,直接返回 401 Unauthorized
    image.png

总结

通过以上几步我们将 SpringCloud Gateway 整合好了 Oauth2.0,这样我们整个项目也基本完成了,后面几期再来对项目进行优化,欢迎持续关注。

  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    938 引用 • 1456 回帖 • 163 关注
  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    139 引用 • 441 回帖
  • 微服务

    微服务架构是一种架构模式,它提倡将单一应用划分成一组小的服务。服务之间互相协调,互相配合,为用户提供最终价值。每个服务运行在独立的进程中。服务于服务之间才用轻量级的通信机制互相沟通。每个服务都围绕着具体业务构建,能够被独立的部署。

    96 引用 • 155 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • someone
    作者

    有偿提供本系列源码!

    1 回复
  • 其他回帖
  • yp

    架构图看着提一个疑问 为啥具体服务还需要去认证服务器验证一次, 网关的作用仅仅是 API 聚合和下发 token?

    如果在网关已经验证通过,其实服务是不是只需要验证请求是否是从网关过来的就可以了

  • someone
    作者

    嗯嗯,后面也会换成 jwt 的

  • 最近也在搞 gateway 的 oauth2,指出几个问题哈:

    ReactiveJdbcAuthenticationManager 感觉是多此一举,spring-security-oauth2-resource-server 的自动化配置一点没用上,这里全靠手动装配。

    另外在 spring cloud 的情况下,不可能让你直接操作 TokenStore 的。授权模块的东西,直接在这里用是完全不合适的。如果每个 oauth2 资源服务器都这样会有很大的问题。现在要不用 jwt 私钥 或者 JWK 解密,要不用 opaqueToken 方式进行校验。所以在 spring security 5.2 以后,官方也只提供了这两种方式的支持。

  • 查看全部回帖