api-gateway
api-gateway是一款轻量级、高性能、易扩展的基于zuul的网关产品,提供API的统一管理服务、涵盖API发布、管理、运维的全生命周期管理。对内辅助用户简单、快速、低成本、低风险的实现微服务聚合、前后端分离、系统集成等功能;对外面向合作伙伴、开发者开放服务。通过使用API-Gateway,我们能快速帮助用户实现传统ESB面临的主要场景,又能满足新型业务场景(移动应用等)所需的高性能、安全、可靠等要求。
#设置最大容错超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 90000
#设置最大超时时间
ribbon:
eager-load:
enabled: true
ServerListRefreshInterval: 10 #刷新服务列表源的间隔时间
httpclient:
enabled: false
okhttp:
enabled: true
ReadTimeout: 90000
ConnectTimeout: 90000
OkToRetryOnAllOperations: true
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 1
参考:https://www.jianshu.com/p/295e51bc1518
路由注册相关逻辑
org.springframework.cloud.netflix.zuul.filters.discovery.DiscoveryClientRouteLocator
启动时配置mapping等信息
zuul转发逻辑
以访问http://127.0.0.1:9200/api-user/users-anon/login?username=admin为例
总结
参考代码:https://gitee.com/owenwangwen/config-center/tree/master/apollo-gateway
https://github.com/ctripcorp/apollo-use-cases/tree/master/spring-cloud-zuul/src/main/java/com/ctrip/framework/apollo/use/cases/spring/cloud/zuul
com.open.capacity
uaa-client-spring-boot-starter
uaa-client-spring-boot-starter源码分析
security:
oauth2:
ignored: /test163/** , /api-auth/** , /doc.html ,/test111 ,/api-user/users-anon/login, /api-user/users/save, /user-center/users-anon/login,/document.html,**/v2/api-docs,/oauth/** ,/login.html ,/user/login,/**/**.css ,/**/**.js ,/getVersion
token:
store:
type: redis
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
String token = (String) authentication.getPrincipal();
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
}
Collection resourceIds = auth.getOAuth2Request().getResourceIds();
if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
}
checkClientDetails(auth);
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
// Guard against a cached copy of the same details
if (!details.equals(auth.getDetails())) {
// Preserve the authentication details from the one loaded by token services
details.setDecodedDetails(auth.getDetails());
}
}
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
@Bean
public OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler(ApplicationContext applicationContext) {
OAuth2WebSecurityExpressionHandler expressionHandler = new OAuth2WebSecurityExpressionHandler();
expressionHandler.setApplicationContext(applicationContext);
return expressionHandler;
}
相同用户,不同应用的权限隔离
客户端模式 :
客户端A 申请的token ,可以访问/api-user/menu/current ,
客户端B 申请的token,不让访问/api-user/menu/current
密码模式:
客户端模式 :
客户端A admin用户 申请的token ,可以访问/api-user/menu/current ,
客户端B admin用户 申请的token,不让访问/api-user/menu/current
参考issue:https://gitee.com/owenwangwen/open-capacity-platform/issues/IRG23
网关引依赖
网关是否开启基于应用隔离,代码注释了,只是基于token的合法性校验,按建议开启是否启用api接口服务权限
OpenAuthorizeConfigManager
游乐场买了通票,有些地方可以随便玩,有些地方另外
单独校验买票
config.anyRequest().authenticated() ;
//这种通票,token校验正确访问
config.anyRequest().access("@rbacService.hasPermission(request, authentication)"); //这种另外
单独校验,适用于网关对api权限校验
字段 | 备注 |
---|---|
if_limit | 是否需要控制访问次数 |
limit_count | 访问阀值 |
redis存储结构
加载oauth_client_details 到redis
应用访问次数控制过滤器
/**
* Created by owen on 2017/9/10. 根据应用 url 限流 oauth_client_details if_limit 限流开关
* limit_count 阈值
*/
@Component
public class RateLimitFilter extends ZuulFilter {
private static Logger logger = LoggerFactory.getLogger(RateLimitFilter.class);
private ThreadLocal error_info = new ThreadLocal();
@Autowired
private RedisLimiterUtils redisLimiterUtils;
@Autowired
private ObjectMapper objectMapper;
@Resource
SysClientServiceImpl sysClientServiceImpl;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if (!checkLimit(request)) {
logger.error("too many requests!");
error_info.set(Result.failedWith(null, 429, "too many requests!"));
serverResponse(ctx, 429);
return null;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/***
* 统一禁用输出
*
* @param ctx
* @param ret_message
* 输出消息
* @param http_code
* 返回码
*/
public void serverResponse(RequestContext ctx, int http_code) {
try {
ctx.setSendZuulResponse(false);
outputChineseByOutputStream(ctx.getResponse(), error_info);
ctx.setResponseStatusCode(http_code);
} catch (IOException e) {
StackTraceElement stackTraceElement= e.getStackTrace()[0];
logger.error("serverResponse:" + "---|Exception:" +stackTraceElement.getLineNumber()+"----"+ e.getMessage());
}
}
/**
* 使用OutputStream流输出中文
*
* @param request
* @param response
* @throws IOException
*/
public void outputChineseByOutputStream(HttpServletResponse response, ThreadLocal data) throws IOException {
/**
* 使用OutputStream输出中文注意问题: 在服务器端,数据是以哪个码表输出的,那么就要控制客户端浏览器以相应的码表打开,
* 比如:outputStream.write("中国".getBytes("UTF-8"));//使用OutputStream流向客户端浏览器输出中文,以UTF-8的编码进行输出
* 此时就要控制客户端浏览器以UTF-8的编码打开,否则显示的时候就会出现中文乱码,那么在服务器端如何控制客户端浏览器以以UTF-8的编码显示数据呢?
* 可以通过设置响应头控制浏览器的行为,例如: response.setHeader("content-type",
* "text/html;charset=UTF-8");//通过设置响应头控制浏览器以UTF-8的编码显示数据
*/
OutputStream outputStream = response.getOutputStream();// 获取OutputStream输出流
response.setHeader("content-type", "application/json;charset=UTF-8");// 通过设置响应头控制浏览器以UTF-8的编码显示数据,如果不加这句话,那么浏览器显示的将是乱码
/**
* data.getBytes()是一个将字符转换成字节数组的过程,这个过程中一定会去查码表,
* 如果是中文的操作系统环境,默认就是查找查GB2312的码表, 将字符转换成字节数组的过程就是将中文字符转换成GB2312的码表上对应的数字
* 比如: "中"在GB2312的码表上对应的数字是98 "国"在GB2312的码表上对应的数字是99
*/
/**
* getBytes()方法如果不带参数,那么就会根据操作系统的语言环境来选择转换码表,如果是中文操作系统,那么就使用GB2312的码表
*/
String msg = objectMapper.writeValueAsString(data.get());
byte[] dataByteArr = msg.getBytes("UTF-8");// 将字符转换成字节数组,指定以UTF-8编码进行转换
outputStream.write(dataByteArr);// 使用OutputStream流向客户端输出字节数组
}
public boolean checkLimit(HttpServletRequest request) {
// 解决zuul token传递问题
Authentication user = SecurityContextHolder.getContext().getAuthentication();
if (user != null) {
if (user instanceof OAuth2Authentication) {
try {
OAuth2Authentication athentication = (OAuth2Authentication) user;
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) athentication.getDetails();
String clientId = athentication.getOAuth2Request().getClientId();
Map client = sysClientServiceImpl.getClient(clientId);
if(client!=null){
String flag = String.valueOf(client.get("if_limit") ) ;
if("1".equals(flag)){
String accessLimitCount = String.valueOf(client.get("limit_count") );
if (!accessLimitCount.isEmpty()) {
Result result = redisLimiterUtils.rateLimitOfDay(clientId, request.getRequestURI(),
Long.parseLong(accessLimitCount));
if (-1 == result.getResp_code()) {
logger.error("token:" + details.getTokenValue() + result.getResp_msg());
// ((ResultMsg)
// this.error_info.get()).setMsg("clientid:" +
// client_id + ":token:" + accessToken + ":" +
// result.getMsg());
// ((ResultMsg) this.error_info.get()).setCode(401);
return false;
}
}
}
}
} catch (Exception e) {
StackTraceElement stackTraceElement= e.getStackTrace()[0];
logger.error("checkLimit:" + "---|Exception:" +stackTraceElement.getLineNumber()+"----"+ e.getMessage());
}
}
}
return true;
}
}
RedisLimiterUtils核心类
@Component
public class RedisLimiterUtils {
public static final String API_WEB_TIME_KEY = "time_key:";
public static final String API_WEB_COUNTER_KEY = "counter_key:";
private static final String EXCEEDS_LIMIT = "规定的时间内超出了访问的限制!";
private static Logger logger = LoggerFactory.getLogger(RedisLimiterUtils.class);
@Resource
RedisTemplate