SpringCloud实操(三)API网关整合OAuth2认证授权服务

1. 基本概念

API网关将自己注册为Eureka服务治理下的应用,同时也从Eureka服务治理中获得所有其他微服务的实例信息。我们通过搭建独立的OAuth2认证授权服务,将微服务中冗余的登录校验、签名校验单独剥离出来,这些校验与微服务自己的业务并没有太大的关系,所以这些功能完全可以独立成一个单独的服务存在。只是独立出来之后,并不是给每个微服务调用,而是通过API网关进行统一调用,来对微服务接口做前置过滤,实现对分布式系统中的其他的微服务接口的拦截和安全校验。

我们需要改造Spring Cloud的API网关服务项目,为项目增加OAuth2依赖,并且新增前置过滤器类,在前置过滤中对请求进行安全校验。流程如下:


  1. 用户请求某个资源前,需要先通过api网关访问Oauth2认证授权服务请求一个AccessToken
  2. 用户通过认证授权服务得到AccessToken后,通过api网关调用其他资源服务A、B、C
  3. 资源服务根据AccessToken从OAuth2认证授权服务验证该token的用户请求是否有效

2. 要做的事情

  1. 修改 producer-service 微服务项目,配置OAuth2认证授权服务地址,该项目被当做资源服务。
  2. 修改 api-gateway网关微服务项目,配置OAuth2,并且添加前置过滤器,对请求资源进行过滤和路由。
  3. 演示如何通过api网关得到OAuth2授权服务器的AccessToken,并且根据得到的AccessToken通过api网关请求资源服务。

2. 代码示例

2.1 打开项目 api-gateway

为项目添加OAuth2依赖,本项目完整依赖如下:
build.gradle

dependencies {
    compile('org.springframework.cloud:spring-cloud-starter-zuul')
    compile('org.springframework.cloud:spring-cloud-starter-eureka')
    compile('org.springframework.cloud:spring-cloud-starter-oauth2')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

修改application.yml,为OAuth2服务和producer服务添加路由,完整内容如下:

#服务器配置
server:
  #端口
  port: 8080

#服务器发现注册配置
eureka:
  client:
    serviceUrl:
      #配置服务中心(可配置多个,用逗号隔开)
      defaultZone: https://www.apiboot.cn/eureka

#spring配置
spring:
  #应用配置
  application:
    #名称: api网关服务
    name: api-gateway

#API网关配置
zuul:
  #路由配置
  routes:
    auth:    #认证服务
      #响应的路径
      path: /auth/**
      #敏感头信息
      sensitiveHeaders:
      #重定向到的服务(根据服务id名称从注册中心获取服务地址)
      serviceId:  auth-server
    producer: #生产者服务
      #响应的路径
      path: /producer/**
      sensitiveHeaders:
      #重定向到的服务(根据服务id名称从注册中心获取服务地址)
      serviceId:  producer-service
  #添加代理头
  add-proxy-headers: true

新增安全配置类SecurityConfig.java

/**
 * 安全配置
 * @ EnableWebSecurity 启用web安全
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * http安全配置
     * @param http http安全对象
     * @throws Exception http安全异常信息
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();  // 禁用csrf
    }
}

新增资源前置过滤器,访问所有资源时都需要携带AccessToken值,所以在api网关这里新建一个前置过滤器,在路由前先判断请求头里是否有Authorization参数值,如果无参数值就没必要路由到资源服务器上了,这里对请求统一做过滤拦截。

AccessFilter.java

/**
 * 资源过滤器
 * 所有的资源请求在路由之前进行前置过滤
 * 如果请求头不包含 Authorization参数值,直接拦截不再路由
 */
public class AccessFilter extends ZuulFilter {

    private static Logger logger = LoggerFactory.getLogger(AccessFilter.class);

    /**
     * 过滤器的类型 pre表示请求在路由之前被过滤
     * @return 类型
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 过滤器的执行顺序
     * @return 顺序 数字越大表示优先级越低,越后执行
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 过滤器是否会被执行
     * @return true
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 过滤逻辑
     * @return 过滤结果
     */
    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        logger.info("send {} request to {}",request.getMethod(),request.getRequestURL().toString());

        Object accessToken = request.getHeader("Authorization");
        if (accessToken==null){
            logger.warn("Authorization token is empty");
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(401);
            requestContext.setResponseBody("Authorization token is empty");
            return null;
        }
        logger.info("Authorization token is ok");
        return null;
    }
}

启动类添加注解,并且注入资源过滤器

/**
 * API网关服务
 * @ EnableZuulProxy 启用网关路由
 * @ EnableOAuth2Sso 启用OAuth2单点登录
 */
@SpringBootApplication
@EnableZuulProxy
@EnableOAuth2Sso
public class ApiGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }

    /**
     * 资源过滤器
     * @return 资源过滤器
     */
    @Bean
    public AccessFilter accessFilter(){
        return new AccessFilter();
    }
}

2.2 打开项目 producer-service

为项目添加OAuth2依赖,本项目完整依赖如下:
build.gradle

dependencies {
    compile('org.springframework.cloud:spring-cloud-starter-eureka')
    compile('org.springframework.cloud:spring-cloud-starter-config')
    compile('org.springframework.boot:spring-boot-starter-actuator')
    compile('org.springframework.cloud:spring-cloud-starter-bus-amqp')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.cloud:spring-cloud-starter-oauth2')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

修改application.yml,添加安全配置,完整内容如下:

#spring配置
spring:
  application:
    #应用名称(服务提供者)
    name: producer-service
  cloud:
    #消息总线
    bus:
      trace:
        #开启消息跟踪
        enabled: true

#服务器配置
server:
  #端口
  port: 8000
  #显示名称
  display-name: producer-service

#服务器发现注册配置
eureka:
  client:
    serviceUrl:
      #配置服务中心(可配置多个,用逗号隔开)
      defaultZone: https://www.apiboot.cn/eureka

#安全配置
security:
  oauth2:
    resource:
      id: producer-service
      #指定用户信息地址
      user-info-uri: https://api.apiboot.cn/auth/user
      prefer-token-info: false

新增资源服务配置类,继承 ResourceServerConfigurerAdapter 重写http安全配置
ResourceServerConfig.java

/**
 * 资源服务配置
 * @ EnableResourceServer 启用资源服务
 * @ EnableWebSecurity 启用web安全
 * @ EnableGlobalMethodSecurity 启用全局方法安全注解,就可以在方法上使用注解来对请求进行过滤
 */
@Configuration
@EnableResourceServer
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .httpBasic();
    }
}

新增资源控制器,为了稍后演示方便,这里添加了3个方法,每个方法对应不同角色的访问权限
ResourceController.java

/**
 * 资源控制器
 * 用来演示不同角色访问该控制器的接口返回的信息
 */
@RestController
@RequestMapping("/resources")
public class ResourceController {

    /**
     * 只有 ROLE_USER 角色的用户才能访问
     * @return 问候信息
     */
    @GetMapping("/hello")
    @PreAuthorize("hasRole('ROLE_USER')")
    public String helloUser(){
        return "hello User";
    }

    /**
     * 只有 ROLE_ADMIN 角色的用户才能访问
     * @return 问候信息
     */
    @GetMapping("/admin")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public String helloAdmin(){
        return "hello Admin";
    }

    /**
     * 只有 ROLE_GUEST 角色的用户才能访问
     * @return 问候信息
     */
    @GetMapping("/guest")
    @PreAuthorize("hasRole('ROLE_GUEST')")
    public String helloGuest(){
        return "hello Guest";
    }

}

3. 演示操作

为了演示方便,我把服务全部部署到了我的公网服务器上,并且绑定了域名。

1.打开浏览器访问Eureka服务注册中心 https://www.apiboot.cn/

可以看到API网关服务,OAuth2服务,Producer资源服务已经全部正常运行起来了。

2.打开PostMan,直接通过api网关访问OAuth2服务的获取token操作

发现请求被api网关的资源过滤器拦截了,提示没有Authorization token。

所以我们需要切换到如下图的Authorization页卡,选择Basic Auth认证,并且在右侧的Username和Password中分别输入OAuth2项目授权配置里的Client和secret的值。

填写完毕后点击 Preview Request,发现Headers页卡多了一个Authorization的参数。我们点击Send发送请求,发现api网关的资源过滤器已经不拦截我们的请求了,并且将我们的请求正常的发送到了OAuth2认证授权服务器,由于没有填写grant等参数,所以授权服务器返回了如下图所示的信息。

填写grant_type的值为password,并且填写正确的用户名和密码,再次点击Send

可以看到我们已经成功的通过api网关获取到了OAuth2认证授权服务器的access_token数据.

3.得到token后,我们接下来演示如何通过token来获取资源。同样,在postMan里输入OAuth2认证授权服务的 /user接口,获取当前授权用户的信息

我们可以看到同样被api网关拦截了。

选择OAuth 2.0授权,点击右侧黄色的Get New Access Token

选择GrantType为Password类型,填写Access Token URL,以及其他信息。

其中Username和Password是上篇文章中我们在OAuth2认证授权服务中初始化的用户名和密码。

点击Request Token 请求Token,信息填写正确的情况下将会返回如下图所示信息

可以看到我们已经通过OAuth2.0成功获取到了Access Token,点击Use Token

点击黄色的Preview Request按钮,将会把我们请求到的AccessToken自动添加到Headers的Authorization参数中

此时我们再次点击Send按钮,发现api网关不再拦截,而且成功的路由到了OAuth2认证授权服务的接口,并且成功返回信息。

4.接下来我们演示如何获取其他微服务的资源

直接访问producer微服务的/resources/user接口,得到如下信息,api网关的资源拦截器拦截了我们的请求,并且提示我们没有Authorization的token

根据之前的步骤,在请求头中添加了Authorization参数值,成功返回信息

访问producer微服务的/resources/admin接口,正常返回信息。

访问producer微服务的/resources/guest接口,发现没有预期返回接口数据,而是提示不允许访问,应为我们的账号是admin,根据上篇文章写的 我们为该账号分配了两个角色,分别是 ROLE_USER 和 ROLE_ADMIN,这两个角色的用户分别能够访问producer微服务的/resources/ hello接口和 /admin接口。

producer微服务的/resources/guest接口需要ROLE_GUEST角色才能访问

我们为Guest用户请求一个AccessToken

输入正确的账号密码后点击Request Token,得到该用户的Access Token,该用户就可以拿着这个令牌去访问其他资源了

guest用户的token访问producer微服务的/resources/hello 接口,显示不允许访问

image.png

guest用户的token访问producer微服务的/resources/admin 接口,显示不允许访问

guest用户的token访问producer微服务的/resources/guest 接口,正常访问

到此,演示就结束了。

在分布式微服务架构中,我们通过api网关服务从OAuth2认证授权服务获取AccessToken,然后使用AccessToken通过api网关对分布式系统中的其他微服务进行资源调用,被调用的微服务从OAuth2认证授权服务队token进行校验。Zuul+OAuth2+DS整合到此结束。

4. 注意事项

1.zuul网关配置一定要设置添加代理头的值为 ture

Zuul的敏感头信息根据需要设置,本示例的主要配置如下:

#API网关配置
zuul:
  #路由配置
  routes:
    auth:    #认证服务
      #响应的路径
      path: /auth/**
      #敏感头信息
      sensitiveHeaders:
      #重定向到的服务(根据服务id名称从注册中心获取服务地址)
      serviceId:  auth-server
    producer: #生产者服务
      #响应的路径
      path: /producer/**
      sensitiveHeaders:
      #重定向到的服务(根据服务id名称从注册中心获取服务地址)
      serviceId:  producer-service
  #添加代理头
  add-proxy-headers: true

2.producer 资源服务一定要设置用户信息地址(OAuth2授权认证服务的用户信息)

#安全配置
security:
  oauth2:
    resource:
      id: producer-service
      #指定用户信息地址
      user-info-uri: https://api.apiboot.cn/auth/user
      prefer-token-info: false

  1. 如果要在方法上使用注解来对请求进行过滤,要添加启用全局方法安全注解
@EnableGlobalMethodSecurity(prePostEnabled = true)

本项目开源地址:https://github.com/lanshiqin/cloud-project

发布了8 篇原创文章 · 获赞 0 · 访问量 7286

猜你喜欢

转载自blog.csdn.net/fedorafrog/article/details/103968641