SpringCloud配置网关安全

时间:2022-02-16 16:34:09

网关安全

微服务整体架构图

SpringCloud配置网关安全

相关功能组件

  • 服务注册与发现:consul,etcd,zookeeper,eureka
  • 配置中心:Spring Cloud Config,携程的阿波罗,duic

OAuth2

OAuth(Open Authorization,开放授权)是为用户资源的授权定义了一个安全、开放及简单的标准,第三方无需知道用户的账号及密码,就可获取到用户的授权信息

主要角色及步骤

角色:

  • 用户
  • 客户端应用
  • 认证服务器
  • 订单服务

认证步骤

  1. 用户直接访问客户端应用(可能是一个WebApp,也可能是一个手机App)
  2. 客户端应用从认证服务器获取令牌
  3. 客户端应用访问资源服务器
  4. 资源服务访问认证服务器来验证从客户端应用获取的请求中的令牌

SpringCloud配置网关安全

Oauth认证服务器服务端配置

使用Spring Security OAuth2 构建认证服务

  • 创建相关数据库表文件
create table oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY,
  resource_ids VARCHAR(256),
  client_secret VARCHAR(256),
  scope VARCHAR(256),
  authorized_grant_types VARCHAR(256),
  web_server_redirect_uri VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(256)
);

create table oauth_client_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256)
);

create table oauth_access_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication BLOB,
  refresh_token VARCHAR(256)
);

create table oauth_refresh_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication BLOB
);

create table oauth_code (
  code VARCHAR(256), authentication BLOB
);

create table oauth_approvals (
	userId VARCHAR(256),
	clientId VARCHAR(256),
	scope VARCHAR(256),
	status VARCHAR(10),
	expiresAt DATETIME,
	lastModifiedAt DATETIME
);
  • 配置认证服务基础类AuthorizationServerConfigurerAdapter
@Configuration
// 设置当前服务器用来作为授权服务
@EnableAuthorizationServer
public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {

	// 配置的认证控制器,自定义认证的流程,比如获取到用户名,查询数据库然后比较验证
	@Autowired
	private AuthenticationManager authenticationManager;


	// 配置的数据库信息
	@Autowired
	private DataSource dataSource;

	// 设置存储token为mysql数据库,默认存在内存中
	// 也可设置为Redis
	@Bean
	public TokenStore tokenStore() {
		return new JdbcTokenStore(dataSource);
	}

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints
			.tokenStore(tokenStore())
			.authenticationManager(authenticationManager);
	}
	// 配置客户端应用信息
	// 使用数据mysql数据库存储
	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		clients.jdbc(dataSource);
	}


	// 设置服务的策略为认证通过才能继续
	@Override
	public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
		security.checkTokenAccess("isAuthenticated()");
	}

}
  • 配置具体认证流程
@Configuration
@EnableWebSecurity
public class OAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {

	// 自定义的认证类,返回一个UserDetails对象
	@Autowired
	private UserDetailsService userDetailsService;

	// 密码转换
	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	// 继承 WebSecurityConfigurerAdapter 的验证方式
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService)
			.passwordEncoder(passwordEncoder());
	}


	// 继承 WebSecurityConfigurerAdapter 的验证方式
	@Bean
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}

}
  • 配置具体的用户信息类,返回UserDetails
@Component
public class UserDetailsServiceImpl implements UserDetailsService {

	@Autowired
	private PasswordEncoder passwordEncoder;

    // UserDetailsService的方法
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
	    // 这儿写死了,应该调用DB查询相关信息,然后返回对应的用户信息
		return User.withUsername(username)
					.password(passwordEncoder.encode("12345"))
					.authorities("ROLE_ADMIN")
					.build();
	}

}

  • 写具体的对象信息继承UserDetails,实现其中的方法
Oauth2服务使用验证
  • 配置服务为一个资源服务,相当于加了一个过滤器,到来的请求会发送到相应认证服务器服务端
@Configuration
// 声明为资源服务器
@EnableResourceServer
public class Oauth2ResourceServiceConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        // 设置当前资源服务器的名称
        resources.resourceId("order-server");
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated();
        // 这里可以自定义配置路由规则
        // http.authorizeRequests().antMatchers("/haha").permitAll();

    }
}
  • 配置认证服务器连接地址信息
@EnableWebSecurity
@Configuration
public class Oauth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public ResourceServerTokenServices tokenServices(){
        RemoteTokenServices tokenServices = new RemoteTokenServices();
        tokenServices.setClientId("orderService");
        tokenServices.setClientSecret("12345");
        tokenServices.setCheckTokenEndpointUrl("http://localhost:9090/oauth/check_token");

        return tokenServices;
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        OAuth2AuthenticationManager auth2AuthenticationManager = new OAuth2AuthenticationManager();
        auth2AuthenticationManager.setTokenServices(tokenServices());
        return auth2AuthenticationManager;
    }
}

这种认证方法会使得系统很臃肿,并且不好维护,一般我们都是在网关的地方进行认证。如下。

引入网关

将认证信息写在特定的资源服务里面的缺点:

  1. 认证代码与业务代码耦合度高,不便于修改
  2. 服务之间调用复杂

所以需要将认证的步骤放在网关内。便于控制

简单测试

引入zuul相关包
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

配置对应yaml文件

zuul:
  routes:
    token:
      url: http://localhost:9090
    order:
      url: http://localhost:9080
server:
    port: 9079

这样每次访问9070/token的接口都会转发到对应认证服务

正式步骤

  1. 配置认证过滤器,继承ZuulFilter实现相应方法
  2. 配置日志过滤器,记录响应访问日志
  3. 配置授权过滤器,对响应请求授权
  4. 配置流控设置
  5. 将获取到的用户信息设置到请求头中,转发到对应服务器
配置认证过滤器
@Slf4j
@Component
public class OAuthFilter extends ZuulFilter {

	private RestTemplate restTemplate = new RestTemplate();


	// 判断过滤器是不是起作用
	@Override
	public boolean shouldFilter() {
		return true;
	}


	@Override
	public Object run() throws ZuulException {

		log.info("oauth start");

		// 获取请求对象
		RequestContext requestContext = RequestContext.getCurrentContext();
		HttpServletRequest request = requestContext.getRequest();

		// 发往token服务器的请求,直接过去,因为现在还没有token
		if(StringUtils.startsWith(request.getRequestURI(), "/token")) {
			return null;
		}

		// 不是发往认证服务器的,就需要校验token
		String authHeader = request.getHeader("Authorization");
		// 没有认证信息 也继续往下走
		if(StringUtils.isBlank(authHeader)) {
			return null;
		}

		// 判断是否是Bearer认证
		if(!StringUtils.startsWithIgnoreCase(authHeader, "bearer ")) {
			return null;
		}

		try {
			// 获取token中的相关信息
			TokenInfo info = getTokenInfo(authHeader);
			request.setAttribute("tokenInfo", info);

		} catch (Exception e) {
			log.error("get token info fail", e);
		}

		return null;
	}

    // 发送请求到认证服务器,验证获取相关token信息
    // 这里的配置信息可以注入其他Bean来实现
    // 也可用consul等配置中心配置
	private TokenInfo getTokenInfo(String authHeader) {

		String token = StringUtils.substringAfter(authHeader, "Bearer ");
		String oauthServiceUrl = "http://localhost:9090/oauth/check_token";

		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
		headers.setBasicAuth("gateway", "12345");

		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
		params.add("token", token);

		HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers);

		ResponseEntity<TokenInfo> response = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, TokenInfo.class);

		log.info("token info :" + response.getBody().toString());

		return response.getBody();
	}

	// 过滤器类型 有四种 pre,post,error,route
	// pre: 业务逻辑执行之前,执行run里的逻辑
	// post : 执行之后...
	// error : 报错之后,执行...
	@Override
	public String filterType() {
		return "pre";
	}

	// 过滤器执行顺序
	@Override
	public int filterOrder() {
		return 1;
	}

}
配置日志过滤器
@Slf4j
@Component
public class AuditLogFilter extends ZuulFilter {

	@Override
	public boolean shouldFilter() {
		return true;
	}

    // 业务处理插入相关日志信息
	@Override
	public Object run() throws ZuulException {
		log.info("audit log insert");
		return null;
	}


	@Override
	public String filterType() {
		return "pre";
	}


	@Override
	public int filterOrder() {
		return 2;
	}

}
配置授权过滤器
@Slf4j
@Component
public class AuthorizationFilter extends ZuulFilter {

	@Override
	public boolean shouldFilter() {
		return true;
	}

    // 主要是将认证服务返回的token信息进行权限认证
    // 没有对应权限的话就调用 requestContext.setSendZuulResponse(false);
    // 设置zull网关不通过
    // 有对应权限就放开,发送到具体的地方
	@Override
	public Object run() throws ZuulException {

		log.info("authorization start");

		RequestContext requestContext = RequestContext.getCurrentContext();
		HttpServletRequest request = requestContext.getRequest();

		if(isNeedAuth(request)) {

			TokenInfo tokenInfo = (TokenInfo)request.getAttribute("tokenInfo");

			if(tokenInfo != null && tokenInfo.isActive()) {
				if(!hasPermission(tokenInfo, request)) {
					log.info("audit log update fail 403");
					handleError(403, requestContext);
				}

				requestContext.addZuulRequestHeader("username", tokenInfo.getUser_name());
			}else {
				// 去获取token的请求不做校验
				if(!StringUtils.startsWith(request.getRequestURI(), "/token")) {
					log.info("audit log update fail 401");
					handleError(401, requestContext);
				}
			}
		}

		return null;
	}

	private void handleError(int status, RequestContext requestContext) {
		requestContext.getResponse().setContentType("application/json");
		requestContext.setResponseStatusCode(status);
		requestContext.setResponseBody("{"message":"auth fail"}");
		// 调用这句话就不会向下执行了
		requestContext.setSendZuulResponse(false);
	}

	private boolean hasPermission(TokenInfo tokenInfo, HttpServletRequest request) {
		return true; //RandomUtils.nextInt() % 2 == 0;
	}

	private boolean isNeedAuth(HttpServletRequest request) {
		return true;
	}

	@Override
	public String filterType() {
		return "pre";
	}

	@Override
	public int filterOrder() {
		return 3;
	}

}
流控配置
  1. 引入相关包
<dependency>
	<groupId>com.marcosbarbero.cloud</groupId>
	<artifactId>spring-cloud-zuul-ratelimit</artifactId>
	<version>2.2.4.RELEASE</version>
</dependency>
  1. 配置对应application.yaml
zuul:
  routes:
    token:
      url: http://localhost:9090
    order:
      url: http://localhost:9080
  # 默认不会转发请求头包含Authorization 、cookie的服务
  # 这儿设置为空,都会转发
  sensitive-headers:
  ratelimit:
    enabled: true
    # 这里可以设置存储的方式
    repository: JPA
    default-policy-list:
    # 3秒中最多2个请求 限制请求数
    - limit: 2
      # 2个请求的处理时间加起来不能超过1s,在请求数量少,但处理时间长的情景中使用
      quota: 1
      refresh-interval: 3
      # 根据url、请求方式、ip去限制流量
      type:
        - url
        - httpmethod
        #- origin

上述的方法会根据自己的策略生成对应请求的限流控制,这种策略可以自己设置, 根据自己的规则,设置相关的key

@Component
public class MyKeyGen extends DefaultRateLimitKeyGenerator {

	public MyKeyGen(RateLimitProperties properties,
			RateLimitUtils rateLimitUtils) {
		super(properties, rateLimitUtils);
	}

	@Override
	public String key(HttpServletRequest request, Route route, Policy policy) {
		// TODO Auto-generated method stub
		return super.key(request, route, policy);
	}
}

当请求数操作限制被拦截的时候,可以自定义错误处理

@Component
public class MyRateLimitErrorHandler extends DefaultRateLimiterErrorHandler {

	@Override
	public void handleError(String msg, Exception e) {
		super.handleError(msg, e);
	}
}