1. 基本概念
API网关将自己注册为Eureka服务治理下的应用,同时也从Eureka服务治理中获得所有其他微服务的实例信息。我们通过搭建独立的OAuth2认证授权服务,将微服务中冗余的登录校验、签名校验单独剥离出来,这些校验与微服务自己的业务并没有太大的关系,所以这些功能完全可以独立成一个单独的服务存在。只是独立出来之后,并不是给每个微服务调用,而是通过API网关进行统一调用,来对微服务接口做前置过滤,实现对分布式系统中的其他的微服务接口的拦截和安全校验。
我们需要改造Spring Cloud的API网关服务项目,为项目增加OAuth2依赖,并且新增前置过滤器类,在前置过滤中对请求进行安全校验。流程如下:
- 用户请求某个资源前,需要先通过api网关访问Oauth2认证授权服务请求一个AccessToken
- 用户通过认证授权服务得到AccessToken后,通过api网关调用其他资源服务A、B、C
- 资源服务根据AccessToken从OAuth2认证授权服务验证该token的用户请求是否有效
2. 要做的事情
- 修改 producer-service 微服务项目,配置OAuth2认证授权服务地址,该项目被当做资源服务。
- 修改 api-gateway网关微服务项目,配置OAuth2,并且添加前置过滤器,对请求资源进行过滤和路由。
- 演示如何通过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
- 如果要在方法上使用注解来对请求进行过滤,要添加启用全局方法安全注解
@EnableGlobalMethodSecurity(prePostEnabled = true)