小东子的个人技术专栏

重点关注Android、Java、智能硬件、JavaEE、react-native,Swift,微信小程序


  • 首页

  • 归档

  • 标签
小东子的个人技术专栏

微服务访问安全设计方案全探索

发表于 2017-03-14   |   字数统计: 4,565(字)   |   阅读时长: 16(分)

文章来源:https://segmentfault.com/a/1190000006785852

今天给大家带来的是 数人云工程师文权在高效运维线上群的分享实录。从传统单体应用架构到微服务架构,安全问题一直是人们关注的重点,文权与大家分享了关于微服务访问安全设计方案的探索与实践。

我们首先从传统单体应用架构下的访问安全设计说起,然后分析现代微服务架构下,访问安全涉及的原则,接着讨论目前常用的几种微服务架构下的访问安全设计方案。最后,详析Spring Cloud微服务架构下如何解决访问安全的问题。

1.传统单体应用的访问安全设计

这里写图片描述

上面的示意图展示了单体应用的访问逻辑。用户通过客户端发出http或者https请求,经过负载均衡后,单体应用收到请求。接着经过auth层,进行身份验证和权限批准,这里,一般会有跟后端数据库的交互。通过后,将请求分发到对应的功能逻辑层中去。完成相关操作后,返回结果给客户端。

1.1传统单体应用的访问安全设计——原则

这里写图片描述
从以上分析可以看到,传统单体应用的访问安全设计原则为:

第一,每次的用户请求都需要验证是否安全,这里可以分两种情况:

一种是没有session的请求,需要经过几个步骤完成session化。一般为验证当前用户的credential,获取当前用户的identity,这两步都需要访问数据库等持久化对象来完成,最后一步是为当前可用创建session,返回给客户端后,启用该session。

另一种是有session的请求,只需验证请求中当前session的有效性,即可继续请求。

第二,用户的操作请求都在后端单个进程中执行完成,完全依赖后端调用方法的可靠性。一旦出错,应用是无法再次重复请求。

1.2 传统单体应用的访问安全设计——优势和注意点

这里写图片描述

传统单体应用由于设计相对简单单一,暴露给外界的入口相对较少,从而具有被攻击并造成危害性的可能小的优势。

也正是由于单体应用简单单一的特点,需要注意相关问题:

应用后端保存了所有的credential等敏感信息
一旦入侵了对这个应用的请求,就有可能拿到所有的保存在后端的信息
应用的每次操作一般都需要和数据库进行交互,造成数据库负载变高

2.微服务架构下,访问安全设计原则

这里写图片描述

来看下这张典型的微服务设计架构图,如图所示,有以下几点特征:

  • 每个服务只有权限去操作自己负责的那部分功能。
  • 用户请求的身份验证和权限批准都由独立的gateway服务来保障
  • 对外服务的LB层无法直接与提供业务服务的应用层进行访问

这里写图片描述

从上面的特征分析来看,想要给出一份访问安全设计的原则说明,就要看看微服务架构下,访问安全有哪些痛点,以下罗列了几点:

  • 单点登录,即在微服务这种多独立服务的架构下,实现用户只需要登录一次就能访问所有相互信任的应用系统
  • 微服务架构下的应用一般都是无状态的,导致用户的请求每次都需要鉴权,可能引发Auth服务的性能瓶颈
  • 微服务架构下,每个组件都管理着各自的功能权限,这种细粒度的鉴权机制需要事先良好的规划
  • 微服务架构下,需要考虑到那些非浏览器端的客户请求,是否具有良好的可操作性

根据实际情况,还有一些其他痛点,这里不再一一赘述,而这些痛点,就形成了我们在为微服务架构设计访问安全的原则。

3.微服务架构下,常用的访问安全设计方案

  • HTTP Basic Authentication + Independent Auth DB
  • HTTP Basic Authentication + Central Auth DB
  • API Tokens
  • SAML

这里列出4种,首先简单介绍下,然后一一叙述。

第一种,使用HTTP Basic Auth协议,加上独立的Auth数据库。
第二种,也是使用HTTP Basic Auth协议,跟第一种不同的是,使用集中式的Auth数据库
第三种,API Tokens协议,这种大家应该比较熟悉,很多公有服务(比如Github、Twitter等)的API都是用这种方式。
第四种,SAML,即Security Assertion Markup Language,翻译过来,是『安全声明标记语言』,它是基于XML的一种协议,企业内使用得较多。

下面一一做介绍。

3.1 微服务常用访问安全设计方案——Basic Auth + Independent Auth DB

这里写图片描述

一种,如上示意图所示,使用Basic Auth协议,配合每个服务自己都拥有存储Credential敏感数据的数据库(或者其他持久化仓库)。

简单介绍下Basic Auth协议,它是在用户的请求中添加一个Authorization消息头,这个消息头的值是一个固定格式:Basic base64encode(username+“:”+password)

完整的消息头列子为:Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

Basic Auth协议基本上被所有流行的网页浏览器都支持。

这种方案的特点:

  • 每个提供功能的服务都拥有自己独立的鉴权和授权机制
  • 每个提供功能的服务都拥有自己独立的数据库,来保存敏感信息
  • 每次用户请求都需要携带用户的credential来完成操作

小结下使用这种方案的好处:

  • 微服务的应用可以实现100%无状态化
  • 基于Basic Auth开发简单

同时,小结下使用这种方案需要注意的地方:

由于每个服务都有自己存储credential的机制,需要事先为每个服务设计好如何存储和查找用户的Credential由于每次用户请求都会携带用户的Credential,需要事先设计好如何管理鉴权机制。

3.2微服务常用访问安全设计方案——Basic Auth + Central Auth DB

这里写图片描述

二种,如上示意图所示,使用Basic Auth协议,与第一种方案相比,每个服务共用有同一个Auth DB。

第二种方案的特点和第一种很相似:

  • 每个提供功能的服务都拥有自己独立的鉴权和授权机制
  • 每个提供功能的服务共用同一个DB,来保存Credential等敏感信息
  • 每次用户请求都需要携带用户的credential来完成操作

小结下使用第二种方案的好处:

除了拥有第一种方案相似的好处外,由于共用了同一个持久化仓库来管理用户信息,简化了原来独立管理的机制

同时,小结下使用这种方案需要注意的地方:

  • 中心化Auth DB会被每次用户请求来访问连接,可能引发AuthDB性能瓶颈
  • 需要在每个服务中实现对共有Auth DB查找用户信息的逻辑

3.3微服务常用访问安全设计方案——API Tokens

这里写图片描述
第三种,如上示意图所示,使用Token Based协议来对用户请求进行操作鉴权。

简单介绍下最基本的Token Based的交互方式:

  • 用户使用包含用户名和密码的credential从客户端发起资源请求
  • 后端接受请求,通过授权中心,生产有效token字符串,返回给客户端
  • 客户端获得token后,再次发出资源请求
  • 后端接受带token的请求,通过授权中心,获取相关资源,返回给客户端
    业界常用的OAuth就是基于Token Based这套逻辑,实现的互联网级的鉴权机制。

第三种方案的特点明显:

使用token来进行鉴权,替换用户本身的用户名和密码,提高了交互安全性每次用户请求需要携带有效token,与Auth服务进行交互验证

小结下使用第三种方案的好处:

由于使用了token来鉴权,业务服务不会看到用户的敏感信息

同时,小结下使用这种方案需要注意的地方:

Auth服务可能需要处理大量的生产token的操作

3.4微服务常用访问安全设计方案——SAML

这里写图片描述

第四种,如上示意图所示,使用SAML协议来对用户请求进行操作鉴权。它是一个基于XML的标准,用于在不同的安全域(security domain)之间交换认证和授权数据。在SAML标准定义了身份提供者(identity provider)和服务提供者(service provider),这两者构成了前面所说的不同的安全域。

以上图Google提供的Apps SSO的机制,简单介绍下SAML鉴权的交互方式:

  • 用户请求访问自建的google application
  • 当前application 生成一个 SAML 身份验证请求。SAML 请求将进行编码并嵌入到SSO 服务的网址中。
  • 当前application将重定向发送到用户的浏览器。重定向网址包含应向SSO 服务提交的编码 SAML 身份验证请求。
  • SSO(统一认证中心或叫Identity Provider)解码 SAML 请求,并提取当前application的 ACS(声明客户服务)网址以及用户的目标网址(RelayState 参数)。然后,统一认证中心对用户进行身份验证。
  • 统一认证中心生成一个 SAML 响应,其中包含经过验证的用户的用户名。按照 SAML 2.0 规范,此响应将使用统一认证中心的 DSA/RSA 公钥和私钥进行数字签名。
  • 统一认证中心对 SAML 响应和 RelayState 参数进行编码,并将该信息返回到用户的浏览器。统一认证中心提供了一种机制,以便浏览器可以将该信息转发到当前application ACS。
  • 当前application使用统一认证中心的公钥验证 SAML 响应。如果成功验证该响应,ACS 则会将用户重定向到目标网址。
  • 用户将重定向到目标网址并登录到当前application。

目前SAML在业界也有相当的使用度,包括IBM Weblogic等产品。

第四种方案的特点有:

由Identity Provider提供可信的签名声明服务的访问安全由可信的Identity Provider提供

小结下使用第四种方案的好处:标准的可信访问模型

同时,小结下使用这种方案需要注意的地方:

基于XML协议,传输相对复杂对非浏览器客户端适配不方便。

4.Spring Cloud Security解决方案

Spring Cloud Security特点有:

  • 基于OAuth2 和OpenID协议的可配置的SSO登录机制
  • 基于tokens保障资源访问安全
  • 引入UAA鉴权服务,UAA是一个Web服务,用于管理账户、Oauth2客户端和用户用于鉴权的问题令牌(Issue Token)。UAA实现了Oauth2授权框架和基于JWT(JSON web tokens)的问题令牌。

这里写图片描述

下面简单介绍下UAA,事实上,它是由CloudFoundry发起的,也是CloudFoundry平台的身份管理服务(https://docs.cloudfoundry.org...)。

主要功能是基于OAuth2,当用户访问客户端应用时,生成并发放token给目标客户端。

UAA认证服务包含如下几个方面的内容:

  • 认证对象。如用户、客户端以及目标资源服务器
  • 认证类型。主要有授权码模式、密码模式以及客户端模式
  • 认证范围,即认证权限,并作为一个命名的参数附加到AccessToken上。

接下来,结合实例,一起来看下UAA在Spring Cloud中的实践。

这里写图片描述

如图所示,这是一个简单的基于Spring Cloud微服务架构的例子,它的主要组件有:

  • Eureka组件提供服务发现功能
  • 独立的Config组件提供类似配置中心的服务,持久化层可以是文件系统,也可是git repository
  • Auth组件提供基于UAA的鉴权服务
  • Account组件保存用户的业务信息 其他组件不一一介绍了

这里主要讲Auth组件和Account组件是如何基于UAA服务进行认证和授权。

这里写图片描述
一为Auth组件业务代码中定义了不同客户端的认证类型和认证范围,其中:

浏览器端的认证类型是password,认证范围是uiaccount组件端的认证类型是client_credentials,认证范围是server

图二为config组件(配置中心)定义的请求路由的规则,其中:

使用/uaa/来转发基于uaa的认证请求至auth组件
使用/accouts/
来转发请求至account组件,并标记serviceId为account-service,与图一中的withClient对应。

这里写图片描述

一为浏览器打开应用入口后,输入用户名和密码后,发出的认证请求:

认证url为/uaa/oauth/token,这是uaa模式下标准的请求获取token的url表单中包含了字段scope(认证范围)和字段password(认证类型)

图二为图一发出认证请求的返回结果:

Access_token为有效认证token,将来被其他请求使用

图三为发出获取当前用户的信息的请求:

在请求里的Authorization的值为图二中获得的access_token
这里写图片描述

图一为Account组件在Config组件(配置中心)定义的OAuth2协议下获取token的方式,这里定义了:

clientID和clientSecret
accessTokenUrl,这里指定了auth组件的uaa获取token的url
grant-type,即认证类型,这里指定为client_credentials
scope,这里指定了server,说明是这个认证请求只适用在各微服务之间的访问。

图二为Accout组件业务代码中定义了需要使用Auth组件进行事先鉴权的方法:

使用@PreAuthorize
annotation中可以指定认证范围的具体条件,这里是限定了server或者是demo账户,才有权限发起认证。
这里写图片描述
最后小结下Spring Cloud Security的特点:

  • 基于UAA,使用OAuth2协议。不会暴露用户的敏感信息
  • 基于认证类型和认证范围,实现细粒度的鉴权机制
  • 非浏览器客户端下良好的操作性

5.Q&A

问题:Basic Auth + Central Auth DB这种方式中,每个服务有自己的鉴权DB,这块只是一个缓冲吗?如果中途通过别的方式修改了中心DB的数据,而缓冲又没过期,这个时候有什么解决方案吗?答:不是缓冲,这里的Central Auth DB是指各个微服务共用一个数据库。

问题:微服务架构需要服务路由和服务注册么?跟esb的区别在哪里?答:服务路由组件和服务注册组件和是相对必要的,他们保证了用户请求能发到正确的微服务中去。ESB企业服务总线是相对比较重的组件,而不是像微服务组件一样只负责单个业务。

问题:在微服务中,对于数据权限的粒度,是可以集中在在gateway中进行还是由每个微服务自己独立配置?答:推荐由那个专门负载权限的微服务组件来配置。

问题:您好,辛苦了,请问现在有类似SAML协议,但是不基于XML,而是基于JSON或者其他简化格式的协议吗?答:目前据我所知没有基于JSON的SAML协议。有个叫JWT(JSON web token)的协议,它是完全基于JSON的,Spring Cloud架构中也使用了JWT。

问题:对于这个架构,服务划分的粒度有没有什么好的建议?另外登录凭证保存在客户端如何解决报文被拦截的安全漏洞?答:服务划分需要按具体业务来说,一般来说,一个业务实体作为一个微服务。使用https可以一定程度上提高安全性。

问题:spring cloud security可以解决token重放攻击么?答:token重放攻击不是特别了解,可能是数据弱一致性导致的,建议设计尽可能短的过期时间。

问题:我们公司现在在设计DMP,从行业的状况来看,采用了微服务,但是有一点,首先对于应用本身暴露出来的服务,是和应用一起部署的,也就是并非单独的部署,那么业务组件接口暴露部署是否合理呢?答:业务组件的接口一般可以通过统一网关来管理。也可以对业务接口像spring cloud中设置访问scope限制。

问题:所有暴露的微服务是否需要一个统一的服务管控和治理平台?答:是的,一般有服务网关和服务发现组件来管理用户请求。

问题:微服务的gateway需要实现底层多个细粒度的API组合的场景,我们现在一部分使用异步,但是遇到了没办法全面的解藕。我想问问,对于此,使用响应式?还是异步回调式?它们的区别点会有哪些呢?答:使用哪种API方案,其实要看业务。如果后端业务需要强数据一致性,建议使用响应式的。反之,可以使用异步回调或者消息队列。

问题:uaa和netflix zull集成 可行吗?是否做过这方面的尝试?
答:可以。Zuul组件提供网关服务,uaa是基于OAuth2协议,提供授权服务的。微服务架构下,他们是独立的,是可以自由组合的。举个例子,可以在zuul组件的配置文件中,为授权服务(auth-service)组件的指定路由表。

可以参考:https://gist.github.com/nevermosby/875b9f7b1799a6207832010d6eafcfc3

小东子的个人技术专栏

Spring Security OAuth2 开发指南

发表于 2017-03-14   |   字数统计: 5,054(字)   |   阅读时长: 18(分)

1.官网文档:

http://projects.spring.io/spring-security-oauth/docs/oauth2.html

2. Spring OAuth2.0 提供者实现原理

Spring OAuth2.0提供者实际上分为:

  • 授权服务 Authorization Service.
  • 资源服务 Resource Service.

注意:虽然这两个提供者有时候可能存在同一个应用程序中,但在Spring Security OAuth中你可以把他它们各自放在不同的应用上,而且你可以有多个资源服务,它们共享同一个中央授权服务。

所有获取令牌的请求都将会在Spring MVC controller endpoints中进行处理,并且访问受保护的资源服务的处理流程将会放在标准的Spring Security请求过滤器中(filters)。

2.1 配置一个授权服务必须要实现的endpoints:

  • AuthorizationEndpoint:用来作为请求者获得授权的服务,默认的URL是/oauth/authorize.
  • TokenEndpoint:用来作为请求者获得令牌(Token)的服务,默认的URL是/oauth/token.

2.2 配置一个资源服务必须要实现的过滤器:

  • OAuth2AuthenticationProcessingFilter:用来作为认证令牌(Token)的一个处理流程过滤器。只有当过滤器通过之后,请求者才能获得受保护的资源。

配置提供者(授权、资源)都可以通过简单的Java注解@Configuration来进行适配或者也可以使用基于XML的声明式语法来进行配置,如果使用XML配置的的话,那么请使用http://www.springframework.org/schema/security/spring-security-oauth2.xsd来作为XML的schema(即XML概要定义)以及使用http://www.springframework.org/schema/security/oauth2来作为命名空间。

3. 授权服务配置

配置一个授权服务,你需要考虑几种授权类型(Grant Type),不同的授权类型为客户端(Client)提供了不同的获取令牌(Token)方式,为了实现并确定这几种授权,需要配置使用 ClientDetailsService 和 TokenService 来开启或者禁用这几种授权机制。到这里就请注意了,不管你使用什么样的授权类型(Grant Type),每一个客户端(Client)都能够通过明确的配置以及权限来实现不同的授权访问机制。这也就是说,假如你提供了一个支持”client_credentials”的授权方式,并不意味着客户端就需要使用这种方式来获得授权。下面是几种授权类型的列表,具体授权机制的含义可以参见RFC6749(中文版本):

  • authorization_code:授权码类型。
  • implicit:隐式授权类型。
  • password:资源所有者(即用户)密码类型。
  • client_credentials:客户端凭据(客户端ID以及Key)类型。
  • refresh_token:通过以上授权获得的刷新令牌来获取新的令牌。

可以用 @EnableAuthorizationServer 注解来配置OAuth2.0 授权服务机制,通过使用@Bean注解的几个方法一起来配置这个授权服务。下面咱们介绍几个配置类,这几个配置是由Spring创建的独立的配置对象,它们会被Spring传入AuthorizationServerConfigurer中:

  • ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
  • AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束.
  • AuthorizationServerEndpointsConfigurer:用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)。

注意:以上的配置可以选择继承AuthorizationServerConfigurerAdapter并且覆写其中的三个configure方法来进行配置。)

配置授权服务一个比较重要的方面就是提供一个授权码给一个OAuth客户端(通过 authorization_code 授权类型),一个授权码的获取是OAuth客户端跳转到一个授权页面,然后通过验证授权之后服务器重定向到OAuth客户端,并且在重定向连接中附带返回一个授权码。

如果你是通过XML来进行配置的话,那么可以使用

1
<authorization-server/>

标签来进行配置。

3.1 配置客户端详情信息(Client Details):

ClientDetailsServiceConfigurer (AuthorizationServerConfigurer 的一个回调配置项) 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService),有几个重要的属性如下列表:

  • clientId:(必须的)用来标识客户的Id。
  • secret:(需要值得信任的客户端)客户端安全码,如果有的话。
  • scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
  • authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
  • authorities:此客户端可以使用的权限(基于Spring Security authorities)。

客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户端详情存储在一个关系数据库的表中,就可以使用 JdbcClientDetailsService)或者通过 ClientDetailsService 接口(同时你也可以实现 ClientDetailsService 接口)来进行管理。

3.2 管理令牌(Managing Token):

AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,在使用这些操作的时候请注意以下几点:
当一个令牌被创建了,你必须对其进行保存,这样当一个客户端使用这个令牌对资源服务进行请求的时候才能够引用这个令牌。
当一个令牌是有效的时候,它可以被用来加载身份信息,里面包含了这个令牌的相关权限。

当你自己创建 AuthorizationServerTokenServices 这个接口的实现时,你可能需要考虑一下使用 DefaultTokenServices 这个类,里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了所有的事情。并且 TokenStore 这个接口有一个默认的实现,它就是 InMemoryTokenStore ,如其命名,所有的令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都实现了TokenStore接口:

  • InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。

  • JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时,你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把”spring-jdbc”这个依赖加入到你的classpath当中。

  • JwtTokenStore:这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。

3.3 JWT令牌(JWT Tokens)

使用JWT令牌你需要在授权服务中使用 JwtTokenStore,资源服务器也需要一个解码的Token令牌的类 JwtAccessTokenConverter,JwtTokenStore依赖这个类来进行编码以及解码,因此你的授权服务以及资源服务都需要使用这个转换类。

Token令牌默认是有签名的,并且资源服务需要验证这个签名,因此呢,你需要使用一个对称的Key值,用来参与签名计算,这个Key值存在于授权服务以及资源服务之中。或者你可以使用非对称加密算法来对Token进行签名,Public Key公布在/oauth/token_key这个URL连接中,默认的访问安全规则是”denyAll()”,即在默认的情况下它是关闭的,你可以注入一个标准的 SpEL 表达式到 AuthorizationServerSecurityConfigurer 这个配置中来将它开启(例如使用”permitAll()”来开启可能比较合适,因为它是一个公共密钥)。

如果你要使用 JwtTokenStore,请务必把”spring-security-jwt”这个依赖加入到你的classpath中。

3.4 配置授权类型(Grant Types):

授权是使用 AuthorizationEndpoint 这个端点来进行控制的,你能够使用 AuthorizationServerEndpointsConfigurer 这个对象的实例来进行配置(AuthorizationServerConfigurer 的一个回调配置项,见上的概述) ,如果你不进行设置的话,默认是除了资源所有者密码(password)授权类型以外,支持其余所有标准授权类型的(RFC6749),我们来看一下这个配置对象有哪些属性可以设置吧,如下列表:

  • authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个 AuthenticationManager 对象。
  • userDetailsService:如果啊,你设置了这个属性的话,那说明你有一个自己的 UserDetailsService 接口的实现,或者你可以把这个东西设置到全局域上面去(例如 GlobalAuthenticationManagerConfigurer 这个配置对象),当你设置了这个之后,那么 “refresh_token” 即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。
  • authorizationCodeServices:这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对象),主要用于 “authorization_code” 授权码类型模式。
  • implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。
  • tokenGranter:这个属性就很牛B了,当你设置了这个东西(即 TokenGranter 接口实现),那么授权将会交由你来完全掌控,并且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的需求的时候,才会考虑使用这个。

在XML配置中呢,你可以使用 “authorization-server” 这个标签元素来进行设置。

3.5 配置授权端点的URL(Endpoint URLs):

AuthorizationServerEndpointsConfigurer 这个配置对象(AuthorizationServerConfigurer 的一个回调配置项,见上的概述) 有一个叫做 pathMapping() 的方法用来配置端点URL链接,它有两个参数:
第一个参数:String 类型的,这个端点URL的默认链接。
第二个参数:String 类型的,你要进行替代的URL链接。
以上的参数都将以 “/“ 字符为开始的字符串,框架的默认URL链接如下列表,可以作为这个 pathMapping() 方法的第一个参数:

  • /oauth/authorize:授权端点。
  • /oauth/token:令牌端点。
  • /oauth/confirm_access:用户确认授权提交端点。
  • /oauth/error:授权服务错误信息端点。
  • /oauth/check_token:用于资源服务访问的令牌解析端点。
  • /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。

需要注意的是授权端点这个URL应该被Spring Security保护起来只供授权用户访问,我们来看看在标准的Spring Security中 WebSecurityConfigurer 是怎么用的。

1
2
3
4
5
6
7
@Override
protected void configure(HttpSecurity http) throws Exception {
http .authorizeRequests().antMatchers("/login").permitAll().and()
// default protection for all resources (including /oauth/authorize)
.authorizeRequests() .anyRequest().hasRole("USER")
// ... more configuration, e.g. for form login
}

注意:如果你的应用程序中既包含授权服务又包含资源服务的话,那么这里实际上是另一个的低优先级的过滤器来控制资源接口的,这些接口是被保护在了一个访问令牌(access token)中,所以请挑选一个URL链接来确保你的资源接口中有一个不需要被保护的链接用来取得授权,就如上面示例中的 /login 链接,你需要在 WebSecurityConfigurer 配置对象中进行设置。

令牌端点默认也是受保护的,不过这里使用的是基于 HTTP Basic Authentication 标准的验证方式来验证客户端的,这在XML配置中是无法进行设置的(所以它应该被明确的保护)。

在XML配置中可以使用

1
<authorization-server/>

元素标签来改变默认的端点URLs,注意在配置 /check_token 这个链接端点的时候,使用 check-token-enabled 属性标记启用。

3.6 强制使用SSL(Enforcing SSL):

使用简单的HTTP请求来进行测试是可以的,但是如果你要部署到产品环境上的时候,你应该永远都使用SSL来保护授权服务器在与客户端进行通讯的时候进行加密。你可以把授权服务应用程序放到一个安全的运行容器中,或者你可以使用一个代理,如果你设置正确了的话它们应该工作的很好(这样的话你就不需要设置任何东西了)。
但是也许你可能希望使用 Spring Security 的 requiresChannel() 约束来保证安全,对于授权端点来说(还记得上面的列表吗,就是那个 /authorize 端点),它应该成为应用程序安全连接的一部分,而对于 /token 令牌端点来说的话,它应该有一个标记被配置在 AuthorizationServerEndpointsConfigurer 配置对象中,你可以使用 sslOnly() 方法来进行设置。当然了,这两个设置是可选的,不过在以上两种情况中,会导致Spring Security 会把不安全的请求通道重定向到一个安全通道中。(注:即将HTTP请求重定向到HTTPS请求上)。

3.7自定义错误处理(Error Handling):

端点实际上就是一个特殊的Controller,它用于返回一些对象数据。
授权服务的错误信息是使用标准的Spring MVC来进行处理的,也就是 @ExceptionHandler 注解的端点方法,你也可以提供一个 WebResponseExceptionTranslator 对象。最好的方式是改变响应的内容而不是直接进行渲染。
假如说在呈现令牌端点的时候发生了异常,那么异常委托了 HttpMessageConverters 对象(它能够被添加到MVC配置中)来进行输出。假如说在呈现授权端点的时候未通过验证,则会被重定向到 /oauth/error 即错误信息端点中。whitelabel error (即Spring框架提供的一个默认错误页面)错误端点提供了HTML的响应,但是你大概可能需要实现一个自定义错误页面(例如只是简单的增加一个 @Controller 映射到请求路径上 @RequestMapping(“/oauth/error”))。

3.8 映射用户角色到权限范围(Mapping User Roles to Scopes):

有时候限制令牌的权限范围是很有用的,这不仅仅是针对于客户端,你还可以根据用户的权限来进行限制。如果你使用 DefaultOAuth2RequestFactory 来配置 AuthorizationEndpoint 的话你可以设置一个flag即 checkUserScopes=true来限制权限范围,不过这只能匹配到用户的角色。你也可以注入一个 OAuth2RequestFactory 到 TokenEnpoint 中,不过这只能工作在 password 授权模式下。如果你安装一个 TokenEndpointAuthenticationFilter 的话,你只需要增加一个过滤器到 HTTP BasicAuthenticationFilter 后面即可。当然了,你也可以实现你自己的权限规则到 scopes 范围的映射和安装一个你自己版本的 OAuth2RequestFactory。AuthorizationServerEndpointConfigurer 配置对象允许你注入一个你自定义的 OAuth2RequestFactory,因此你可以使用这个特性来设置这个工厂对象,前提是你使用 @EnableAuthorizationServer 注解来进行配置

4. 资源服务配置

一个资源服务(可以和授权服务在同一个应用中,当然也可以分离开成为两个不同的应用程序)提供一些受token令牌保护的资源,Spring OAuth提供者是通过Spring Security authentication filter 即验证过滤器来实现的保护,你可以通过 @EnableResourceServer 注解到一个 @Configuration 配置类上,并且必须使用 ResourceServerConfigurer 这个配置对象来进行配置(可以选择继承自 ResourceServerConfigurerAdapter 然后覆写其中的方法,参数就是这个对象的实例),下面是一些可以配置的属性:

  • tokenServices:ResourceServerTokenServices 类的实例,用来实现令牌服务。
  • resourceId:这个资源服务的ID,这个属性是可选的,但是推荐设置并在授权服务中进行验证。
  • 其他的拓展属性例如 tokenExtractor 令牌提取器用来提取请求中的令牌。
  • 请求匹配器,用来设置需要进行保护的资源路径,默认的情况下是受保护资源服务的全部路径。
  • 受保护资源的访问规则,默认的规则是简单的身份验证(plain authenticated)。
  • 其他的自定义权限保护规则通过 HttpSecurity 来进行配置。

@EnableResourceServer 注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter 的过滤器链,

在XML配置中,使用

1
<resource-server />

标签元素并指定id为一个servlet过滤器就能够手动增加一个标准的过滤器链。

ResourceServerTokenServices 是组成授权服务的另一半,如果你的授权服务和资源服务在同一个应用程序上的话,你可以使用 DefaultTokenServices ,这样的话,你就不用考虑关于实现所有必要的接口的一致性问题,这通常是很困难的。如果你的资源服务器是分离开的,那么你就必须要确保能够有匹配授权服务提供的 ResourceServerTokenServices,它知道如何对令牌进行解码。
在授权服务器上,你通常可以使用 DefaultTokenServices 并且选择一些主要的表达式通过 TokenStore(后端存储或者本地编码)。

RemoteTokenServices 可以作为一个替代,它将允许资源服务器通过HTTP请求来解码令牌(也就是授权服务的 /oauth/check_token 端点)。如果你的资源服务没有太大的访问量的话,那么使用RemoteTokenServices 将会很方便(所有受保护的资源请求都将请求一次授权服务用以检验token值),或者你可以通过缓存来保存每一个token验证的结果。

使用授权服务的 /oauth/check_token 端点你需要将这个端点暴露出去,以便资源服务可以进行访问,这在咱们授权服务配置中已经提到了,下面是一个例子:

1
2
3
4
5
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')")
.checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
}

在这个例子中,我们配置了 /oauth/check_token 和 /oauth/token_key 这两个端点(受信任的资源服务能够获取到公有密匙,这是为了验证JWT令牌)。这两个端点使用了HTTP Basic Authentication 即HTTP基本身份验证,使用 client_credentials 授权模式可以做到这一点。

4.1 配置OAuth-Aware表达式处理器(OAuth-Aware Expression Handler):

你也许希望使用 Spring Security’s expression-based access control 来获得一些优势,一个表达式处理器会被注册到默认的 @EnableResourceServer 配置中,这个表达式包含了 #oauth2.clientHasRole,#oauth2.clientHasAnyRole 以及 #oauth2.denyClient 所提供的方法来帮助你使用权限角色相关的功能(在 OAuth2SecurityExpressionMethods 中有完整的列表)。
在XML配置中你可以注册一个 OAuth-Aware 表达式处理器即

1
<expression-handler />

元素标签到 常规的 <http /> 安全配置上。

资料来源:http://www.cnblogs.com/xingxueliao/p/5911292.html

小东子的个人技术专栏

jvm知识点总览

发表于 2017-03-14   |   字数统计: 3,716(字)   |   阅读时长: 13(分)

转载 作者:纯洁的微笑
出处:http://www.ityouknow.com/
在江湖中要练就绝世武功必须内外兼备,精妙的招式和深厚的内功,武功的基础是内功。对于武功低(就像江南七怪)的人,招式更重要,因为他们不能靠内功直接去伤人,只能靠招式,利刃上优势来取胜了,但是练到高手之后,内功就更主要了。一个内功低的人招式在奇妙也打不过一个内功高的人。比如,你剑法再厉害,一剑刺过来,别人一掌打断你的剑,你还怎么使剑法,你一掌打到一个武功高的人身上,那人没什么事,却把你震伤了,你还怎么打。同样两者也是相辅相成的,内功深厚之后,原来普通的一招一式威力也会倍增。

对于搞开发的我们其实也是一样,现在流行的框架越来越多,封装的也越来越完善,各种框架可以搞定一切,几乎不用关注底层的实现,初级程序员只要熟悉基本的使用方法,便可以快速的开发上线;但对于高级程序员来讲,内功的修炼却越发的重要,比如算法、设计模式、底层原理等,只有把这些基础熟练之后,才能在开发过程中知其然知其所以然,出现问题时能快速定位到问题的本质。

对于Java程序员来讲,spring全家桶几乎可以搞定一切,spring全家桶便是精妙的招式,jvm就是内功心法很重要的一块,线上出现性能问题,jvm调优更是不可回避的问题。因此JVM基础知识对于高级程序员的重要性不必言语,我司在面试高级开发的时候,jvm相关知识也必定是考核的标准之一。本篇文章会根据之前写的jvm系列文章梳理出jvm需要关注的所有考察点。

jvm 总体梳理

jvm体系总体分四大块:

  • 类的加载机制
  • jvm内存结构
  • GC算法 垃圾回收
  • GC分析 命令调优

当然这些知识点在之前的文章中都有详细的介绍,这里只做主干的梳理

这里画了一个思维导图,将所有的知识点进行了陈列,因为图比较大可以点击右键下载了放大查看。

{:.center}

类的加载机制

主要关注点:

  • 什么是类的加载
  • 类的生命周期
  • 类加载器
  • 双亲委派模型

什么是类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

类的生命周期

类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,如下图;

{:.center}

  • 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
  • 连接,连接又包含三块内容:验证、准备、初始化。1)验证,文件格式、元数据、字节码、符号引用验证;2)准备,为类的静态变量分配内存,并将其初始化为默认值;3)解析,把类中的符号引用转换为直接引用
  • 初始化,为类的静态变量赋予正确的初始值
  • 使用,new出对象程序中使用
  • 卸载,执行垃圾回收

几个小问题?
1、JVM初始化步骤 ? 2、类初始化时机 ?3、哪几种情况下,Java虚拟机将结束生命周期?
答案参考这篇文章jvm系列(一):java类的加载机制

类加载器

{:.center}

  • 启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库
  • 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器

类加载机制

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

jvm内存结构

主要关注点:

  • jvm内存结构都是什么
  • 对象分配规则

jvm内存结构

{:.center}

方法区和对是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。

  • Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
  • 方法区(Method Area),方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 程序计数器(Program Counter Register),程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
  • JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
  • 本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

对象分配规则

  • 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
  • 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  • 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。

如何通过参数来控制个各个内存区域
参考此文章:jvm系列(二):JVM内存结构

GC算法 垃圾回收

主要关注点:

  • 对象存活判断
  • GC算法
  • 垃圾回收器

对象存活判断

判断对象是否存活一般有两种方式:

  • 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
  • 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。

GC算法

GC最基础的算法有三种:标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。

  • 标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
  • 复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
  • 标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
  • 分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

垃圾回收器

  • Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
  • ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。
  • Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。
  • Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
  • CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
  • G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征

GC算法和垃圾回收器算法图解以及更详细内容参考 jvm系列(三):GC算法 垃圾收集器

GC分析 命令调优

主要关注点:

  • GC日志分析
  • 调优命令
  • 调优工具

GC日志分析

摘录GC日志一部分(前部分为年轻代gc回收;后部分为full gc回收):

2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs] 
2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]

通过上面日志分析得出,PSYoungGen、ParOldGen、PSPermGen属于Parallel收集器。其中PSYoungGen表示gc回收前后年轻代的内存变化;ParOldGen表示gc回收前后老年代的内存变化;PSPermGen表示gc回收前后永久区的内存变化。young gc 主要是针对年轻代进行内存回收比较频繁,耗时短;full gc 会对整个堆内存进行回城,耗时长,因此一般尽量减少full gc的次数

young gc 日志:
{:.center}

Full GC日志:
{:.center}

调优命令

Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo

  • jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
  • jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  • jmap,JVM Memory Map命令用于生成heap dump文件
  • jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
  • jstack,用于生成java虚拟机当前时刻的线程快照。
  • jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。

详细的命令使用参考这里jvm系列(四):jvm调优-命令篇

调优工具

常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。

  • jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控
  • jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
  • MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
  • GChisto,一款专业分析gc日志的工具
小东子的个人技术专栏

Java并发编程:Callable、Future和FutureTask

发表于 2017-02-27   |   字数统计: 1,475(字)   |   阅读时长: 7(分)

1、简介

在Java中创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。

2、需求

在Java中,如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。

3、Callable、FutureTask简介

在学习Callable和FutureTask之前,我们先看一下java.lang.Runnable吧,它是一个接口,在它里面只声明了一个run()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}

由于run()方法返回值为void类型,所以在执行完任务之后无法返回任何结果。

Callable位于java.util.concurrent包下,它也是一个接口,在它里面也只声明了一个方法,只不过这个方法叫做call():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package java.util.concurrent;
/**
* A task that returns a result and may throw an exception.
* Implementors define a single method with no arguments called
* {@code call}.
*
* <p>The {@code Callable} interface is similar to {@link
* java.lang.Runnable}, in that both are designed for classes whose
* instances are potentially executed by another thread. A
* {@code Runnable}, however, does not return a result and cannot
* throw a checked exception.
*
* <p>The {@link Executors} class contains utility methods to
* convert from other common forms to {@code Callable} classes.
*
* @see Executor
* @since 1.5
* @author Doug Lea
* @param <V> the result type of method {@code call}
*/
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}

可以看到,这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。

那么怎么使用Callable呢?一般情况下是配合ExecutorService来使用的,在ExecutorService接口中声明了若干个submit方法的重载版本:

1
2
3
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
  • 第一个submit方法里面的参数类型就是Callable。
  • Callable一般是和ExecutorService配合来使用的,具体的使用方法讲在后面讲述。
  • 一般情况下我们使用第一个submit方法和第三个submit方法,第二个submit方法很少使用。
  • Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

Future类位于java.util.concurrent包下,它是一个接口:

1
2
3
4
5
6
7
8
public interface Future<V> {
boolean cancel( boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get( long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}

在Future接口中声明了5个方法,下面依次解释每个方法的作用:

  • cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
  • isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
  • isDone方法表示任务是否已经完成,若任务完成,则返回true;
  • get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
  • get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。

Future提供了三种功能:

  1. 判断任务是否完成;
  2. 能够中断任务;
  3. 能够获取任务执行结果。

因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。

###4、Callable、FutureTask使用

4.1简单使用

CallableTask.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int hours=5;
int amount = 0;
while(hours>0){
System.out.println("我正在工作,剩余时间 "+hours+"小时");
amount++;
hours--;
Thread.sleep(1000);
}
return amount;
}
}

TestDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class TestDemo {
public static void main(String args[]) throws ExecutionException {
CallableTask worker = new CallableTask();
FutureTask<Integer> jiangong = new FutureTask<Integer>(worker);
new Thread(jiangong).start();
while(!jiangong.isDone()){
try {
System.out.println("获取结果"+jiangong.get());
System.out.println("任务是否取消"+jiangong.isCancelled());
System.out.println("任务是否执行"+jiangong.isDone());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int amount;
try {
amount = jiangong.get();
System.out.println("工作做完了,上交了"+amount);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

运行的结果:

这里写图片描述

4.2并发的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.lidong.demo;
import java.util.concurrent.Callable;
/**
* @项目名称:lidong-dubbo
* @类名:Task
* @类的描述:
* @作者:lidong
* @创建时间:2017/2/21 下午3:42
* @公司:chni
* @QQ:1561281670
* @邮箱:lidong1665@163.com
*/
public class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+" 子线程在进行计算开始");
Thread.sleep(3000);
int sum = 0;
for(int i=0;i<100;i++)
sum += i;
System.out.println(Thread.currentThread().getName()+" 子线程在进行计算结束");
return sum;
}
}

Test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.lidong.demo;
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
executor.submit(futureTask);
executor.shutdown();
System.out.println(Thread.currentThread().getName()+" 主线程在执行任务");
try {
System.out.println(Thread.currentThread().getName()+ " task运行结果"+futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ " 所有任务执行完毕");
}
}

运行结果:

这里写图片描述

小东子的个人技术专栏

第一个 spring Boot 应用通过Docker 来实现构建、运行、发布

发表于 2017-02-27   |   字数统计: 1,226(字)   |   阅读时长: 6(分)

1. Docker 简介

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。Docker image 是用于运行容器化进程的方案,在本文中,我们将构建一个简单的 Spring Boot 应用程序。

2.环境搭建

  • JDK 1.8+
  • Maven 3.0+
  • Docker 最新版。

    3.用 Maven 构建项目

    3.1 创建目录结构

1
mkdir -p src/main/java/com/lidong/demo

在linux或者mac系统中。
这里写图片描述

3.2 创建 pom.xml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lidong.demo</groupId>
<artifactId>lidong-spring-boot-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.1.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<docker.image.prefix>springio</docker.image.prefix>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.4.13</version>
<configuration>
<imageName>${docker.image.prefix}/${project.artifactId}</imageName>
<dockerDirectory>src/main/docker</dockerDirectory>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</build>
</project>

注意:

  1. Spring Boot Maven plugin 提供了很多方便的功能:
    1)它收集的类路径上所有 jar 文件,并构建成一个单一的、可运行的jar,这使得它更方便地执行和传输服务。
    2)它搜索的 public static void main() 方法来标记为可运行的类。
    3)它提供了一个内置的依赖解析器,用于设置版本号以匹配 Spring Boot 的依赖。您可以覆盖任何你想要的版本,但它会默认选择的 Boot 的版本集。
  1. Spotify 的 docker-maven-plugin 插件是用于构建 Maven 的 Docker Image
    1)imageName指定了镜像的名字,本例为 springio/lidong-spring-boot-demo
    2)dockerDirectory指定 Dockerfile 的位置
    3)resources是指那些需要和 Dockerfile 放在一起,在构建镜像时使用的文件,一般应用 jar 包需要纳入。

4.编写 第一个Spring Boot 应用

编写一个简单的 Spring Boot 应用 :

src/main/java/com/lidong/demo/SampleController.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.lidong.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @项目名称:lidong-dubbo
* @类名:SampleController
* @类的描述:
* @作者:lidong
* @创建时间:2017/2/19 上午9:34
* @公司:chni
* @QQ:1561281670
* @邮箱:lidong1665@163.com
*/
@Controller
@SpringBootApplication
public class SampleController {
@ResponseBody
@RequestMapping(value = "/")
String home(){
return "Hello Docker World";
}
public static void main(String[] args) {
SpringApplication.run(SampleController.class,"--server.port=8081");
}
}
  • 类用 @SpringBootApplication @RestController 标识,可用 Spring MVC 来处理 Web 请求。
  • @RequestMapping 将 / 映射到 home() ,并将”Hello Docker World” 文本作为响应。
  • main() 方法使用 Spring Boot 的 SpringApplication.run() 方法来启动应用。

    5.运行程序

#####5.1使用Maven命令

1
mvn package

运行:

1
java -jar target/lidong-spring-boot-demo-1.0-SNAPSHOT.jar

访问项目

如果程序正确运行,浏览器访问 http://localhost:8081/,可以看到页面 “Hello Docker World.” 字样。

5.2 使用IDEA 插件

这里写图片描述

6.将项目容器化

Docker 使用 Dockerfile 文件格式来指定 image 层,

创建文件 src/main/docker/Dockerfile:

1
2
3
4
5
6
FROM frolvlad/alpine-oraclejdk8:slim
VOLUME /tmp
ADD lidong-spring-boot-demo-1.0-SNAPSHOT.jar app.jar
RUN sh -c 'touch /app.jar'
ENV JAVA_OPTS=""
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]

解释下这个配置文件:

VOLUME 指定了临时文件目录为/tmp。其效果是在主机 /var/lib/docker 目录下创建了一个临时文件,并链接到容器的/tmp。改步骤是可选的,如果涉及到文件系统的应用就很有必要了。/tmp目录用来持久化到 Docker 数据文件夹,因为 Spring Boot 使用的内嵌 Tomcat 容器默认使用/tmp作为工作目录
项目的 jar 文件作为 “app.jar” 添加到容器的
ENTRYPOINT 执行项目 app.jar。为了缩短 Tomcat 启动时间,添加一个系统属性指向 “/dev/urandom” 作为 Entropy Source
构建 Docker Image

执行构建成为 docker image:

1
mvn package docker:build

运行

运行 Docker Image

1
docker run -p 8081:8081 -t springio/lidong-spring-boot-demo

这里写图片描述

看到这个Spring的图标。就以为这我们在docker 上发布Spring boot 程序已经完成。

接下来去访问在浏览器访问 http://localhost:8081/,可以看到页面 “Hello Docker World.” 字样。

参考资料:
http://spring.io/guides/gs/spring-boot-docker/

在这里是一个简单的spring boot的入门。后面会详细介绍Spring-boot 构建微服务一些具体的知识。

123…10
李东

李东

细节决定成败,点滴铸就辉煌

46 文章
18 标签
© 2017 李东
由 Hexo 强力驱动
主题 - NexT.Pisces