在今年的LC3大會上,ServiceComb展臺所展示的demo視頻“30分鐘開發雛形CRM應用”引起了參會者的廣泛關注,大家紛紛對其背後的技術表現出濃厚的興趣。本文將從房地產企業的客戶管理管理場景入手,使用領域驅動設計,深入技術細節,詳解如何快速開發落地一個微服務化的客戶管理系統。
牛刀小試
打開瀏覽器,輸入地址http://start.servicecomb.io/打開SERVICECOMB SPRING INITIALIZR,修改Project Metadata中的Group,Artifact和ServiceComb Parameters中的ServiceCenter Address,Governance等,點擊GenerateProject,解壓生成下載的demo.zip。
運行它也很簡單,使用IDE打開項目,DEBUG -> Application.java,或在命令行:
稍等微服務啟動就緒,打開瀏覽器輸入http://localhost:9080/hello驗證一下:
是不是非常輕鬆呢?
腳手架
在建築領域,腳手架是施工現場為方便工人操作並解決垂直和水平運輸而搭設的各種支架以及平臺。
在軟件開發領域,它引申為預提供一些基礎框架代碼加速開發過程,避免從零開始構建項目。用戶只需要依據需求場景選擇合適的腳手架,然後填充定製的業務邏輯即可,不必再去處理一些基礎功能,例如數據庫連接、日誌實現、RPC傳輸等。
微服務框架一般都會提供腳手架功能,例如Spring,提供了SPRING INITIALIZR;ServiceComb基於SPRING INITIALIZR,提供了更具優勢的特性:
- 生成的項目除了在POM中自動添加必要的依賴,還會提供Producer和Consumer示例代碼(Hello World);
- 會進一步提供Edge Server、Authcation Server等更貼近業務的腳手架項目,讓用戶能快速構建體系完整的微服務系統。
那麼什麼叫一個完整的微服務系統呢?我們可以拿一個具體的場景做例子,會更有感覺:
場景:地產CRM
您經營著一家房地產開發商,銷售房產,迫切需要一套銷售系統,考慮到微服務的優勢,您決定使用微服務的方式構建系統;主要的業務流程也非常簡單:用戶前來購買購買產品(房產),首先需要登記用戶信息,並繳納一定數量的定金,待交易當日,挑選心儀的產品(房產),支付尾款,完成交易。
1、使用DDD指導地產CRM系統的設計
微服務系統的設計方面,領域驅動設計(Domain-Driven Design,DDD)是一個從業務出發的好選擇,它由Eric Evans提出,是一種全新的系統設計和建模方法,這裡的模型指的就是領域模型(DomainModel)。領域模型通過聚合(Aggregate)組織在一起,聚合間有明顯的業務邊界,這些邊界將領域劃分為一個個限界上下文(Bounded Context)。Martin Fowler對它們都有詳細的解讀 [1]。
理論概念都搞清楚了,那麼怎麼來找模型和聚合呢?一個非常流行的方法就是Event Storming [2],它是由AlbertoBrandolini發明,經歷了DDD社區和很多團隊的實踐,也是一種非常有參與感的團隊活動:
上圖就是我們對地產CRM這個場景使用Event Storming探索的結果,現在我們能夠將限界上下文清晰的梳理出來:
提示:Event Storming是一項非常有創造性的活動,也是一個持續討論和反覆改進的過程,不同的團隊關注的核心域(Core Domain)不同,得到的最終結果也會有差異。我們的目的是為了演示完整的微服務系統構建的過程,並不涉及商業核心競爭力方面的探討,因此沒有Core Domain和Sub Domain之類的偏重。
2、將分析成果轉化為方案域設計
當我們完成所有的限界上下文的識別後,可以直接將它們落地為微服務:
- 用戶服務:提供用戶信息管理服務,這裡保存這用戶的賬號和密碼,負責登錄和認證;
- 產品(房產)服務:提供產品管理服務,保存著房產的信息諸如價格、是否已售出等信息;
- 支付服務:提供交易時支付服務,模擬對接銀行支付定金,以及購房時支付尾款;
由於完成一筆交易是一個複雜的流程,與這三個微服務都有關聯,因此我們引入了一個複合服務——交易服務:
- 交易服務:提供產品交易服務。它通過編排調用將整個交易流程串起來,交易服務中有兩個流程:
- 定金支付
Step1:通過用戶服務驗證用戶身份;
Step2:通過支付服務請求銀行扣款,增加定金賬號內的定金;
- 購房交易
Step1:通過用戶服務驗證用戶身份;
Step2:通過資源服務確定用戶希望購買的資源(房產)尚未售出;
Step3:通過資源服務標記目標資源(房產)已售出;
Step4:通過支付服務請求扣減定金賬號內的定金,以及銀行扣剩下的尾款;
最後兩個步驟需要保證事務一致性,其中Step4包含兩個扣款操作。
之後,我們引入Edge服務提供統一入口:
- Edge服務:很多時候也被稱為API網關(API Gateway),負責集中認證、動態路由等等;
提示:Edge服務需要依賴服務註冊-發現機制,因此同時導入了ServiceCenter。
最後還需要提供UI:
- 前端UI(同樣以微服務方式提供):用戶交互界面;
至此,DDD設計地產CRM的工作就結束了。
快速實現客戶關係管理系統的用戶服務
1、用戶微服務並不簡單
用戶微服務是所有系統中不可或缺的部分,它承載了認證和授權等核心功能——無論是登錄一個網站、還是打開一個APP,當涉及到需要身份識別後才能夠執行的操作,都需要用戶微服務把關。例如觀看視頻網站上的視頻,匿名用戶會插播廣告,如果希望屏蔽廣告,則需要登錄併購買VIP會員,登錄即是身份認證的過程,而VIP屏蔽廣告即是授權的過程。
認證
認證不僅僅是一次性驗證用戶名和密碼的過程,還需要能反覆使用認證的結果,確保後繼所有操作都是合法的,這就涉及到“有狀態”,但HTTP是一個無狀態協議,如何能夠將登錄成功後的認證信息與後繼的請求關聯起來呢?
我們非常熟悉的做法是使用Session或Cookie:
- Session存儲在服務端,因此具備良好的防篡改能力,但弊端是使服務有狀態,微服務系統中,同一個微服務會依據系統壓力的大小彈性伸縮出多個運行實例負載均衡,跨實例訪問會狀態丟失。
- Cookie存儲在客戶端,它正好與Session相反,優勢是服務不必保持狀態,但弊端是客戶比較容易的篡改Cookie信息,例如修改過期時間以逃避驗證,而且瀏覽器對Cookie也有較多限制。
那麼,如何兼顧這兩方面的需求呢?Token就是一個比較好的解決方案。
Token中文翻譯為令牌,它將登錄認證後的信息簽名後返回,服務端不保存,客戶端請求的時候將認證的完整信息附帶上提供給服務端驗籤,簽名可以保證信息不被篡改。瞭解了了解Token的原理,自然要關注Token的格式,JWT就是這樣一個基於JSON的開放標準RFC-7519 [3]。
JWT (Java Web Token)規範
簡而言之JWT規範由三部分構成:
1、Header: 聲明Token的類型也就是JWT,以及加密算法,例如:
{
"typ":"JWT",
"alg":"HS256"
}
2、Playload:存放有效信息,既包含標準簽發者、用戶、簽發時間、過期時間,唯一標識等信息;也可以存放用戶自定義的聲明信息,例如權限控制相關的內容,例如:
{
"sub":"1234567890",
"name":"YangYongZheng",
"iat":1516239022
}
3、Signature:簽名信息,包含Header和Playload的原始信息(Base64編碼過)以及簽名過後的信息。
提示:JWT IO提供了在線編碼和解碼工具 [4]。
授權的本意是指將完成某項工作所必須的權力授給下屬人員,在軟件系統中往往引申為使人或角色具備訪問特定資源或更改行為的許可。例如之前提到的VIP屏蔽廣告,即是視頻網站允許播放終端在特定的帳號登錄後跳過廣告播放環節(行為)的許可。
授權系統比較常見的做法有ACL和RBAC:
- ACL:ACL全稱Access Control List,它是以受控資源為核心,每一個受控資源,都有一個權限控制列表記錄哪些用戶或角色對這項資源執行具體操作(也被稱為授權點)的權限設置,例如查詢(可見)、修改、刪除等等。Windows中的文件系統安全即是一個經典的ACL實現案例:
- RBAC:RBAC全稱Role Based Access Control,與ACL相比,它以角色為核心,權限落地在角色上,不為特定用戶授權。它的優勢是大幅簡化了用戶與權限的管理,在受控對象不多或控制粒度要求不高(例如接口訪問控制)的場景下非常適用。
由於微服務系統的權限控制主要是接口訪問控制上,並且多采用用戶組方式組織用戶,因此RBAC是比較流行的做法。
2、實現用戶微服務
第一步:創建微服務項目
使用SERVICECOMBSPRING INITIALIZR創建用戶微服務,創建完畢後使用IDEA或Eclipse打開項目,我們刪掉HelloImpl和HelloConsumer,之後添加自己的實現。
第二步:使用MySQL持久化用戶信息
用戶微服務需要持久化用戶信息,我們使用MySQL數據庫,ORM使用SpringData JPA:
引入依賴
mysql mysql-connector-java
org.springframework.boot spring-boot-starter-data-jpa
定義存儲User信息的UserEntity實體
@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操作
@Repository
public interface UserRepository extends PagingAndSortingRepository{
UserEntity findByName(String name);
}
配置數據庫連接
在項目的resources目錄下新增application.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的更多資料請參見這篇文檔 [5],為了能夠簡化依賴的引入我們實際上使用的是Spring Boot JPA Starter,詳細的例子請參見這篇文檔 [6]。
第三步:實現JWT認證
定義JWT接口
public interface TokenStore{
String generate(String userName);
booleanvalidate(String token);
}
generate用於生成Token,validate用於驗證Token是否正確。
實現TokenStore
我們使用jjwt [7]提供的JWT實現,創建JwtTokenStore類,繼承TokenStore接口,並重寫方法:
@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);
}
returnfalse;
}
}
第四步:實現用戶服務
定義UserService接口
public interface UserService{
ResponseEntitylogon(UserDTO user);
ResponseEntitylogin(UserDTO user);
}
logon用於新用戶註冊,login用於用戶登錄驗證,UserDTO用於參數傳遞:
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接口:
@RestSchema(schemaId ="user")
@RequestMapping(path ="/")
public class UserServiceImpl implements UserService{
private final UserRepository repository;
private final TokenStore tokenStore;
@Autowired
public UserServiceImpl(UserRepository repository, TokenStoretokenStore){
this.repository = repository;
this.tokenStore = tokenStore;
}
@Override
@PostMapping(path ="logon")
public ResponseEntitylogon(@RequestBody UserDTOuser){
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 namehad exist");
}
throw new InvocationException(BAD_REQUEST,"incorrectuser");
}
@Override
@PostMapping(path ="login")
public ResponseEntitylogin(@RequestBody UserDTOuser){
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);
//addauthentication header
return new ResponseEntity<>(true, headers, HttpStatus.OK);
}
throw new InvocationException(BAD_REQUEST,"wrongpassword");
}
throw new InvocationException(BAD_REQUEST,"user namenot exist");
}
throw new InvocationException(BAD_REQUEST,"incorrectuser");
}
private boolean validateUser(UserDTO user){
return user != null && StringUtils.isNotEmpty(user.getName())&& StringUtils.isNotEmpty(user.getPassword());
}
private HttpHeaders generateAuthenticationHeaders(String token){
HttpHeaders headers =newHttpHeaders();
headers.add(AUTHORIZATION, token);
return headers;
}
}
登錄成功後,會從TokenStore生成Token,並將其寫入Key為AUTHORIZATION的Header。
由於我們允許任何用戶註冊和登錄,所以目前還沒有授權的需求,經過上面四步,具有基本註冊和登錄功能的用戶微服務就構建好了。
3、驗證實現的用戶服務
啟動用戶微服務,我們先註冊一個賬號:
顯示註冊成功,現在我們使用這個賬號登錄:
返回登錄成功,Response中已經包含了AUTHORIZATIONHeader,後繼的所有請求都需要使用這個Token值進行合法認證。
至此,實現客戶關係管理系統的用戶服務工作就結束了,現在我們會將目光轉移到Edge服務,通過Edge服務作為微服務調用的統一入口,在它之上構建統一認證,應對海量級調用的挑戰。
開發高性能邊緣服務
1、什麼是邊緣服務(Edge Service)
邊緣服務也是一個微服務,微服務化系統通常使用邊緣服務(Edge Service)作為所有其它微服務的統一入口,因此它也常常會被稱為APIGateway,使用邊緣服務的好處有如下幾點:
- 動態路由:動態配置URL地址與微服務之間的對應關係,便於擴展,以及實現版本灰度發佈等;
- 統一認證:在入口處進行訪問認證,避免需要在所有的微服務中都承載重複的認證機制;
- 集中監控:與統一認證類似,在邊緣服務對入口調用進行監控,容易統計流量信息。
2、邊緣服務的作用和原理
我們先來看不使用邊緣服務,UI直接調用用戶服務的場景:
可以看出這種調用方式,UI缺乏一定的靈活性,體現在:
- UI的實現綁定了Chassis的編程語言Java,無法使用PHP等其它前端技術開發;
- UI訪問微服務的路徑無法動態配置,如果作為後端的微服務系統發生調整,則UI很可能需要修改;
- UI很容易混入複合(編排)調用的邏輯,使得結構變得複雜難以維護。
我們再看引入邊緣服務後,UI如何通過邊緣服務調用用戶服務:
Edge服務將在9090端口上接受http rest調用,我們設計了下面的轉發規則:
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,如此一來:
- URL能夠更加簡潔;
- 當微服務名發生變化,只需要調整對應的配置,不需要更改前端UI路徑代碼。
3、實現邊緣服務
第一步:引入Edge Core依賴
org.apache.servicecomb edge-core
Copy
第二步:編寫調度器Dispatcher
Edge服務的核心就是調度器Dispatcher,ServiceComb Edge Core中的Dispatcher基於高性能的Vertx Reactive,輕鬆應對百萬量級API請求的挑戰;只需要繼承AbstractEdgeDispatcher抽象類,添加對應的邏輯即可:
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){
MappathParams = 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(ServiceProvider Interface)的方式加載已經編寫好的調度器Dispatcher,在resources目錄下創建META-INF.services/org.apache.servicecomb.transport.rest.vertx.VertxHttpDispatcher配置文件,寫入上一步EdgeDispatcher的類全名:
{EdgeDispatcher的包名}.EdgeDispatcher
第四步:配置microservice.yaml
邊緣服務本身也是一個微服務,同樣需要配置microservice.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
提示:
- 除了配置Rest Endpoint,我們也支持配置Highway Endpoint,但Highway Endpoint只支持ServiceComb開發的微服務調用;
- microservice.yaml中沒有配置Handler,Edge支持所有Consumer端Handler,不支持Producer端Handler,調用鏈原理如下:
4、驗證邊緣服務
啟動用戶微服務和Edge服務,使用Postman [8]註冊一個用戶:
成功,現在我們使用新註冊的用戶名ldg登錄:
同樣成功,並在Response中已經包含了正確的AUTHORIZATIONHeader。
5、性能比拼
ServiceComb JavaChassis也支持集成Netflix Zuul作為網關服務,我們做了一次性能比較,使用ServiceComb Edge作為網關吞吐能力大幅優於Netflix Zuul,性能測試項目源代碼在這裡 [9]。
擴展邊緣服務支持統一認證
1、設計思路
正如前面提到的,統一認證的目的是在Edge入口處進行訪問認證,避免需要在所有的微服務中都承載重複的認證機制,因此:
- 我們先要將認證功能作為一個獨立的Procuder發佈出來,使Edge服務能夠隨時認證Token,我們將其命名為AuthenticationService,放在用戶服務中;
- 將無需認證的訪問請求識別出來,包括:
除此之外其他業務請求都需要做Token認證;
- Edge服務轉發訪問請求之前,對需要認證的請求先做統一認證,認證通過之後才轉發,我們使用HttpServerFilter擴展這個能力:
統一認證流程時序圖為:
2、實現統一認證
第一步:發佈認證服務
定義AuthenticationService
public interface AuthenticationService{
String validate(String token);
}
Copy
實現併發布AuthenticationService
@RestSchema(schemaId ="authentication")
@RequestMapping(path ="/")
public class AuthenticationServiceImpl implements AuthenticationService{
private final TokenStore tokenStore;
@Autowired
publicAuthenticationServiceImpl(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,"incorrecttoken");
}
return userName;
}
}
第二步:實現統一認證AuthenticationFilter
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 SetNOT_REQUIRED_VERIFICATION_USER_SERVICE_METHODS =new HashSet<>(
Arrays.asList("login","logon","validate"));
@Override
public int getOrder(){
return0;
}
@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,"authenticationfailed, 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;
}
}
Copy
別忘了通過SPI機制加載它,在resources\META-INF\services目錄中創建org.apache.servicecomb.common.rest.filter.HttpServerFilter文件:
org.apache.servicecomb.scaffold.edge.filter.AuthenticationFilter
第三步:在用戶微服務中增加修改密碼的功能用於驗證
現有的login和logon都無需認證,因此我們在用戶微服務中增加需要認證的修改密碼的功能用於驗證統一認證。
在UserService中添加修改密碼
public interface UserService{
ResponseEntitylogon(UserDTO user);
ResponseEntitylogin(UserDTO user);
//需要認證的修改密碼功能
ResponseEntitychangePassword(UserUpdateDTO userUpdate);
}
在UserServiceImpl中實現修改密碼
@Override
@PostMapping(path ="changePassword")
public ResponseEntitychangePassword(@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 newResponseEntity<>(true, HttpStatus.OK);
}
throw new InvocationException(BAD_REQUEST,"wrongpassword");
}
throw new InvocationException(BAD_REQUEST,"user namenot exist");
}
throw new InvocationException(BAD_REQUEST,"incorrectuser");
}
3、驗證實現的統一認證
確認AuthenticationFilter在Edge服務中成功加載
在Edge服務的啟動日誌中能夠找到:
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登錄:
拿到的Token值為:eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ6aGVuZ3lhbmd5b25nIiwiZXhwIjoxNTMwNjA4OTczfQ.90teWUNbypPZvds_SD7Kus_y7wLc4b6VzC_aIVg8sLItKxwQ0g4V9BDU665PlqQY5KM-mnk8y0R6ENL1T8YVFg
不帶Authorization Header請求changePassword
返回的失敗信息是:authenticationfailed, missing AUTHORIZATION header
使用錯誤的Token請求changePassword
返回的失敗信息是:authenticationfailed : InvocationException: code=400;msg=CommonExceptionData[message=incorrect token]
使用正確的Token請求changePassword
修改密碼成功。
這裡可能有疑問,使用zhengyangyong登錄後,是可以通過這個Token修改其他用戶例如lidagang的密碼的,這是因為我們目前構建的validate僅檢查Token的有效性,而不做權限檢查,基於RBAC的角色權限管理系統將會在未來構建。
提示:
- AuthenticationFilter的完整代碼 [10];
- HttpServerFilter的介紹 [11]。
小結
本文詳細介紹瞭如何使用http://start.servicecomb.io腳手架快速構建微服務項目、使用領域驅動設計(Domain-Driven Design,DDD)設計地產CRM系統、使用Edge Service構建統一認證邊緣服務等內容。至此,一個地產客戶關係管理系統的骨架已經初步搭建起來,剩下的模塊,我們將在接下來的文章裡詳細介紹
參考詳情鏈接地址
[1] https://martinfowler.com/tags/domain driven design.html
[2] https://en.wikipedia.org/wiki/Event_storming
[3] https://tools.ietf.org/html/rfc7519
[4] https://jwt.io/
[5] https://projects.spring.io/spring-data-jpa/
[6] https://spring.io/guides/gs/accessing-data-jpa/
[7] https://github.com/jwtk/jjwt
[8] https://www.getpostman.com/
[9] https://github.com/zhengyangyong/gateway-perf
[10] https://github.com/zhengyangyong/scaffold/blob/master/edge-service/src/main/java/org/apache/servicecomb/scaffold/edge/filter/AuthenticationFilter.java
[11] https://github.com/apache/incubator-servicecomb-docs/blob/master/java-chassis-reference/zh_CN/general-development/http-filter.md
ServiceComb相關資料
官方網站:
http://servicecomb.incubator.apache.org/
加入社區:
https://servicecomb.incubator.apache.org/cn/docs/join_the_community
JIRA:
https://issues.apache.org/jira/browse/SCB
ServiceComb Java-Chassis:
https://github.com/apache/incubator-servicecomb-java-chassis
ServiceComb Saga:
https://github.com/apache/incubator-servicecomb-saga
ServiceComb Service-Center:
https://github.com/apache/incubator-servicecomb-service-center
點擊左下角“瞭解更多”,給SeriveComb加個Star吧
閱讀更多 ServiceComb 的文章