SpringCloud配置网关安全

网关安全

微服务整体架构图

avatar

相关功能组件

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

OAuth2

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

主要角色及步骤

角色:

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

认证步骤

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

avatar

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);
	}
}

版权声明:著作权归作者所有。

相关推荐

Python安全地创建多层嵌套目录

Python的pathlib包里的Path类提供了.mkdir()方法,我们使用它就可以安全地创建多层嵌套的目录。1、从pathlib导入Path类from pathlib import Path2、创建一个Path对象,并且以它将要创建的目录作为构造参数p = Path("/nested/directory")3、调用.mkdir()方法创建目录p.mkdir()如果目录是不存在的,这样就可以了

Spirng Security使用注解对方法做权限安全控制

 Spring Security默认情况下是关闭了对方法级的安全控制。可以通过xml或者是在添加了@Configuration注解的bean上添加@EnableGlobalMethodSecurity来开启对方法级别的安全控制。Spring Security 支持三种方法级注解, 分别是 JSR-205/Secured 注解/prePostEnabled,可以在开启对方法级的安全控制时设

Python安全创建目录的方法

在介绍Python安全创建目录之前,先举一个不安全创建目录的方式:if not os.path.exists(directory):     os.makedirs(directory) 在例子里,先判断目录是否存在,然后创建目录。这种方式是不安全的,它会导致竞争条件。在os.path.exists()和os.makedirs()之间的时

Java 8使用parallel Stream测试StringBuilder线程安全

我们知道StringBuilder不是线程安全的,但如何证明它非线程安全呢?测试StringBuilder是否线程安全一个简单的思路如下:模拟多个线程并发向StringBuilder实例添加字符,最后检测builder.toString().length()的值是否为添加字符的次数,如果非线程安全会出现builder.toString().length()的值与添加字符的次数不一致。Java&nb

[译]iOS11安全区布局指南(Safe Area Layout Guide)

Apple在iOS7为UIViewController新增了topLayoutGuide和bottomLayoutGuide属性。它们可以让你创建约束以避免内容被UIKit的横条,如状态、导航或标签栏覆盖。在iOS 11这些布局指南被废弃,并被单一的安全区布局指南代替。顶部和底部布局指南——概述这是使用顶部和底部布局指南在导航控制器和标签栏插入视图控制器的示例:绿色的内容视图相对底部锚点

Python 函数内修改关键字参数**kw的安全性考虑

Python定义函数常常会使用关键字参数**kw来接收字典值。基本语法def f(p1,p2,**kw) **kw将接收0个或任意个含参数名的参数,这些关键字参数在函数内部会自动组合为一个字典。示例def person(**kw)     for k in kw:    &nbs