This is an automated email from the ASF dual-hosted git repository. ningjiang pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-servicecomb-website.git
commit dabb3ba939707da7e31c9b8ee92bd70670491fd3 Author: zhengyangyong <yangyong.zh...@huawei.com> AuthorDate: Tue Aug 28 10:59:01 2018 +0800 添加客户管理系统微服务化实战-PartI Signed-off-by: zhengyangyong <yangyong.zh...@huawei.com> --- _posts/cn/2018-08-28-crm-part-I.md | 747 +++++++++++++++++++++++++ assets/images/scaffold/OutputHello.png | Bin 17437 -> 19140 bytes assets/images/scaffold/StartServiceComb.io.png | Bin 0 -> 75973 bytes 3 files changed, 747 insertions(+) diff --git a/_posts/cn/2018-08-28-crm-part-I.md b/_posts/cn/2018-08-28-crm-part-I.md new file mode 100644 index 0000000..6582a54 --- /dev/null +++ b/_posts/cn/2018-08-28-crm-part-I.md @@ -0,0 +1,747 @@ +--- +title: "客户管理系统微服务化实战-PartI" +lang: cn +ref: crm-part-I +permalink: /cn/docs/crm-part-I/ +excerpt: "客户管理系统微服务化实战-PartI" +last_modified_at: 2018-08-28T10:00:00+08:00 +author: Yangyong Zheng +tags: [CRM,Scaffold,DDD,JPA,JWT,Edge] +redirect_from: + - /theme-setup/ +--- + + +## 客户管理系统微服务化实战-PartI +## ——DDD、场景、极速开发 + +在今年的LC3大会上,ServiceComb展台所展示的demo视频`“30分钟开发雏形CRM应用”`引起了参会者的广泛关注,大家纷纷对其背后的技术表现出浓厚的兴趣。本文将从房地产企业的客户管理管理场景入手,使用领域驱动设计,深入技术细节,详解如何快速开发落地一个微服务化的客户管理系统。 + +# 1. 牛刀小试 +打开浏览器,输入地址`http://start.servicecomb.io/`打开SERVICECOMB SPRING INITIALIZR,修改Project Metadata中的`Group`,`Artifact`和ServiceComb Parameters中的`ServiceCenter Address`,`Governance`等,点击Generate Project,解压生成下载的demo.zip。 + +![SERVICECOMB SPRING INITIALIZR截图](/assets/images/scaffold/StartServiceComb.io.png) + +运行它也很简单,使用IDE打开项目,DEBUG -> Application.java,或在命令行: + +稍等微服务启动就绪,打开浏览器输入`http://localhost:9080/hello`验证一下: + +![输出hello截图](/assets/images/scaffold/OutputHello.png) + + + +是不是非常轻松呢? + +# 2. 脚手架 +在建筑领域,脚手架是施工现场为方便工人操作并解决垂直和水平运输而搭设的各种支架以及平台。 + +![scaffold](/assets/images/scaffold/Scaffold.jpg) + +在软件开发领域,它引申为预提供一些基础框架代码加速开发过程,避免从零开始构建项目。用户只需要依据需求场景选择合适的脚手架,然后填充定制的业务逻辑即可,不必再去处理一些基础功能,例如数据库连接、日志实现、RPC传输等。 + +微服务框架一般都会提供脚手架功能,例如Spring,提供了SPRING INITIALIZR;ServiceComb基于SPRING INITIALIZR,提供了更具优势的特性: +1. 生成的项目除了在POM中自动添加必要的依赖,还会提供Producer和Consumer示例代码(Hello World); +2. 会进一步提供**Edge Server**、**Authcation Server**等更贴近业务的脚手架项目,让用户能快速构建体系完整的微服务系统。 + +那么什么叫一个完整的微服务系统呢?我们可以拿一个具体的场景做例子,会更有感觉: + +# 3. 场景:地产CRM +您经营着一家房地产开发商,销售房产,迫切需要一套销售系统,考虑到微服务的优势,您决定使用微服务的方式构建系统;主要的业务流程也非常简单:用户前来购买购买产品(房产),首先需要登记用户信息,并缴纳一定数量的定金,待交易当日,挑选心仪的产品(房产),支付尾款,完成交易。 + +## 3.1 使用DDD指导地产CRM系统的设计 +微服务系统的设计方面,领域驱动设计(Domain-Driven Design,DDD)是一个从业务出发的好选择,它由Eric Evans提出,是一种全新的系统设计和建模方法,这里的模型指的就是领域模型(Domain Model)。领域模型通过聚合(Aggregate)组织在一起,聚合间有明显的业务边界,这些边界将领域划分为一个个限界上下文(Bounded Context)。Martin Fowler对它们都有详细的[解读](https://martinfowler.com/tags/domain%20driven%20design.html)。 + +理论概念都搞清楚了,那么怎么来找模型和聚合呢?一个非常流行的方法就是[Event Storming](https://en.wikipedia.org/wiki/Event_storming),它是由Alberto Brandolini发明,经历了DDD社区和很多团队的实践,也是一种非常有参与感的团队活动: + +![EventStorming](/assets/images/scaffold/EventStorming.png) + +上图就是我们对地产CRM这个场景使用Event Storming探索的结果,现在我们能够将限界上下文清晰的梳理出来: + +![BoundedContext](/assets/images/scaffold/BoundedContext.png) + +>提示:Event Storming是一项非常有创造性的活动,也是一个持续讨论和反复改进的过程,不同的团队关注的核心域(Core Domain)不同,得到的最终结果也会有差异。我们的目的是为了演示完整的微服务系统构建的过程,并不涉及商业核心竞争力方面的探讨,因此没有Core Domain和Sub Domain之类的偏重。 + +## 3.2 将分析成果转化为方案域设计 +当我们完成所有的限界上下文的识别后,可以直接将它们落地为微服务: + +![MicroserviceDesign1](/assets/images/scaffold/MicroserviceDesign1.png) + +1. 用户服务:提供用户信息管理服务,这里保存这用户的账号和密码,负责登录和认证; +2. 产品(房产)服务:提供产品管理服务,保存着房产的信息诸如价格、是否已售出等信息; +3. 支付服务:提供交易时支付服务,模拟对接银行支付定金,以及购房时支付尾款; + +由于完成一笔交易是一个复杂的流程,与这三个微服务都有关联,因此我们引入了一个复合服务——交易服务: + +![MicroserviceDesign2](/assets/images/scaffold/MicroserviceDesign2.png) + +4. 交易服务:提供产品交易服务,它通过编排调用将整个交易流程串起来,交易服务中有两个流程: +- 定金支付 + + Step1:通过用户服务验证用户身份; + + Step2:通过支付服务请求银行扣款,增加定金账号内的定金; + +- 购房交易 + + Step1:通过用户服务验证用户身份; + + Step2:通过资源服务确定用户希望购买的资源(房产)尚未售出; + + Step3:通过资源服务标记目标资源(房产)已售出; + + Step4:通过支付服务请求扣减定金账号内的定金,以及银行扣剩下的尾款; + + 最后两个步骤需要保证事务一致性,其中Step4包含两个扣款操作。 + +之后,我们引入Edge服务提供统一入口: + +![MicroserviceDesign3](/assets/images/scaffold/MicroserviceDesign3.png) + +5. Edge服务:很多时候也被称为API网关(API Gateway),负责集中认证、动态路由等等; + +>提示:Edge服务需要依赖服务注册-发现机制,因此同时导入了ServiceCenter。 + +最后还需要提供UI: + +![MicroserviceDesign4](/assets/images/scaffold/MicroserviceDesign4.png) + +6. 前端UI(同样以微服务方式提供):用户交互界面; + +至此,DDD设计地产CRM的工作就结束了。 + +# 4. 快速实现客户关系管理系统的用户服务 +## 4.1 用户微服务并不简单 +用户微服务是所有系统中不可或缺的部分,它承载了认证和授权等核心功能——无论是登录一个网站、还是打开一个APP,当涉及到需要身份识别后才能够执行的操作,都需要用户微服务把关。例如观看视频网站上的视频,匿名用户会插播广告,如果希望屏蔽广告,则需要登录并购买VIP会员,登录即是身份认证的过程,而VIP屏蔽广告即是授权的过程。 + +##### 认证 +认证不仅仅是一次性验证用户名和密码的过程,还需要能反复使用认证的结果,确保后继所有操作都是合法的,这就涉及到“有状态”,但HTTP是一个无状态协议,如何能够将登录成功后的认证信息与后继的请求关联起来呢? + +我们非常熟悉的做法是使用Session或Cookie: +- Session存储在服务端,因此具备良好的防篡改能力,但弊端是使服务有状态,微服务系统中,同一个微服务会依据系统压力的大小弹性伸缩出多个运行实例负载均衡,跨实例访问会状态丢失。 +- Cookie存储在客户端,它正好与Session相反,优势是服务不必保持状态,但弊端是客户比较容易的篡改Cookie信息,例如修改过期时间以逃避验证,而且浏览器对Cookie也有较多限制。 + +那么,如何兼顾这两方面的需求呢?Token就是一个比较好的解决方案。 + +Token中文翻译为令牌,它将登录认证后的信息签名后返回,服务端不保存,客户端请求的时候将认证的完整信息附带上提供给服务端验签,签名可以保证信息不被篡改。了解了了解Token的原理,自然要关注Token的格式,JWT就是这样一个基于JSON的开放标准[RFC-7519](https://tools.ietf.org/html/rfc7519)。 + +###### JWT (Java Web Token)规范 +简而言之JWT规范由三部分构成: +1. Header: 声明Token的类型也就是JWT,以及加密算法,例如: + +```json +{ + "typ": "JWT", + "alg": "HS256" +} +``` + +2. Playload:存放有效信息,既包含标准签发者、用户、签发时间、过期时间,唯一标识等信息;也可以存放用户自定义的声明信息,例如权限控制相关的内容,例如: + +```json +{ + "sub": "1234567890", + "name": "YangYong Zheng", + "iat": 1516239022 +} +``` + +3. Signature:签名信息,包含Header和Playload的原始信息(Base64编码过)以及签名过后的信息。 + +>提示:JWT IO提供了[在线编码和解码工具](https://jwt.io/)。 + +##### 授权 +授权的本意是指将完成某项工作所必须的权力授给下属人员,在软件系统中往往引申为使人或角色具备访问特定资源或更改行为的许可。例如之前提到的VIP屏蔽广告,即是视频网站允许播放终端在特定的帐号登录后跳过广告播放环节(行为)的许可。 + +授权系统比较常见的做法有ACL和RBAC: +- ACL:ACL全称Access Control List,它是以受控资源为核心,每一个受控资源,都有一个权限控制列表记录哪些用户或角色对这项资源执行具体操作(也被称为授权点)的权限设置,例如查询(可见)、修改、删除等等。Windows中的文件系统安全即是一个经典的ACL实现案例: + +![ACL](/assets/images/scaffold/ACL.png) + +- RBAC:RBAC全称Role Based Access Control,与ACL相比,它以角色为核心,权限落地在角色上,不为特定用户授权。它的优势是大幅简化了用户与权限的管理,在受控对象不多或控制粒度要求不高(例如接口访问控制)的场景下非常适用。 + +![RBAC](/assets/images/scaffold/RBAC.png) + +由于微服务系统的权限控制主要是接口访问控制上,并且多采用用户组方式组织用户,因此RBAC是比较流行的做法。 + +## 4.2 实现用户微服务 +##### 第一步:创建微服务项目 +使用SERVICECOMB SPRING INITIALIZR创建用户微服务,创建完毕后使用IDEA或Eclipse打开项目,我们删掉HelloImpl和HelloConsumer,之后添加自己的实现。 + +##### 第二步:使用MySQL持久化用户信息 +用户微服务需要持久化用户信息,我们使用MySQL数据库,ORM使用Spring Data JPA: +###### 引入依赖 +```xml +<dependency> + <groupId>mysql</groupId> + <artifactId>mysql-connector-java</artifactId> +</dependency> +<dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-jpa</artifactId> +</dependency> +``` +###### 定义存储User信息的UserEntity实体 +```java +@Entity +@Table(name = "T_User") +public class UserEntity { + @Id + private String name; + + private String password; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public UserEntity() { + } + + public UserEntity(String name, String password) { + this.name = name; + this.password = password; + } +} +``` + +在CodeFist模式下,Spring Data JPA会在数据库中自动创建T_User表与此实体映射。 + +###### 实现UserEntity实体的Repository +我们继承JPA的PagingAndSortingRepository来实现ORM操作 + +```java +@Repository +public interface UserRepository extends PagingAndSortingRepository<UserEntity, Long> { + UserEntity findByName(String name); +} +``` + +###### 配置数据库连接 +在项目的`resources`目录下新增`application.properties`文件,写入数据库连接信息: + +```properties +spring.datasource.url=jdbc:mysql://localhost:3306/user_db?useSSL=false +spring.datasource.username=root +spring.datasource.password=pwd +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect +``` + +>提示:关于Spring Data JPA的更多资料请参见[这篇文档](https://projects.spring.io/spring-data-jpa/),为了能够简化依赖的引入我们实际上使用的是Spring Boot JPA Starter,详细的例子请参见[这篇文档](https://spring.io/guides/gs/accessing-data-jpa/)。 + +##### 第三步:实现JWT认证 +###### 定义JWT接口 + +```java +public interface TokenStore { + String generate(String userName); + + boolean validate(String token); +} +``` + +generate用于生成Token,validate用于验证Token是否正确。 + +###### 实现TokenStore +我们使用[jjwt](https://github.com/jwtk/jjwt)提供的JWT实现,创建JwtTokenStore类,继承TokenStore接口,并重写方法: + +```java +@Component +@Component +public class JwtTokenStore implements TokenStore { + private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenStore.class); + + private final String secretKey; + + private final int secondsToExpire; + + public JwtTokenStore() { + this.secretKey = "someSecretKeyForAuthentication"; + this.secondsToExpire = 60 * 60 * 24; + } + + public JwtTokenStore(String secretKey, int secondsToExpire) { + this.secretKey = secretKey; + this.secondsToExpire = secondsToExpire; + } + + @Override + public String generate(String userName) { + return Jwts.builder().setSubject(userName) + .setExpiration(Date.from(ZonedDateTime.now().plusSeconds(secondsToExpire).toInstant())) + .signWith(HS512, secretKey).compact(); + } + + @Override + public boolean validate(String token) { + try { + return StringUtils.isNotEmpty(Jwts.parser() + .setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject()); + } catch (JwtException | IllegalArgumentException e) { + LOGGER.info("validateToken token : " + token + " failed", e); + } + return false; + } +} +``` + +##### 第四步:实现用户服务 +###### 定义UserService接口 + +```java +public interface UserService { + ResponseEntity<Boolean> logon(UserDTO user); + ResponseEntity<Boolean> login(UserDTO user); +} + +``` + +logon用于新用户注册,login用于用户登录验证,UserDTO用于参数传递: + +```java +public class UserDTO { + private String name; + private String password; + public String getName() { + return name; + } + public String getPassword() { + return password; + } + public UserDTO() { + } + public UserDTO(String name, String password) { + this.name = name; + this.password = password; + } +} +``` + +###### 实现并发布UserService +创建UserServiceImpl,继承`UserService`接口: + +```java +@RestSchema(schemaId = "user") +@RequestMapping(path = "/") +public class UserServiceImpl implements UserService { + private final UserRepository repository; + + private final TokenStore tokenStore; + + @Autowired + public UserServiceImpl(UserRepository repository, TokenStore tokenStore) { + this.repository = repository; + this.tokenStore = tokenStore; + } + + @Override + @PostMapping(path = "logon") + public ResponseEntity<Boolean> logon(@RequestBody UserDTO user) { + if (validateUser(user)) { + UserEntity dbUser = repository.findByName(user.getName()); + if (dbUser == null) { + UserEntity entity = new UserEntity(user.getName(), user.getPassword()); + repository.save(entity); + return new ResponseEntity<>(true, HttpStatus.OK); + } + throw new InvocationException(BAD_REQUEST, "user name had exist"); + } + throw new InvocationException(BAD_REQUEST, "incorrect user"); + } + + @Override + @PostMapping(path = "login") + public ResponseEntity<Boolean> login(@RequestBody UserDTO user) { + if (validateUser(user)) { + UserEntity dbUser = repository.findByName(user.getName()); + if (dbUser != null) { + if (dbUser.getPassword().equals(user.getPassword())) { + String token = tokenStore.generate(user.getName()); + HttpHeaders headers = generateAuthenticationHeaders(token); + //add authentication header + return new ResponseEntity<>(true, headers, HttpStatus.OK); + } + throw new InvocationException(BAD_REQUEST, "wrong password"); + } + throw new InvocationException(BAD_REQUEST, "user name not exist"); + } + throw new InvocationException(BAD_REQUEST, "incorrect user"); + } + + private boolean validateUser(UserDTO user) { + return user != null && StringUtils.isNotEmpty(user.getName()) && StringUtils.isNotEmpty(user.getPassword()); + } + + private HttpHeaders generateAuthenticationHeaders(String token) { + HttpHeaders headers = new HttpHeaders(); + headers.add(AUTHORIZATION, token); + return headers; + } +} +``` + +登录成功后,会从TokenStore生成Token,并将其写入Key为`AUTHORIZATION`的Header。 + +由于我们允许任何用户注册和登录,所以目前还没有授权的需求,经过上面四步,具有基本注册和登录功能的用户微服务就构建好了。 + +## 4.3 验证实现的用户服务 +启动用户微服务,我们先注册一个账号: + +![TestLogon](/assets/images/scaffold/TestLogon.png) + +显示注册成功,现在我们使用这个账号登录: + +![TestLogin](/assets/images/scaffold/TestLogin.png) + +返回登录成功,Response中已经包含了`AUTHORIZATION`Header,后继的所有请求都需要使用这个Token值进行合法认证。 + +至此,实现客户关系管理系统的用户服务工作就结束了,现在我们会将目光转移到Edge服务,通过Edge服务作为微服务调用的统一入口,在它之上构建统一认证,应对海量级调用的挑战。 + +# 5 开发高性能边缘服务 +## 5.1 什么是边缘服务(Edge Service) +边缘服务也是一个微服务,微服务化系统通常使用边缘服务(Edge Service)作为所有其它微服务的统一入口,因此它也常常会被称为API Gateway,使用边缘服务的好处有如下几点: +* 动态路由:动态配置URL地址与微服务之间的对应关系,便于扩展,以及实现版本灰度发布等; +* 统一认证:在入口处进行访问认证,避免需要在所有的微服务中都承载重复的认证机制; +* 集中监控:与统一认证类似,在边缘服务对入口调用进行监控,容易统计流量信息。 + +## 5.2 边缘服务的作用和原理 +我们先来看不使用边缘服务,UI直接调用用户服务的场景: + +![DirectInvoke](/assets/images/scaffold/DirectInvoke.png) + +可以看出这种调用方式,UI缺乏一定的灵活性,体现在: +* UI的实现绑定了Chassis的编程语言Java,无法使用PHP等其它前端技术开发; +* UI访问微服务的路径无法动态配置,如果作为后端的微服务系统发生调整,则UI很可能需要修改; +* UI很容易混入复合(编排)调用的逻辑,使得结构变得复杂难以维护。 + +我们再看引入边缘服务后,UI如何通过边缘服务调用用户服务: + +![InvokeViaEdge](/assets/images/scaffold/InvokeViaEdge.png) + +Edge服务将在`9090`端口上接受http rest调用,我们设计了下面的转发规则: + +```text +http://{edge-host-name}:9090/{ServiceComb微服务Name}/{服务路径&参数} +``` + +用户微服务名(`service_description.name`)是`user-service`,因此login调用URL:*cse://user-service/login*可以通过:*http://{edge-host-name}:9090/user-service/login* 访问。 + +如此一来,微服务名成为了路径的一部分,http协议的`hostname`和`port`将固定指向Edge服务保持不变,灵活性大大增加了。 + +到此我们还可以再做一点点改进,引入一个自定义配置`edge.routing-short-path.{简称}`,映射微服务名: + +``` +edge: + routing-short-path: + user: user-service +``` + +上面的配置代表:*http://{edge-host-name}:9090/user/login* 等效于:*http://{edge-host-name}:9090/user-service/login* ,如此一来: +1. URL能够更加简洁; +2. 当微服务名发生变化,只需要调整对应的配置,不需要更改前端UI路径代码。 + +## 5.3 实现边缘服务 +##### 第一步:引入Edge Core依赖 +```xml +<dependency> + <groupId>org.apache.servicecomb</groupId> + <artifactId>edge-core</artifactId> +</dependency> +``` + +##### 第二步:编写调度器Dispatcher +Edge服务的核心就是调度器Dispatcher,ServiceComb Edge Core中的Dispatcher基于高性能的Vertx Reactive,轻松应对百万量级API请求的挑战;只需要继承AbstractEdgeDispatcher抽象类,添加对应的逻辑即可: + +```java +public class EdgeDispatcher extends AbstractEdgeDispatcher { + private static final Logger LOGGER = LoggerFactory.getLogger(EdgeDispatcher.class); + + //此Dispatcher的优先级,Order级越小,路由策略优先级越高 + public int getOrder() { + return 10000; + } + + //初始化Dispatcher的路由策略 + public void init(Router router) { + ///捕获 {ServiceComb微服务Name}/{服务路径&参数} 的URL + String regex = "/([^\\\\/]+)/(.*)"; + router.routeWithRegex(regex).handler(CookieHandler.create()); + router.routeWithRegex(regex).handler(createBodyHandler()); + router.routeWithRegex(regex).failureHandler(this::onFailure).handler(this::onRequest); + } + + //处理请求,请注意 + private void onRequest(RoutingContext context) { + Map<String, String> pathParams = context.pathParams(); + //从匹配的param0拿到{ServiceComb微服务Name} + final String service = pathParams.get("param0"); + //从匹配的param1拿到{服务路径&参数} + String path = "/" + pathParams.get("param1"); + + //还记得我们之前说的做出一点点改进吗?引入一个自定义配置edge.routing-short-path.{简称},映射微服务名;如果简称没有配置,那么就认为直接是微服务的名 + final String serviceName = DynamicPropertyFactory.getInstance() + .getStringProperty("edge.routing-short-path." + service, service).get(); + + //创建一个Edge转发 + EdgeInvocation edgeInvocation = new EdgeInvocation(); + //允许接受任意版本的微服务实例作为Provider,未来我们会使用此(设置版本)能力实现灰度发布 + edgeInvocation.setVersionRule(DefinitionConst.VERSION_RULE_ALL); + edgeInvocation.init(serviceName, context, path, httpServerFilters); + edgeInvocation.edgeInvoke(); + } +} +``` + +##### 第三步:加载调度器Dispatcher +ServiceComb Edge使用SPI(Service Provider Interface)的方式加载已经编写好的调度器Dispatcher,在resources目录下创建`META-INF.services/org.apache.servicecomb.transport.rest.vertx.VertxHttpDispatcher`配置文件,写入上一步EdgeDispatcher的类全名: + +```text +{EdgeDispatcher的包名}.EdgeDispatcher +``` + +##### 第四步:配置microservice.yaml +边缘服务本身也是一个微服务,同样需要配置microservice.yaml: + +```yaml +APPLICATION_ID: scaffold +service_description: + name: edge-service + version: 0.0.1 +servicecomb: + service: + registry: + #配置ServiceCenter使得Edge能够发现其他微服务 + address: http://127.0.0.1:30100 + #配置Rest Endpoint + rest: + address: 0.0.0.0:9090 + +#自定义的简称机制配置(这是我们自行扩展实现的) +edge: + routing-short-path: + user: user-service +``` + +>提示: +>1. 除了配置Rest Endpoint,我们也支持配置Highway Endpoint,但Highway Endpoint只支持ServiceComb开发的微服务调用; +>2. microservice.yaml中没有配置Handler,Edge支持所有Consumer端Handler,不支持Producer端Handler,调用链原理如下: +> +>![EdgeOnlySupportConsumerHandler](/assets/images/scaffold/EdgeOnlySupportConsumerHandler.png) +> + +## 5.4 验证边缘服务 +启动用户微服务和Edge服务,使用[Postman](https://www.getpostman.com/)注册一个用户: + +![LogonViaEdge](/assets/images/scaffold/LogonViaEdge.png) + +成功,现在我们使用新注册的用户名`ldg`登录: + +![LoginViaEdge](/assets/images/scaffold/LoginViaEdge.png) + +同样成功,并在Response中已经包含了正确的`AUTHORIZATION`Header。 + +## 5.5 性能比拼 +ServiceComb Java Chassis也支持集成Netflix Zuul作为网关服务,我们做了一次性能比较,使用ServiceComb Edge作为网关吞吐能力大幅优于Netflix Zuul,性能测试项目源代码在[这里](https://github.com/zhengyangyong/gateway-perf)。 + +# 6 扩展边缘服务支持统一认证 +## 6.1 设计思路 +正如前面提到的,统一认证的目的是在Edge入口处进行访问认证,避免需要在所有的微服务中都承载重复的认证机制,因此: +1. 我们先要将认证功能作为一个独立的Procuder发布出来,使Edge服务能够随时认证Token,我们将其命名为`AuthenticationService`,放在用户服务中; +2. 将无需认证的访问请求识别出来,包括: + +| 功能 | 描述 | +| :------- | :--------------------- | +| login | 登录验证,通过后为用户生成Token | +| logon | 新用户注册 | + +除此之外其他业务请求都需要做Token认证; + +3. Edge服务转发访问请求之前,对需要认证的请求先做统一认证,认证通过之后才转发,我们使用`HttpServerFilter`扩展这个能力: + +![FilterChain](/assets/images/scaffold/FilterChain.png) + +统一认证流程时序图为: + +![EdgeAuth](/assets/images/scaffold/EdgeAuth.png) + +## 6.2 实现统一认证 +##### 第一步:发布认证服务 +###### 定义AuthenticationService +```java +public interface AuthenticationService { + String validate(String token); +} +``` +###### 实现并发布AuthenticationService +```java +@RestSchema(schemaId = "authentication") +@RequestMapping(path = "/") +public class AuthenticationServiceImpl implements AuthenticationService { + + private final TokenStore tokenStore; + + @Autowired + public AuthenticationServiceImpl(TokenStore tokenStore) { + this.tokenStore = tokenStore; + } + + @Override + @GetMapping(path = "validate") + public String validate(String token) { + String userName = tokenStore.validate(token); + if (userName == null) { + throw new InvocationException(BAD_REQUEST, "incorrect token"); + } + return userName; + } +} +``` + +##### 第二步:实现统一认证AuthenticationFilter +```java +public class AuthenticationFilter implements HttpServerFilter { + + private final RestTemplate template = RestTemplateBuilder.create(); + + private static final String USER_SERVICE_NAME = "user-service"; + + public static final String EDGE_AUTHENTICATION_NAME = "edge-authentication-name"; + + private static final Set<String> NOT_REQUIRED_VERIFICATION_USER_SERVICE_METHODS = new HashSet<>( + Arrays.asList("login", "logon", "validate")); + + @Override + public int getOrder() { + return 0; + } + + @Override + public Response afterReceiveRequest(Invocation invocation, HttpServletRequestEx httpServletRequestEx) { + if (isInvocationNeedValidate(invocation.getMicroserviceName(), invocation.getOperationName())) { + String token = httpServletRequestEx.getHeader(AUTHORIZATION); + if (StringUtils.isNotEmpty(token)) { + String userName = template + .getForObject("cse://" + USER_SERVICE_NAME + "/validate?token={token}", String.class, token); + if (StringUtils.isNotEmpty(userName)) { + //Add header + invocation.getContext().put(EDGE_AUTHENTICATION_NAME, userName); + } else { + return Response + .failResp(new InvocationException(Status.UNAUTHORIZED, "authentication failed, invalid token")); + } + } else { + return Response.failResp( + new InvocationException(Status.UNAUTHORIZED, "authentication failed, missing AUTHORIZATION header")); + } + } + return null; + } + + private boolean isInvocationNeedValidate(String serviceName, String operationPath) { + if (USER_SERVICE_NAME.equals(serviceName)) { + for (String method : NOT_REQUIRED_VERIFICATION_USER_SERVICE_METHODS) { + if (operationPath.startsWith(method)) { + return false; + } + } + } + return true; + } +} +``` + +别忘了通过SPI机制加载它,在`resources\META-INF\services`目录中创建`org.apache.servicecomb.common.rest.filter.HttpServerFilter`文件: +```text +org.apache.servicecomb.scaffold.edge.filter.AuthenticationFilter +``` + +##### 第三步:在用户微服务中增加修改密码的功能用于验证 +现有的`login`和`logon`都无需认证,因此我们在用户微服务中增加需要认证的修改密码的功能用于验证统一认证。 +###### 在UserService中添加修改密码 +```java +public interface UserService { + ResponseEntity<Boolean> logon(UserDTO user); + + ResponseEntity<Boolean> login(UserDTO user); + //需要认证的修改密码功能 + ResponseEntity<Boolean> changePassword(UserUpdateDTO userUpdate); +} +``` + +###### 在UserServiceImpl中实现修改密码 +```java +@Override +@PostMapping(path = "changePassword") +public ResponseEntity<Boolean> changePassword(@RequestBody UserUpdateDTO userUpdate) { + if (validateUserUpdate(userUpdate)) { + UserEntity dbUser = repository.findByName(userUpdate.getName()); + if (dbUser != null) { + if (dbUser.getPassword().equals(userUpdate.getOldPassword())) { + dbUser.setPassword(userUpdate.getNewPassword()); + repository.save(dbUser); + return new ResponseEntity<>(true, HttpStatus.OK); + } + throw new InvocationException(BAD_REQUEST, "wrong password"); + } + throw new InvocationException(BAD_REQUEST, "user name not exist"); + } + throw new InvocationException(BAD_REQUEST, "incorrect user"); +} +``` + +## 6.3 验证实现的统一认证 +##### 确认AuthenticationFilter在Edge服务中成功加载 +在Edge服务的启动日志中能够找到: +```text +2018-07-13 14:38:48,756 [INFO] 1. org.apache.servicecomb.scaffold.edge.filter.AuthenticationFilter. org.apache.servicecomb.foundation.common.utils.SPIServiceUtils.loadSortedService(SPIServiceUtils.java:79) +``` + +##### 用户登录 +使用zhengyangyong登录: + +![LoginFirst](/assets/images/scaffold/LoginFirst.png) + +拿到的Token值为:eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ6aGVuZ3lhbmd5b25nIiwiZXhwIjoxNTMwNjA4OTczfQ.90teWUNbypPZvds_SD7Kus_y7wLc4b6VzC_aIVg8sLItKxwQ0g4V9BDU665PlqQY5KM-mnk8y0R6ENL1T8YVFg + +##### 不带Authorization Header请求changePassword + +![NoAuthHeader](/assets/images/scaffold/NoAuthHeader.png) + +返回的失败信息是:authentication failed, missing AUTHORIZATION header + +##### 使用错误的Token请求changePassword + +![ErrorAuthHeader](/assets/images/scaffold/ErrorAuthHeader.png) + +返回的失败信息是:authentication failed : InvocationException: code=400;msg=CommonExceptionData [message=incorrect token] + +##### 使用正确的Token请求changePassword + +![AuthSuccess](/assets/images/scaffold/AuthSuccess.png) + +修改密码成功。 + +**这里可能有疑问,使用zhengyangyong登录后,是可以通过这个Token修改其他用户例如lidagang的密码的,这是因为我们目前构建的validate仅检查Token的有效性,而不做权限检查,基于RBAC的角色权限管理系统将会在未来构建。** + +>提示: +>1. AuthenticationFilter的完整[代码](https://github.com/zhengyangyong/scaffold/blob/master/edge-service/src/main/java/org/apache/servicecomb/scaffold/edge/filter/AuthenticationFilter.java); +>2. HttpServerFilter的[介绍](https://github.com/apache/incubator-servicecomb-docs/blob/master/java-chassis-reference/zh_CN/general-development/http-filter.md)。 + +# 7.小结 + + 本文详细介绍了如何使用http://start.servicecomb.io脚手架快速构建微服务项目、使用领域驱动设计(Domain-Driven Design,DDD)设计地产CRM系统、使用Edge Service构建统一认证边缘服务等内容。至此,一个地产客户关系管理系统的骨架已经初步搭建起来,剩下的模块,我们将在接下来的文章里详细介绍。 + + + diff --git a/assets/images/scaffold/OutputHello.png b/assets/images/scaffold/OutputHello.png index c514cea..5f1ab6b 100644 Binary files a/assets/images/scaffold/OutputHello.png and b/assets/images/scaffold/OutputHello.png differ diff --git a/assets/images/scaffold/StartServiceComb.io.png b/assets/images/scaffold/StartServiceComb.io.png new file mode 100644 index 0000000..568ea81 Binary files /dev/null and b/assets/images/scaffold/StartServiceComb.io.png differ