scservers(十)监控管理-应用管理

前言

说到监控管理,springcloud 不应该把springboot admin落下,与actuator 结合提供日志,应用运行参数等的环境监控。

  • 显示 name/id 和版本号
  • 显示在线状态
  • Logging 日志级别管理
  • JMX beans 管理
  • Threads 会话和线程管理
  • Trace 应用请求跟踪
  • 应用运行参数信息,如:
    • Java 系统属性
    • Java 环境变量属性
    • 内存信息
    • Spring 环境属性

      ## admin 服务端
  • 依赖
<dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-server</artifactId>
            <version>${admin-server.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
  • 注解
@EnableAdminServer
public class AdminApplication {
    public static void main(String[] args) {
        SpringApplication.run(AdminApplication.class, args);
    }
}
  • 属性设置
 #spring boot admin的登陆账号和密码配置
  spring:
    security:
     user:
        name: admin
        password: 123456

admin 客户端

  • 依赖
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  • 注解

  • 属性设置

#Actuator 在 spring boot 2.0 版本后,只暴露了两个端点,下面开启所有端点
management:
  endpoints:
    web:
      exposure:
        include: '*'

即admin 通过actuator,mq 收集监控信息。

scservers(九)监控管理-断路器管理

前言

作为一个能上生产的系统,我觉得需要两个指标

  • 性能稳定
  • 安全运行

    前者考量设计编码能力,后者更加侧重后续监控管理,这往往被人忽略却异常重要。性能稳定需要性能优化,这个下次再讲,监控管理,我们需要对机器,应用,日志等各方面进行。这是框架应用运行良好生态系统。

    springcloud 在这方面做得很好,如springcloud admin,日志跟踪管理,甚至断路器监控管理。我们首先先补上断路器的监控管理,在前面讲熔断的时候就已经讲过部分,现在把它补全。

    ## Hystrix Dashboard

    在熔断篇已经有过,当时直接讲面板集成到了应用中,这里再说下其使用并讲面板应用独立出来。
  • 配置

    dashboard 为ui,不然不需要
    xml
    <dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-actuator</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>

    </dependency>

    添加注解
@EnableCircuitBreaker
@EnableHystrixDashboard

手动开启

feign: 
  hystrix:
    enabled: true
    
management:
  endpoints:
    web:
      exposure:
        include: hystrix.stream
  • 使用

    通过/hystrix访问dashboard界面,在这里可以通过输入各个应用的hystrix.stream地址来访问监控各应用的熔断情况。

Turbine

通过hystrix dashboard 可以监控各应用熔断情况,而turbine补充了对集群进行监控。我们通过mq 收集数据流的方式来继承。

  • 配置

    依赖
 <!--集群监控-->
        <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-turbine-stream</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
注解
@EnableTurbineStream

属性

turbine:
  stream:
    port: 8030
  • 服务调用者设置

    添加依赖
    xml
    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-netflix-hystrix-stream</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>

    </dependency>

scservers(八)网关-动态路由及管理

前言

前面我们已经弄了网关的基本路由鉴权等,初步的是个可运行的poc验证demo了,那么在上实际生产之前我们还要解决一个问题——静态路由,是的,之前我们在yml文件里面配置了静态的路由,在实际生产中,不可能每次增删改路由信息都要从新启动,那么我们就得做到路由是可以在运行时动态管理的。

我们将路由信息改为从Redis里面动态加载,并且增删动作放到一个独立的应用中,gateway-admin,而不是gateway-server,gateway-admin只需要内部使用。

动态路由实现

springcloud gateway支持这个动态路由的扩展,我们只需要实现RouteDefinitionRepository 这个接口,这个接口表示从存储设备中获取路由信息,我们这里选择实现从Redis获取。

  • gateway-server 路由动态获取
@Component
@Slf4j
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
    @Autowired
    private IRouteService routeService;
    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        return routeService.getRouteDefinitions();
    }
    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return routeService.save(route);
    }
    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeService.delete(routeId);
    }
}
@Service
@Slf4j
public class RouteService implements IRouteService {

    private static final String GATEWAY_ROUTES = "gateway_routes::";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private Map<String, RouteDefinition> routeDefinitionMaps = new HashMap<>();

    private void loadRouteDefinition() {
        Set<String> gatewayKeys = stringRedisTemplate.keys(GATEWAY_ROUTES + "*");

        if (CollectionUtils.isEmpty(gatewayKeys)) {
            return;
        }

        List<String> gatewayRoutes = Optional.ofNullable(stringRedisTemplate.opsForValue().multiGet(gatewayKeys)).orElse(Lists.newArrayList());
        gatewayRoutes.forEach(value -> {
            try {
                RouteDefinition routeDefinition = new ObjectMapper().readValue(value, RouteDefinition.class);
                routeDefinitionMaps.put(routeDefinition.getId(), routeDefinition);
            } catch (IOException e) {
                log.error(e.getMessage());
            }
        });
    }

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        loadRouteDefinition();
        return Flux.fromIterable(routeDefinitionMaps.values());
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> routeDefinitionMono) {
        return routeDefinitionMono.flatMap(routeDefinition -> {
            routeDefinitionMaps.put(routeDefinition.getId(), routeDefinition);
            return Mono.empty();
        });
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeId.flatMap(id -> {
            routeDefinitionMaps.remove(id);
            return Mono.empty();
        });
    }
}
  • gateway-admin 管理动态路由

    动态路由管理我们将初始路由设置到关系型数据库,并且加载到缓存Redis中,增删改的时候同时修改落地数据库及缓存。
@RestController
@RequestMapping("/gateway/routes")
@Api("gateway routes")
@Slf4j
public class GatewayRouteController {

    @Autowired
    private IGatewayRouteService gatewayRoutService;

    @ApiOperation(value = "新增网关路由", notes = "新增一个网关路由")
    @ApiImplicitParam(name = "gatewayRoutForm", value = "新增网关路由form表单", required = true, dataType = "GatewayRouteForm")
    @PostMapping
    public Result add(@Valid @RequestBody GatewayRouteForm gatewayRoutForm) {
        log.info("name:", gatewayRoutForm);
        GatewayRoute gatewayRout = gatewayRoutForm.toPo(GatewayRoute.class);
        return Result.success(gatewayRoutService.add(gatewayRout));
    }

    @ApiOperation(value = "删除网关路由", notes = "根据url的id来指定删除对象")
    @ApiImplicitParam(paramType = "path", name = "id", value = "网关路由ID", required = true, dataType = "long")
    @DeleteMapping(value = "/{id}")
    public Result delete(@PathVariable long id) {
        gatewayRoutService.delete(id);
        return Result.success();
    }

    @ApiOperation(value = "修改网关路由", notes = "修改指定网关路由信息")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "id", value = "网关路由ID", required = true, dataType = "long"),
            @ApiImplicitParam(name = "gatewayRoutForm", value = "网关路由实体", required = true, dataType = "GatewayRouteForm")
    })
    @PutMapping(value = "/{id}")
    public Result update(@PathVariable long id, @Valid @RequestBody GatewayRouteForm gatewayRoutForm) {
        GatewayRoute gatewayRout = gatewayRoutForm.toPo(GatewayRoute.class);
        gatewayRout.setId(id);
        gatewayRoutService.update(gatewayRout);
        return Result.success();
    }

    @ApiOperation(value = "获取网关路由", notes = "根据id获取指定网关路由信息")
    @ApiImplicitParam(paramType = "path", name = "id", value = "网关路由ID", required = true, dataType = "long")
    @GetMapping(value = "/{id}")
    public Result get(@PathVariable long id) {
        log.info("get with id:{}", id);
        return Result.success(new GatewayRouteVo(gatewayRoutService.get(id)));
    }

    @ApiOperation(value = "根据uri获取网关路由", notes = "根据uri获取网关路由信息,简单查询")
    @ApiImplicitParam(paramType = "query", name = "name", value = "网关路由路径", required = true, dataType = "string")
    @ApiResponses(
            @ApiResponse(code = 200, message = "处理成功", response = Result.class)
    )
    @GetMapping
    public Result get(@RequestParam String uri) {
        List<GatewayRouteVo> gatewayRoutesVo = gatewayRoutService.query(new GatewayRouteQueryParam(uri)).stream().map(GatewayRouteVo::new).collect(Collectors.toList());
        return Result.success(gatewayRoutesVo.stream().findFirst());
    }

    @ApiOperation(value = "搜索网关路由", notes = "根据条件查询网关路由信息")
    @ApiImplicitParam(name = "gatewayRoutQueryForm", value = "网关路由查询参数", required = true, dataType = "GatewayRouteQueryForm")
    @ApiResponses(
            @ApiResponse(code = 200, message = "处理成功", response = Result.class)
    )
    @PostMapping(value = "/conditions")
    public Result search(@Valid @RequestBody GatewayRouteQueryForm gatewayRouteQueryForm) {
        List<GatewayRoute> gatewayRoutes = gatewayRoutService.query(gatewayRouteQueryForm.toParam(GatewayRouteQueryParam.class));
        List<GatewayRouteVo> gatewayRoutesVo = gatewayRoutes.stream().map(GatewayRouteVo::new).collect(Collectors.toList());
        return Result.success(gatewayRoutesVo);
    }

    @ApiOperation(value = "重载网关路由", notes = "将所以网关的路由全部重载到redis中")
    @ApiResponses(
            @ApiResponse(code = 200, message = "处理成功", response = Result.class)
    )
    @PostMapping(value = "/overload")
    public Result overload() {
        return Result.success(gatewayRoutService.overload());
    }

}
@Service
@Slf4j
public class GatewayRouteService implements IGatewayRouteService {

    private static final String GATEWAY_ROUTES = "gateway_routes::";

    @Autowired
    private GatewayRouteMapper gatewayRouteMapper;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public long add(GatewayRoute gatewayRoute) {
        long gatewayId = gatewayRouteMapper.insert(gatewayRoute);
        stringRedisTemplate.opsForValue().set(GATEWAY_ROUTES + gatewayRoute.getId(), toJson(new GatewayRouteVo(gatewayRoute)));
        return gatewayId;
    }

    @Override
    public void delete(long id) {
        gatewayRouteMapper.delete(id);
        stringRedisTemplate.delete(GATEWAY_ROUTES + id);
    }

    @Override
    public void update(GatewayRoute gatewayRoute) {
        stringRedisTemplate.delete(GATEWAY_ROUTES + gatewayRoute.getId());
        stringRedisTemplate.opsForValue().set(GATEWAY_ROUTES, toJson(new GatewayRouteVo(get(gatewayRoute.getId()))));
    }

    @Override
    public GatewayRoute get(long id) {
        return gatewayRouteMapper.select(id);
    }

    @Override
    public List<GatewayRoute> query(GatewayRouteQueryParam gatewayRouteQueryParam) {
        return gatewayRouteMapper.query(gatewayRouteQueryParam);
    }

    @Override
    public boolean overload() {
        List<GatewayRoute> gatewayRoutes = gatewayRouteMapper.query(new GatewayRouteQueryParam());
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        gatewayRoutes.forEach(gatewayRoute ->
                opsForValue.set(GATEWAY_ROUTES + gatewayRoute.getId(), toJson(new GatewayRouteVo(gatewayRoute)))
        );
        return true;
    }

    /**
     * GatewayRoute转换为json
     *
     * @param gatewayRouteVo redis需要的vo
     * @return json string
     */
    private String toJson(GatewayRouteVo gatewayRouteVo) {
        String routeDefinitionJson = Strings.EMPTY;
        try {
            routeDefinitionJson = new ObjectMapper().writeValueAsString(gatewayRouteVo);
        } catch (JsonProcessingException e) {
            log.error("网关对象序列化为json String", e);
        }
        return routeDefinitionJson;
    }
}

oauth2 with spring cloud security (转载)

因项目需要,需要和三方的oauth2服务器进行集成。网上关于spring cloud security oauth2的相关资料,一般都是讲如何配置,而能把这块原理讲透彻的比较少,这边自己做一下总结和整理,顺带介绍一下JWT的使用场景。

什么是OAuth2?

OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该令牌在限定时间、限定范围访问指定资源。主要涉及的RFC规范有RFC6749(整体授权框架),RFC6750(令牌使用),RFC6819(威胁模型)这几个,一般我们需要了解的就是RFC6749。获取令牌的方式主要有四种,分别是授权码模式,简单模式,密码模式和客户端模式,如何获取token不在本篇文章的讨论范围,我们这里假定客户端已经通过某种方式获取到了access_token,想了解具体的oauth2授权步骤可以移步阮一峰老师的理解OAuth 2.0,里面有非常详细的说明。

这里要先明确几个OAuth2中的几个重要概念:

  • resource owner: 拥有被访问资源的用户

  • user-agent: 一般来说就是浏览器

  • client: 第三方应用

Authorization server: 认证服务器,用来进行用户认证并颁发token

Resource server:资源服务器,拥有被访问资源的服务器,需要通过token来确定是否有权限访问

明确概念后,就可以看OAuth2的协议握手流程,摘自RFC6749



Abstract Protocol Flow.png

什么是Spring Security?

Spring Security是一套安全框架,可以基于RBAC(基于角色的权限控制)对用户的访问权限进行控制,核心思想是通过一系列的filter chain来进行拦截过滤,以下是ss中默认的内置过滤器列表,当然你也可以通过custom-filter来自定义扩展filter chain列表

Alias

Filter Class

Namespace Element or Attribute

CHANNEL_FILTER

ChannelProcessingFilter

http/intercept-url@requires-channel

SECURITY_CONTEXT_FILTER

SecurityContextPersistenceFilter

http

CONCURRENT_SESSION_FILTER

ConcurrentSessionFilter

session-management/concurrency-control

HEADERS_FILTER

HeaderWriterFilter

http/headers

CSRF_FILTER

CsrfFilter

http/csrf

LOGOUT_FILTER

LogoutFilter

http/logout

X509_FILTER

X509AuthenticationFilter

http/x509

PRE_AUTH_FILTER

AbstractPreAuthenticatedProcessingFilter

N/A

CAS_FILTER

CasAuthenticationFilter

N/A

FORM_LOGIN_FILTER

UsernamePasswordAuthenticationFilter

http/form-login

BASIC_AUTH_FILTER

BasicAuthenticationFilter

http/http-basic

SERVLET_API_SUPPORT_FILTER

SecurityContextHolderAwareRequestFilter

http/@servlet-api-provision

JAAS_API_SUPPORT_FILTER

JaasApiIntegrationFilter

http/@jaas-api-provision

REMEMBER_ME_FILTER

RememberMeAuthenticationFilter

http/remember-me

ANONYMOUS_FILTER

AnonymousAuthenticationFilter

http/anonymous

SESSION_MANAGEMENT_FILTER

SessionManagementFilter

session-management

EXCEPTION_TRANSLATION_FILTER

ExceptionTranslationFilter

http

FILTER_SECURITY_INTERCEPTOR

FilterSecurityInterceptor

http

SWITCH_USER_FILTER

SwitchUserFilter

N/A

这里面最核心的就是FILTER_SECURITY_INTERCEPTOR,通过FilterInvocationSecurityMetadataSource来进行资源权限的匹配,AccessDecisionManager来执行访问策略。

认证与授权(Authentication and Authorization)

一般意义来说的应用访问安全性,都是围绕认证(Authentication)和授权(Authorization)这两个核心概念来展开的。即首先需要确定用户身份,在确定这个用户是否有访问指定资源的权限。认证这块的解决方案很多,主流的有CAS、SAML2、OAUTH2等(不巧这几个都用过-_-),我们常说的单点登录方案(SSO)说的就是这块,授权的话主流的就是spring security和shiro。shiro我没用过,据说是比较轻量级,相比较而言spring security确实架构比较复杂。

Spring Cloud Security Oauth2认证流程

将OAuth2和Spring Security集成,就可以得到一套完整的安全解决方案。

为了便于理解,现在假设有一个名叫“脸盆网”的社交网站,用户在首次登陆时会要求导入用户在facebook的好友列表,以便于快速建立社交关系。具体的授权流程如下:

用户登陆脸盆网,脸盆网试图访问facebook上的好友列表

脸盆网发现该资源是facebook的受保护资源,于是返回302将用户重定向至facebook登陆页面

用户完成认证后,facebook提示用户是否将好友列表资源授权给脸盆网使用(如果本来就是已登陆facebook状态则直接显示是否授权的页面)

用户确认后,脸盆网通过授权码模式获取了facebook颁发的access_token

脸盆网携带该token访问facebook的获取用户接口https://api.facebook.com/user,facebook验证token无误后返回了与该token绑定的用户信息

脸盆网的spring security安全框架根据返回的用户信息构造出了principal对象并保存在session中

脸盆网再次携带该token访问好友列表,facebook根据该token对应的用户返回该用户的好友列表信息

该用户后续在脸盆网发起的访问facebook上的资源,只要在token有效期及权限范围内均可以正常获取(比如想访问一下保存在facebook里的相册)

不难看出,这个假设的场景中,脸盆网就是第三方应用(client),而facebook既充当了认证服务器,又充当了资源服务器。这个流程里面有几个比较重要的关键点,我需要重点说一下,而这也是其他的涉及spring security与OAuth2整合的文章中很少提及的,很容易云里雾里的地方。

细心的同学应该发现了,其实在标准的OAuth2授权过程中,5、6、8这几步都不是必须的,从上面贴的RFC6749规范来看,只要有1、2、3、4、7这几步,就完成了被保护资源访问的整个过程。事实上,RFC6749协议规范本身也并不关心用户身份的部分,它只关心token如何颁发,如何续签,如何用token访问被保护资源(facebook只要保证返回给脸盆网的就是当前用户的好友,至于当前用户是谁脸盆网不需要关心)。那为什么spring security还要做5、6这两步呢?这是因为spring security是一套完整的安全框架,它必须关心用户身份!在实际的使用场景中,OAuth2一般不仅仅用来进行被保护资源的访问,还会被用来做单点登陆(SSO)。在SSO的场景中,用户身份无疑就是核心,而token本身是不携带用户信息的,这样client就没法知道认证服务器发的token到底对应的是哪个用户。设想一下这个场景,脸盆网不想自建用户体系了,想直接用facebook的用户体系,facebook的用户和脸盆网的用户一一对应(其实在很多中小网站现在都是这种模式,可以选择使用微信、QQ、微博等网站的用户直接登陆),这种情况下,脸盆网在通过OAuth2的认证后,就希望拿到用户信息了。所以现在一般主流的OAuth2认证实现,都会预留一个用户信息获取接口,就是上面提到的https://api.facebook.com/user(虽然这不是OAuth2授权流程中必须的),这样client在拿到token后,就可以携带token通过这个接口获取用户信息,完成SSO的整个过程。另外从用户体验的角度来说,如果获取不到用户信息,则意味者每次要从脸盆网访问facebook的资源,都需要重定向一次进行认证,用户体验也不好。

OAuth2与SSO

首先要明确一点,OAuth2并不是一个SSO框架,但可以实现SSO功能。以下是一个使用github作为OAuth2认证服务器的配置文件

server:

port: 11001

security:

user:

password: user # 直接登录时的密码

ignored: /

sessions: never # session策略

oauth2:

sso:

loginPath: /login # 登录路径

client:

clientId: c40fb56cb4sdsdsdsd

clientSecret: c910ec22981daa28e1b59c778sdfjh73j3

accessTokenUri: https://github.com/login/oauth/access_token

userAuthorizationUri: https://github.com/login/oauth/authorize

resource:

userInfoUri: https://api.github.com/user

preferTokenInfo: false

可以看到accessTokenUri和userAuthorizationUri都是为了完成OAuth2的授权流程所必须的配置,而userInfoUri则是spring security框架为了完成SSO所必须要的。所以总结一下就是:通过将用户信息这个资源设置为被保护资源,可以使用OAuth2技术实现单点登陆(SSO),而Spring Security OAuth2就是这种OAuth2 SSO方案的一个实现。

Spring Security在调用user接口成功后,会构造一个OAuth2Authentication对象,这个对象是我们通常使用的UsernamePasswordAuthenticationToken对象的一个超集,里面封装了一个标准的UsernamePasswordAuthenticationToken,同时在detail中还携带了OAuth2认证中需要用到的一些关键信息(比如tokenValue,tokenType等),这时候就完成了SSO的登陆认证过程。后续用户如果再想访问被保护资源,spring security只需要从principal中取出这个用户的token,再去访问资源服务器就行了,而不需要每次进行用户授权。这里要注意的一点是此时浏览器与client之间仍然是通过传统的cookie-session机制来保持会话,而非通过token。实际上在SSO的过程中,使用到token访问的只有client与resource server之间获取user信息那一次,token的信息是保存在client的session中的,而不是在用户本地。这也是之前我没搞清楚的地方,以为浏览器和client之间也是使用token,绕了不少弯路,对于Spring Security来说,不管是用cas、saml2还是Oauth2来实现SSO,最后和用户建立会话保持的方式都是一样的。

OAuth2 SSO与CAS、SAML2的比较

根据前面所说,大家不难看出,OAuth2的SSO方案和CAS、SAML2这样的纯SSO框架是有本质区别的。在CAS和SAML2中,没有资源服务器的概念,只有认证客户端(需要验证客户信息的应用)和认证服务器(提供认证服务的应用)的概念。在CAS中这叫做cas-client和cas-server,SAML2中这叫做Service Providers和Identity Provider,可以看出CAS、SAML2规范天生就是为SSO设计的,在报文结构上都考虑到了用户信息的问题(SAML2规范甚至还带了权限信息),而OAuth2本身不是专门为SSO设计的,主要是为了解决资源第三方授权访问的问题,所以在用户信息方面,还需要额外提供一个接口。

Authorization Server与Resource Server分离

脸盆网的这个例子中,我们看到资源服务器和认证服务器是在一起的(都是facebook),在互联网场景下一般你很难找到一个独立的、权威的、第三方的认证中心(你很难想像腾讯的QQ空间通过支付宝的认证中心去授权,也很难想像使用谷歌服务要通过亚马逊去授权)。但是如果是在公司内部,这种场景其实是很多的,尤其在微服务架构下,有大量服务会对外提供资源访问,他们都需要做权限控制。那么最合理的当然就是建立一个统一的认证中心,而不是每个服务都做一个认证中心。我们前面也介绍了,token本身是不携带用户信息的,在分离后resouce server在收到请求后,如何检验token的真实性?又如何从token中获取对应的用户信息?这部分的介绍网上其实非常少,幸好我们可以直接从官方文档获取相关的蛛丝马迹,官方文档对于resouce server的配置是这样描述的:

security:

oauth2:

resource:

userInfoUri: https://api.github.com/user

preferTokenInfo: false

寥寥数语,但已经足够我们分析了。从这个配置可以看出,client在访问resource server的被保护资源时,如果没有携带token,则资源服务器直接返回一个401未认证的错误





Full authentication is required to access this resource



unauthorized

如果携带了token,则资源服务器会使用这个token向认证服务器发起一个用户查询的请求,若token错误或已经失效,则会返回



49e2c7d8720738cfb75f6b675d62e5ecd66

invalid_token

若token验证成功,则认证服务器向资源服务器返回对应的用户信息,此时resource server的spring security安全框架就可以按照标准的授权流程进行访问权限控制了。

认证与授权的解耦

从这个流程中我们可以看出,通过OAuth2进行SSO认证,有一个好处是做到了认证与授权的解耦。从日常的使用场景来说,认证比较容易做到统一和抽象,毕竟你就是你,走到哪里都是你,但是你在不同系统里面的角色,却可能千差万别(家里你是父亲,单位里你是员工,父母那里你是子女)。同时角色的设计,又是和资源服务器的设计强相关的。从前面的配置中不难发现,如果希望获得为不同资源服务器设计的角色,你只需要替换https://api.facebook.com/user这个配置就行了,这为我们的权限控制带来了更大的灵活性,而这是传统的比如SAML2这样的SSO框架做不到的。

JWT介绍

终于来到了著名的JWT部分了,JWT全称为Json Web Token,最近随着微服务架构的流行而越来越火,号称新一代的认证技术。今天我们就来看一下,jwt的本质到底是什么。

我们先来看一下OAuth2的token技术有没有什么痛点,相信从之前的介绍中你也发现了,token技术最大的问题是不携带用户信息,且资源服务器无法进行本地验证,每次对于资源的访问,资源服务器都需要向认证服务器发起请求,一是验证token的有效性,二是获取token对应的用户信息。如果有大量的此类请求,无疑处理效率是很低的,且认证服务器会变成一个中心节点,对于SLA和处理性能等均有很高的要求,这在分布式架构下是很要命的。

JWT就是在这样的背景下诞生的,从本质上来说,jwt就是一种特殊格式的token。普通的oauth2颁发的就是一串随机hash字符串,本身无意义,而jwt格式的token是有特定含义的,分为三部分:

头部Header

载荷Payload

签名Signature

这三部分均用base64进行编码,当中用.进行分隔,一个典型的jwt格式的token类似xxxxx.yyyyy.zzzzz。关于jwt格式的更多具体说明,不是本文讨论的重点,大家可以直接去官网查看官方文档,这里不过多赘述。

相信看到签名大家都很熟悉了,没错,jwt其实并不是什么高深莫测的技术,相反非常简单。认证服务器通过对称或非对称的加密方式利用payload生成signature,并在header中申明签名方式,仅此而已。通过这种本质上极其传统的方式,jwt可以实现分布式的token验证功能,即资源服务器通过事先维护好的对称或者非对称密钥(非对称的话就是认证服务器提供的公钥),直接在本地验证token,这种去中心化的验证机制无疑很对现在分布式架构的胃口。jwt相对于传统的token来说,解决以下两个痛点:

通过验证签名,token的验证可以直接在本地完成,不需要连接认证服务器

在payload中可以定义用户相关信息,这样就轻松实现了token和用户信息的绑定

在上面的那个资源服务器和认证服务器分离的例子中,如果认证服务器颁发的是jwt格式的token,那么资源服务器就可以直接自己验证token的有效性并绑定用户,这无疑大大提升了处理效率且减少了单点隐患。

JWT适用场景与不适用场景

就像布鲁克斯在《人月神话》中所说的名言一样:“没有银弹”。JWT的使用上现在也有一种误区,认为传统的认证方式都应该被jwt取代。事实上,jwt也不能解决一切问题,它也有适用场景和不适用场景。

适用场景:

一次性的身份认证

api的鉴权

这些场景能充分发挥jwt无状态以及分布式验证的优势

不适用的场景:

传统的基于session的用户会话保持

不要试图用jwt去代替session。这种模式下其实传统的session+cookie机制工作的更好,jwt因为其无状态和分布式,事实上只要在有效期内,是无法作废的,用户的签退更多是一个客户端的签退,服务端token仍然有效,你只要使用这个token,仍然可以登陆系统。另外一个问题是续签问题,使用token,无疑令续签变得十分麻烦,当然你也可以通过redis去记录token状态,并在用户访问后更新这个状态,但这就是硬生生把jwt的无状态搞成有状态了,而这些在传统的session+cookie机制中都是不需要去考虑的。这种场景下,考虑高可用,我更加推荐采用分布式的session机制,现在已经有很多的成熟框架可供选择了(比如spring session)。

scservers(六) 网关-路由

前言

前面的五篇内容已经构成了微服务的基本核心框架,已经可以结合业务,加上存储提供服务,并且整个系统内部服务之间也具备相互识别调用路由能力,那么再将这些服务汇总提供给其他第三方(不管是自己客户端(如h5,mobile app)还是作为开放平台供第三方使用),还是需要一个汇总的接口出口点。网关的需求自然形成。我们需要再提供了一个外部路由的能力。

业务网关

网关我们可以分位接入网关(连接的保持、消息的解析(卸载证书等)、消息的分发)+应用网关(路由,鉴权,限流等功能性网关的能力基础安全通用能力)。方案挺多:

* nginx(f5) + zuul(or) gateway:

nginx卸载https安全证书,高并发连接能力,反向代理能力。

gateway 功能路由鉴权,限流等包含业务功能的网关能力。

* kong(nginx基础上的包含功能性网关及业务网关能力)

nginx 我们应该用得多,比较熟悉,这里主要讲gateway的业务网关附带的一些其他能力。

sprincloud gateway

之前springcloud 一直使用zuul1.0作为其网关,后来鉴于性能(同步模型)及zuul2跳票等可能原因,社区推出了自己的网关,名字直接明了,gateway,同时还支持webflux,整合stream流等功能特性。那么我们就直接使用它来讲解吧。

  • 配置

    依赖

    <!--api网关-->
        <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!--redis限流-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    属性配置(静态路由配置):

    spring: 
      cloud:
    gateway:
    discovery:
    locator:
    enabled: true
    routes:
    #网关路由到订单服务order-service
    - id: demo-ribbon-consumer
    uri: lb://center-ribbon-demo
    predicates:
    - Path=/test/ribbon/**
    default-filters:
    - name: Retry
    args:
    retries: 3
    statuses: BAD_GATEWAY
    - AddResponseHeader=X-Response-Default-Foo, Default-Bar
    - name: RequestRateLimiter
    args:
    redis-rate-limiter.replenishRate: 1 #令牌桶的容积
    redis-rate-limiter.burstCapacity: 1 #流速 每秒
    rate-limiter: "#{@defaultRedisRateLimiter}" #SPEL表达式去的对应的bean
    key-resolver: "#{@remoteAddressKeyResolver}" #SPEL表达式去的对应的bean
  • 使用

    限流

    限流可以在nginx接入就做.gateway 也提供了限流的组件集成,基于redis做的。基本使用:

    主要查看属性配置中的:

    default-filters:
    - name: RequestRateLimiter   
    

    限流基于令牌桶算法,参数有

    * 令牌桶总容量:redis-rate-limiter.replenishRate

    * 令牌桶每秒填充平均速率:流速 每秒:redis-rate-limiter.burstCapacity

    上面两个参数只需要配置,下面两个这两个参数

    * 限流关键字对应:key-resolver:

    * 限流

    需要编写类代码:

    @Component
    public class RequestRateLimiterConfig {
    
  • 令牌桶算法

    令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解。随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务。

令牌桶的另外一个好处是可以方便的改变速度。一旦需要提高速率,则按需提高放入桶中的令牌的速率。一般会定时(比如 100 毫秒)往桶中增加一定数量的令牌,有些变种算法则实时的计算应该增加的令牌的数量。

scservers(五) 服务调用——安全,熔断,限流,降级

前言

服务调用是分布式系统基础,因为微服务的理念,不管粒度如何划分,系统功能间交互调用都会复杂起来,基础服务不管什么原因挂掉,容易就造成整个系统的不可用,这就是雪崩效应。原因是服务不可用,不断重试,造成服务调用者因为同步等待造成的资源耗尽也不可用,扩大后就造成所有服务都不可用了。

解决的策略基本有如下几种:

  • 限流

    按照漏斗模型,从前向后的限流

    • 用户交互限流
    • 网关限流
    • 服务调用关闭重试
  • 改进缓存
    • 缓存预加载
    • 同步改异步刷新
  • 服务自动扩容
    • 可以在docker 容器层面解决
  • 降级

    服务调用者降级服务

    • 资源即调用服务的线程池的隔离
    • 熔断,通过阈值设置失败的服务不再继续请求
    • 快速失败:通过超时机制, 熔断器 进行快速失败
    • 快速失败根据分类可以是应答错误也可以是应答缓存or默认数据

在上述策略中,微服务系统框架可以做的主要:

  • 可用情况下预防:超时处理,进行限流,资源隔离,分类,将不重要业务降级。
  • 不可用情况下直接隔离,熔断,降级等的操作。

而Springcloud 就为我们提供了Hystrix来保护我们的系统,可以将请求隔离,针对服务限流,当服务不可用时能够熔断并降级,防止级联故障。

Hystrix处理策略

当我们使用了Hystrix时,Hystrix将所有的外部调用都封装成一个HystrixCommand或者HystrixObservableCommand对象,这些外部调用将会在一个独立的线程中运行。我们可以将出现问题的服务通过熔断、降级等手段隔离开来,这样不影响整个系统的主业务。

  • 资源隔离

    通过线程池来实现资源隔离。通常在使用的时候我们会根据调用的远程服务划分出多个线程池。例如调用产品服务的 Command 放入 A 线程池,调用账户服务的 Command 放入 B 线程池。这样做的主要优点是运行环境被隔离开了。这样就算调用服务的代码存在 bug 或者由于其他原因导致自己所在线程池被耗尽时,不会对系统的其他服务造成影响。
  • 超时机制

    如果我们加入超时机制,例如2s,那么超过2s就会直接返回了,那么这样就在一定程度上可以抑制消费者资源耗尽的问题

  • 服务限流

    通过线程池+队列的方式,通过信号量的方式。比如商品评论比较慢,最大能同时处理10个线程,队列待处理5个,那么如果同时20个线程到达的话,其中就有5个线程被限流了,其中10个先被执行,另外5个在队列中

  • 服务熔断

    这个熔断可以理解为我们自己家里的电闸。

    当依赖的服务有大量超时时,在让新的请求去访问根本没有意义,只会无畏的消耗现有资源,比如我们设置了超时时间为1s,如果短时间内有大量请求在1s内都得不到响应,就意味着这个服务出现了异常,此时就没有必要再让其他的请求去访问这个服务了,这个时候就应该使用熔断器避免资源浪费

  • 服务降级

    有服务熔断,必然要有服务降级。

    所谓降级,就是当某个服务熔断之后,服务将不再被调用,此时客户端可以自己准备一个本地的fallback(回退)回调,返回一个缺省值。 例如:(备用接口/缓存/mock数据),这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强,当然这也要看适合的业务场景

Hystrix配置使用

  • hystrix & ribbon

    • 配置

      依赖:

       <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-hystrix</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-consul-discovery</artifactId>
      </dependency>

      属性设置:

    • 注解式使用

      • step1:使用注解 @EnableHystrix 启用豪猪断路器功能。
      • step2:在需要降级的接口上添加注解,并添加参数类型相同的降级函数,返回自定义信息

        @HystrixCommand(fallbackMethod = "ribbonfallback")
        @RequestMapping("/test/ribbon")
        public String testconfigrpc(String name) {
        return "RestTemplate+Ribbon get username <<==>> "+restTemplate.getForObject("http://center-demo/test/config/username",String.class);
        }
        public String ribbonfallback(String name) {
        return "ribbonFallback default name :ribbonFallback";
        }
    • 监控

      host:port/hystrix

      此处只是简单的单个应用监控,显然不适合大规模集群监控,后续的多个客户端监控放到后面专门的监控章节去讲吧。

  • hystrix & feign

    • 配置

      依赖

      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
      </dependency>

      属性配置

      feign: 
        hystrix:
      enabled: true
    • 注解式使用

      • step1: 引入注解 @EnableFeignClients,其已经包含启用断路器注解
      • step2:在ribbon中我们使用注解降级函数,在feign中我们使用注解降级类

        @FeignClient( name = "center-demo", path = "/test", decode404 = true,fallback = DemoFallbackFeign.class)
        public interface DemoFeignclient {  @GetMapping("/config")
        String selectconfigByName(@RequestParam String name);
        @GetMapping("/token/back")
        String gettokenback();
        }
        @Component
        public class DemoFallbackFeign implements DemoFeignclient {
        @Override
        public String selectconfigByName(String name) {
        // TODO Auto-generated method stub
        return "FeignFallbackName";
        }
        @Override
        public String gettokenback() {
        // TODO Auto-generated method stub
        return "FeignFallbackToken";
        }
        }
  • 源码

fabric简介

1.设计目的

2.基本架构流程

3.基本组成介绍

4.接口

5.示例

设计目的范围

是一个企业级联盟链,主要考虑商业应用对安全、隐私、监管、审计、性能的需求,提高准入门槛,增加了安全、隐私、可监管审计等商业特性,是区块链技术在商业领域的应用探索。

基本框架流程

image

 

 

逻辑概念:客户端,会员服务,peer节点(背书节点,确认节点,非交易节点),共识节点

CA服务:

上图中最左边是证书服务系统,主要提供会员注册和证书颁发功能,Fabric系统的参与方都必须经过授权,比如Orderer、Peer、Client等都需要拥有受信任的证书。证书一方面用于系统接入,另一方面用于交易签名。所以统一的证书服务非常重要。会员证书又分为注册证书和交易证书,注册证书与会员信息关联在一起,用于标识会员的身份,在必要的时候,还可以支持监管和审计;交易证书用于交易签名,之所以交易要用不同的证书,是为了避免会员的个人信息和交易信息被泄露,比如交易内容或者多笔交易之间的关联关系等,另外交易证书可以申请多份,甚至可以为每一笔交易申请一份交易证书。

client:经过CA认证的业务应用客户端

PEER:

背书节点:使用chaincode预交易,并应用背书策略对交易签名背书。

确认节点:从共识节点接收到块后,对块数据进行校验,然后写入区块中,交易状态信息记录入状态数据库,交易可能失败,对块交易记录。(特别是同一资源的高并发处理,及其容易交易失败,虽然交易数据得到了共识,但是业务的强一致性还是会因为分布式后没法得到保障)

共识节点:对交易进行排序共识,完成后推送到相应的节点

共识网络:共识节点组成共识网络

多链:通过建立不同通道,将数据隔离,加入通道的节点与共识节点共同组成一个链,共识网络对不同通道生成不同的块。

基本组件及特点

组件化,可扩展,可拔插

CA服务:支持PKI系列的证书

共识网络:支持KAFKA,PBFT,SBFT

账本:区块文件 & 状态库 :leveldb,couchdb

链码(合约):可用go,java,nodejs编写,运行在docker容器中,合约就两个接口,init+invoke,用户合约不能创建新合约(由系统生命周期合约统一管理)。0.6版本不是跨链调用合约,现在1.0版本没试过。

底层通信:grpc+pb

接口

0.6版本可直接用restful进行接口调试

1.0 版本我们看流程,客户端需要两段式的递交信息,所以目前是不支持使用postman这种http.restful 进行接口调试,只能应用SDK或封装好的client中进行。

示例

部署示例:理论上支持全平台,但是还是直接在ubuntu上会比较轻松,需要golang,docker,docker-compase,

开发示例:之前都在windows+vagant 虚拟化开发环境,

安装测试示例:

物理架构节点:

1cli+4peers+1orderer

逻辑

2个组织,每个组织2个节点,加入一个通道

步骤

组网:

1.编写配置文件,使用cryptogen 生成相应证书。

2.建立通道,将节点加入通道

在网络上测试:

1.创建合约,并在特定链上实例化一个合约

2.可进行交易

测试需要在cli上操作:

创建合约:

peer chaincode install -n zwrcc -v 1.0 -p github.com/hyperledger/fabric/examples/chaincode/go/zwr

实例化合约:

peer chaincode instantiate -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/cacerts/ca.example.com-cert.pem -C mychannel -n zwrcc -v 1.0 -p github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02 -c '{"Args":["init","zwr", "1000", "mmm","2000"]}' -P "OR ('Org1MSP.member','Org2MSP.member')"

查询:

peer chaincode query -C mychannel -n zwrcc -c '{"Args":["query","zwr"]}'

交易:

peer chaincode invoke -o orderer.example.com:7050  --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/cacerts/ca.example.com-cert.pem  -C mychannel -n zwrcc -c '{"Args":["invoke","mmm","zwr","100"]}'

reading—《高绩效教练》

Coach,在足球场上,为什么很多非科班的人也能教导好一个球队,比如狂人穆里尼奥。

前文的例子不能证明说科班出身的教练不行,但是可以得到教练不一定需要非常在行,因为很多教练虽然科班出身,但是他们在球员时代也并非顶级的存在。

很多时候我们会碰到这样的问题,当有人来跟我们针对某事寻求帮助的时候,这时候你职责就是教练。但是日常经验告诉我们,不管我们对该事情给出何种建议评判,对方都很难满意,有时候他们还会跟你顶牛。

WHY?

因为当你给出具体建议的时候,解决这个事情的责任已经转移到我们头上了,但事实上,球场上去踢球进球的还是球员。所以很多时候,当别人来寻求我们帮助的时候,绝大部分其实他们内心深处是有答案的,所以当我们处在教练身份,我们只需要引导对方把这个深藏心底的答案引导出来,让对方自己承担起这个责任。那么如何做么?

HOW?

1.通过提问让对方自己明确目标

2.通过提问让对方认清现状

3.通过提问让对方分解操作步骤

最后,再通过提问让对方确保会执行这些步骤。

最重要的是, 保留自己的意见,因为那是狗屁。

不要评判,不要建议,不要介入。让对方自己承担起责任。因为要实现目标的是对方不是你。你要做的就是理清思路。不要告诉对方要做什么,不要命令,只需要激发对方就好了。因为这也是更加有尊严,更加能自我实现的一种方式。

所以当别人问你问题的时候,你的回答只需要是:

你觉得呢?

日出健身,logo设计过程

品牌名:日出健身

品牌关键词:运动、健身、跑步、力量、健康、好身材、自信、阳光、蜕变、独立

用户人群:20-35岁,男性与女性

品牌性格:完美、自信、阳光、美丽

Slogan:蜕变,从日出开始!

====================================================

草图构思

方向1: get+时间+太阳。贴近潮流一点,有一定的号召力

方向2:太阳+树叶。 比较靠近自然一点

方向3:地平线。也比较直白,有说服力,后期表现力会有很多空间和可能

方向4:首字母S 。可以是瘦身S曲线,可以是道路,同时也可以做出速度感等

bb5-1

bb1-1

bb2-1

 

选好两个主要方向,开始AI绘制

QQ图片20151113170000

QQ截图20151113165025

△这个S,根据手绘效果做出来,边角有硬朗的棱角,又有宽边圆润的导角,刚中带柔

 

QQ截图20151113164220

△这个S是改变过,圆润的S ,带着10度的倾斜,让他又充满速度感

两个相对,我更喜欢下面这个。

 

乱七八糟的IT

自从踏入IT这一行,就不断的听到高大尚的各类名词,GOF设计模式,AOP架构,RESTFUL架构风格,刚开始听起来那是云里雾里,听完后赶紧GOOGLE,然后嗤之以鼻:也就这样!然后也码了些年代码,跳了不少坑,填了不少洞,回过头来再回顾下,却是感慨万分:如果把这些高大尚的东西都被运用上去我还会跳坑里而不自知么,却也不住的感概,看似高大尚的名称解释起来却也是简单,但是简单的东西其实也是不断跳坑的产悟,被各种牛人不断的推崇,却还是没多少人真正遵从,这是为什么啊!