对接SaaS
基础概念
SaaS
SaaS是Software-as-a-Service(软件即服务)的简称,它是一种通过互联网提供软件服务的模式,与传统软件相比有如下几点区别:
1、SaaS软件不再是用户向软件供应商定制软件或进行二次开发,而是供应商将软件部署在自己的服务器上并通过互联网提供在线服务。
2、软件供应商负责搭建一切网络设备、软硬件运行平台等基础设施,并全权负责运营和维护软件。
3、用户根据实际需要通过互联网订购所需要的软件服务,按照订购服务的多少和时间长短支付费用。
4、用户不需要一次性支付很大一笔软件定制费,只需支付一些租用费用就可以使用软件,风险非常低,当发现软件不满足需求或不适合公司管理模式可以停止续租。
对于许多中小型企业来说,SaaS模式是采用先进技术的最好途径,它减少了企业购买、构建和维护基础设施和应用程序的成本,大大降低了软件费用。
云计算的三种模式:
云计算的发展至今有十几年的历史,如今云计算得到了广泛的市场应用,具体包括三个层次:
IaaS: Infrastructure-as-a-Service(基础设施即服务),也叫Hardware-as-a-Service,将计算机硬件资源打包对外提供服务,比如云主机、云存储等。
PaaS: Platform-as-a-Service(平台即服务),也叫中间件服务,比如:MySQL数据库服务、ElasticSearch搜索服务等。
SaaS: Software-as-a-Service(软件即服务) ,提供具体应用软件服务,比如:CRM系统,电商平台等。
多租户
当一个使用SaaS模式部署的软件同时有多个企业用户租用时,每一个企业都是独立的租用者,我们通常称他为:租户(tenant);同时有多个租用者,那就是多租户(multi-tenant)。多租户(Multi-tenant)是SaaS最重要的核心概念和关键技术。
什么是租户?
一个“组织”在某软件平台上购买了部分软件服务(权限集合),这个“组织”就是这个软件平台的“租户”。“组织”就是指人们为实现一定的目标,互相协作结合而成的集体或团体,如某企业、某学校、某机构、某商户、甚至某家庭。
什么是租户类型?
闪聚支付平台作为一个SaaS平台,提供多租户管理,具有相同业务的为同一类租户,比如:“商户”是一类租户,“XX超市”则是一个具体的商户(租户)。
什么是用户?
“用户”是“组织”(租户)内成员,是软件平台的实际使用者,使用者凭身份(用户名)、凭证(密码)登入平台。
“用户”可以在其所在“租户”内新建其它用户,并在“租户”购买的权限集合范围内对其它用户授权,称之为用户、权限“自管理”。
整个软件平台被很多个租户共同使用,引入“多租户”的用户架构是为了让组织得到用户、权限“自管理”的支持,并与其它“租户”的数据隔离。
下图是闪聚支付平台的多租户结构图:
闪聚支付平台由支付系统、运营系统、统计分析系统、员工管理系统等模块组成,租户根据自己的需求购买平台的服务。比如:以支付为主的租户,他需要购买支付系统、员工管理系统、统计分析系统;需要运营管理的租户则需要购买运营系统。
如何购买服务呢?
闪聚支付平台将平台的系统功能集合作成一个一个的套餐,比如:“套餐A” 包括支付系统、员工管理系统、统计分析系统,以支付为主的租户购买套餐A就拥有了支付系统、员工管理系统、统计分析系统的使用权限。
对接SaaS步骤
业务模型
商户平台应用与SaaS平台模型的对应关系如下:
左侧为商户平台,右侧为SaaS平台。
商户平台的商户对应SaaS平台的租户。
商户平台的员工对应SaaS平台的用户。
门店是什么?
门店是商户平台中员工的组织机构,比如:本商户是一个大型超市,该大型超市在很多地方有自己的连锁店,这个连锁店就是门店,每个连锁店都有自己的员工。
门店和SaaS平台有对应的模型吗?
门店是商户平台由于经营需要所设立的组织机构,在SaaS平台中没有对应模型。
接入步骤
一个商户注册相当于一个租户在SaaS平台注册,下图展示了商户注册时商户服务与SaaS的交互流程:
商户注册,调用SaaS平台的新增租户与用户接口,每个步骤如下:
1、新增租户(上图第9步)
在商户平台新增商户。
调用SaaS系统的接口新增租户。
2、新增用户(上图第10-11步)
在商户平台 新增员工。
调用SaaS的接口新增用户,且自动设置用户和租户的绑定关系,并将该用户设置为该租户的管理员。
3、初始化租户权限(上图第12步)
商户注册成功为该租户设置默认权限。
4、更新管理员的权限(上图第13步)
为第2步新增的账号分配管理员权限,管理员权限即是租户的权限。
商户平台 实际接入SaaS开发步骤如下:
1、部署SaaS系统
SaaS系统是闪聚支付平台独立的系统,我们需要部署SaaS系统并实现商户平台与其对接。
2、商户注册后调用SaaS系统的接口完成上边流程的对接
1)商户平台完成的功能如下:
- 新增商户
- 新增员工
- 新增门店
- 为门店管理员功能
2)调用SaaS系统的接口完成的功能如下:
- 新增租户
- 新增用户
- 自动设置用户和租户的绑定关系,并将该用户设置为该租户的管理员
- 为该租户设置默认权限
- 分配管理员权限
部署SaaS系统
初始化数据
执行 shanjupay_saas.sql 脚本导入SaaS系统数据库。
部署服务
完善配置
网关服务
- 在Nacos上添加jwt.yaml配置,Group: COMMON_GROUP,如下图所示
siging-key: shanju123
2. 将资料中的gateway-service.yaml配置添加到Nacos上,Group:SHANJUPAY_GROUP
#路由规则
zuul:retryable: trueadd-host-header: trueignoredServices: "*"sensitiveHeaders: "*"routes:operation-application:path: /operation/**stripPrefix: falsemerchant-application:path: /merchant/**stripPrefix: falseuaa-service: path: /uaa/**stripPrefix: falsefeign:hystrix:enabled: truecompression:request:enabled: true # 配置请求GZIP压缩mime-types: ["text/xml","application/xml","application/json"] # 配置压缩支持的MIME TYPEmin-request-size: 2048 # 配置压缩数据大小的下限response:enabled: true # 配置响应GZIP压缩hystrix:command:default:execution:isolation:thread:timeoutInMilliseconds: 93000 # 设置熔断超时时间 default 1000timeout:enabled: true # 打开超时熔断功能 default trueribbon:nacos:enabled: true # 不知道是否生效ConnectTimeout: 3000 # 设置连接超时时间 default 2000ReadTimeout: 20000 # 设置读取超时时间 default 5000OkToRetryOnAllOperations: false # 对所有操作请求都进行重试 default falseMaxAutoRetriesNextServer: 1 # 切换实例的重试次数 default 1MaxAutoRetries: 1 # 对当前实例的重试次数 default 0
修改shanjupay-gateway-service工程下的bootstrap.yml中namespace为自己Nacos中的namespace id
UAA服务
1、在Nacos中添加uaa-service.yaml配置,Group:SHANJUPAY_GROUP
# 覆盖spring‐boot‐http.yaml的项目
server:servlet:context-path: /uaa# 覆盖spring‐boot‐starter‐druid.yaml的项目
spring:datasource:druid:url: jdbc:mysql://127.0.0.1:3306/shanjupay_uaa?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=falseusername: rootpassword: root
2、修改shanjupay-uaa-service工程下的bootstrap.yml中namespace为自己Nacos中的namespace id
统一账户服务
1、在Nacos中添加user-service.yaml配置,Group:SHANJUPAY_GROUP
# 覆盖spring‐boot‐http.yaml的项目
server:servlet:context-path: /user# 覆盖spring‐boot‐starter‐druid.yaml的项目
spring:datasource:druid:url: jdbc:mysql://127.0.0.1:3306/shanjupay_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=falseusername: rootpassword: root# 覆盖spring‐boot‐mybatis‐plus.yaml的项目
mybatis-plus:type-aliases-package: com.shanjupay.user.entitymapper-locations: classpath:com/shanjupay/*/mapper/xml/*.xmlsms:url: "http://localhost:56085/sailing"effectiveTime: 6000
2、修改shanjupay-user-service工程下的bootstrap.yml中namespace为自己Nacos中的namespace id
对接SaaS代码实现
根据前边接口SaaS步骤 的分析,需要在商户平台 完成新增员工、新增门店、设置门店管理员等功能。
商户服务新增门店接口
接口定义
在商户服务定义新增门店接口
1、接口描述如下:
1)商户注册的同时新增默认门店
2、接口定义如下:
生成StoreDTO类。
在MerchantService接口类中定义如下接口:
/*** 新增门店* @param storeDTO 门店信息* @return 新增成功的门店信息* @throws BusinessException*/
StoreDTO createStore(StoreDTO storeDTO) throws BusinessException;
接口实现
编写StoreConvert。
在MerchantServiceImpl类中实现createStore方法。
@Autowired
private StoreMapper storeMapper;/*** 新增门店** @param storeDTO 门店信息* @return 新增成功的门店信息* @throws BusinessException*/
@Override
public StoreDTO createStore(StoreDTO storeDTO) throws BusinessException {Store entity = StoreConvert.INSTANCE.dto2entity(storeDTO);log.info("商户下新增门店:{}", JSON.toJSONString(entity));//新增门店storeMapper.insert(entity);return StoreConvert.INSTANCE.entity2dto(entity);
}
商户服务新增员工接口
接口定义
在商户服务定义新增员工接口
1、接口描述如下:
1)接收商户填写的员工数据
2)请求商户服务进行新增员工
员工的账号和手机号需要保持唯一性。
3)返回结果
2、接口定义如下:
生成StaffDTO类。
在MerchantService接口类中定义如下接口:
/*** 商户下新增员工* @param staffDTO 员工信息* @return 新增成功的员工信息* @throws BusinessException*/
StaffDTO createStaff(StaffDTO staffDTO) throws BusinessException;
接口实现
编写StaffConvert。
在MerchantServiceImpl类中定义createStaff方法:
@Autowired
private StaffMapper staffMapper;/*** 商户下新增员工** @param staffDTO 员工信息* @return 新增成功的员工信息* @throws BusinessException*/
@Override
public StaffDTO createStaff(StaffDTO staffDTO) throws BusinessException {//参数合法性校验,校验手机号格式及是否存在if (staffDTO == null || StringUtils.isBlank(staffDTO.getMobile())|| StringUtils.isBlank(staffDTO.getUsername())|| staffDTO.getStoreId() == null) {throw new BusinessException(CommonErrorCode.E_300009);}//校验用户名是否为空String username = staffDTO.getUsername();if (StringUtils.isBlank(username)) {throw new BusinessException(CommonErrorCode.E_100110);}//根据商户id和账号校验唯一性Boolean existStaffByUserName = isExistStaffByUserName(username, staffDTO.getMerchantId());if (existStaffByUserName) {throw new BusinessException(CommonErrorCode.E_100114);}//根据商户id和手机号校验唯一性Boolean existStaffByMobile = isExistStaffByMobile(staffDTO.getMobile(), staffDTO.getMerchantId());if (existStaffByMobile) {throw new BusinessException(CommonErrorCode.E_100113);}Staff staff = StaffConvert.INSTANCE.dto2entity(staffDTO);log.info("商户下新增员工");staffMapper.insert(staff);return StaffConvert.INSTANCE.entity2dto(staff);
}/*** 根据手机号判断员工是否已在指定商户存在* @param mobile* @param merchantId* @return*/
Boolean isExistStaffByMobile(String mobile, Long merchantId) {Integer count = staffMapper.selectCount(new LambdaQueryWrapper<Staff>().eq(Staff::getMobile, mobile).eq(Staff::getMerchantId, merchantId));return count > 0;
}
/*** 根据账号判断员工是否已在指定商户存在* @param username* @param merchantId* @return*/
Boolean isExistStaffByUserName(String username, Long merchantId) {Integer count = staffMapper.selectCount(new LambdaQueryWrapper<Staff>().eq(Staff::getUsername, username).eq(Staff::getMerchantId, merchantId));return count > 0;
}
商户服务门店设置管理员接口
接口定义
1、接口描述如下:绑定门店和员工对应关系
2、接口定义如下:
在MerchantService接口类中定义如下接口:
/*** 将员工设置为门店的管理员* @param storeId* @param staffId* @throws BusinessException*/
void bindStaffToStore(Long storeId, Long staffId) throws BusinessException;
接口实现
在MerchantServiceImpl实现bindStaffToStore。
@Autowired
private StoreStaffMapper storeStaffMapper;/*** 将员工设置为门店的管理员* @param storeId* @param staffId* @throws BusinessException*/
@Override
public void bindStaffToStore(Long storeId, Long staffId) throws BusinessException {StoreStaff storeStaff = new StoreStaff();storeStaff.setStaffId(staffId);//员工idstoreStaff.setStoreId(storeId);//门店idstoreStaffMapper.insert(storeStaff);
}
商户服务商户注册接口修改
接口说明
SaaS系统的用户服务shanjupay-user工程提供注册租户的接口,如下 :
/*** 创建租户如果已存在租户则返回租户信息,否则新增租户、新增租户管理员,同时初始化权限* 1.若管理员用户名已存在(目前不会出现,用户名内部随机生成),禁止创建* 2.手机号已存在,禁止创建* 3.创建根租户对应账号时,需要手机号,账号的用户名密码* @param createTenantRequest 创建租户请求信息* @return*/
TenantDTO createTenantAndAccount(CreateTenantRequestDTO createTenantRequest);
接口参数:
- 手机号
- 账号
- 密码
- 租户类型:shanju-merchant
- 默认套餐:shanju-merchant
- 租户名称,同账号名
接口实现
1、添加SaaS的用户服务API依赖:打开shanjupay-merchant-service工程的pom.xml
<!--SaaS的用户服务API依赖-->
<dependency><groupId>com.shanjupay</groupId><artifactId>shanjupay-user-api</artifactId><version>1.0-SNAPSHOT</version>
</dependency>
2、MerchantServiceImpl,完善createMerchant()方法:
@Reference
private TenantService tenantService;/*** 注册商户服务接口,接收账号、密码、手机号,为了可扩展性使用merchantDto接收数据* 调用SaaS接口:新增租户、用户、绑定租户和用户的关系,初始化权限* @param merchantDTO 商户注册信息* @return 注册成功的商户信息*/
@Override
@Transactional
public MerchantDTO createMerchant(MerchantDTO merchantDTO) throws BusinessException {//校验参数的合法性if (merchantDTO == null) {throw new BusinessException(CommonErrorCode.E_100108);}//手机号非空校验if (StringUtils.isBlank(merchantDTO.getMobile())) {throw new BusinessException(CommonErrorCode.E_100112);}//手机号格式的合法性校验if (!PhoneUtil.isMatches(merchantDTO.getMobile())) {throw new BusinessException(CommonErrorCode.E_100109);}//联系人非空校验if (StringUtils.isBlank(merchantDTO.getUsername())) {throw new BusinessException(CommonErrorCode.E_100110);}//密码非空校验if (StringUtils.isBlank(merchantDTO.getPassword())) {throw new BusinessException(CommonErrorCode.E_100111);}//校验商户手机号的唯一性,根据商户手机号查询商户表,如果存在记录则说明已有相同的手机号重复Integer count = merchantMapper.selectCount(new LambdaQueryWrapper<Merchant>().eq(Merchant::getMobile, merchantDTO.getMobile()));if (count > 0) {throw new BusinessException(CommonErrorCode.E_100113);}//Merchant merchant = new Merchant();//设置审核状态0‐未申请,1‐已申请待审核,2‐审核通过,3‐审核拒绝//merchant.setAuditStatus("0");// 设置手机号//merchant.setMobile(merchantDTO.getMobile());//设置联系人//merchant.setUsername(merchantDTO.getUsername());//..写入其它属性//=======================================================================================//调用SaaS接口,构建调用参数,添加租户 和账号 并绑定关系/*1、手机号2、账号3、密码4、租户类型:shanju-merchant5、默认套餐:shanju-merchant6、租户名称,同账号名*/CreateTenantRequestDTO createTenantRequestDTO = new CreateTenantRequestDTO();createTenantRequestDTO.setMobile(merchantDTO.getMobile());//租户的账号信息createTenantRequestDTO.setUsername(merchantDTO.getUsername());createTenantRequestDTO.setPassword(merchantDTO.getPassword());//表示该租户类型是商户createTenantRequestDTO.setTenantTypeCode("shanju-merchant");//租户类型//设置租户套餐为初始化套餐餐createTenantRequestDTO.setBundleCode("shanju-merchant");//套餐,根据套餐进行分配权限//新增租户并设置为管理员createTenantRequestDTO.setName(merchantDTO.getUsername());//租户名称,和账号名一样log.info("商户中心调用统一账号服务,新增租户和账号");//如果租户在SaaS已经存在,SaaS直接 返回此租户的信息,否则进行添加TenantDTO tenantAndAccount = tenantService.createTenantAndAccount(createTenantRequestDTO);//获取租户的idif (tenantAndAccount == null || tenantAndAccount.getId() == null) {throw new BusinessException(CommonErrorCode.E_200012);}//租户的idLong tenantId = tenantAndAccount.getId();//租户id在商户表唯一//根据租户id从商户表查询,如果存在记录则不允许添加商户Integer count1 = merchantMapper.selectCount(new LambdaQueryWrapper<Merchant>().eq(Merchant::getTenantId, tenantId));if (count1 > 0) {throw new BusinessException(CommonErrorCode.E_200017);}
/*//判断租户下是否已经注册过商户Merchant merchantNew = merchantMapper.selectOne(new QueryWrapper<Merchant>().lambda().eq(Merchant::getTenantId, tenantAndAccount.getId()));if (merchantNew != null && merchantNew.getId() != null) {throw new BusinessException(CommonErrorCode.E_200017);}*///=======================================================================================//使用MapStruct进行对象转换Merchant merchant = MerchantConvert.INSTANCE.dto2entity(merchantDTO);//设置所对应的租户的Idmerchant.setTenantId(tenantId);//审核状态为0-未进行资质申请merchant.setAuditStatus("0");//审核状态 0‐未申请,1‐已申请待审核,2‐审核通过,3‐审核拒绝log.info("保存商户注册信息");//调用mapper向数据库写入记录merchantMapper.insert(merchant);//新增门店,创建根门店StoreDTO storeDTO = new StoreDTO();storeDTO.setStoreName("根门店");storeDTO.setMerchantId(merchant.getId());//商户idStoreDTO store = createStore(storeDTO);//新增员工,并设置归属门店StaffDTO staffDTO = new StaffDTO();staffDTO.setMobile(merchantDTO.getMobile());//手机号staffDTO.setUsername(merchantDTO.getUsername());//账号//为员工选择归属门店,此处为根门店staffDTO.setStoreId(store.getId());//员所属门店idstaffDTO.setMerchantId(merchant.getId());//商户idStaffDTO staff = createStaff(staffDTO);//为门店设置管理员bindStaffToStore(store.getId(), staff.getId());// ...//保存商户//merchantMapper.insert(merchant);//将dto中写入新增商户的id//将新增商户id返回//merchantDTO.setId(merchant.getId());//将entity转成dtoMerchantDTO merchantDTONew = MerchantConvert.INSTANCE.entity2dto(merchant);//返回商户注册信息return merchantDTONew;
}
测试
使用Postman进行商户注册,请求:http://localhost:57010/merchant/merchants/register
发送验证码:http://localhost:57010/merchant/sms?phone=15105100510,验证码在控制台输出。
未知错误:
发现控制台报错:
解决办法:在shanjupay-user-service工程的pom.xml中添加如下依赖:
<dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-jdk8</artifactId><version>1.2.0.Beta2</version>
</dependency>
<dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>1.2.0.Beta2</version>
</dependency>
clean shanjupay-user-service,并重新启动shanjupay-user-service工程,进行测试。
staff表:
注册成功后观察shanjupay_user数据库下的account、tenant、tenant_account表是否有新注册的账号、租户信息。
account表:
tenant表:
tenant_account表:
用户认证
SaaS平台提供统一认证的服务,本章节学习SaaS平台的认证功能。
基本概念
什么是认证
进入移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝、头条等,下边拿微信来举例子说明认证相关的基本概念,在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,输入账号和密码登录微信的过程就是认证。
系统为什么要认证?
认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。
认证
用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。
常见的用户身份认证方式有:
- 用户名密码登录
- 二维码登录
- 手机短信登录
- 指纹认证等方式
什么是会话
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。
基于session的认证方式如下图:
它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的sesssion_id 存放到 cookie 中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。
基于token方式如下图:
它的交互流程是,用户认证成功后,服务端生成一个token发给客户端,客户端存储token,每次请求时带上token,服务端收到token通过验证后即可确认用户身份。
两者区别
基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持cookie;
基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。
什么是授权
还拿微信来举例子,微信登录成功后用户即可使用微信的功能,比如,发红包、发朋友圈、添加好友等,没有绑定银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包,发红包功能、发朋友圈功能都是微信的资源即功能资源,用户拥有发红包功能的权限才可以正常使用发送红包功能,拥有发朋友圈功能的权限才可以使用发朋友圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。
为什么要授权?
认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,控制不同的用户能够访问不同的资源。
授权
授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。
分布式系统认证方案
分布式认证需求
统一认证授权
分布式系统的每个服务(系统)都会有认证、授权的需求,如果每个服务都实现一套认证授权逻辑会非常冗余,考虑分布式系统共享性的特点,需要由独立的认证服务来处理系统认证授权的请求;
如下图,闪聚支付平台包括:商户平台应用、运营平台应用、门户应用,每个应用都需要身份认证,闪聚支付平台统一由UAA认证服务完成认证。
1、前端请求UAA认证服务请求认证,认证通过获取 Token
2、前端携带Token访问各各应用。
开放认证体系
考虑分布式系统开放性的特点,UAA认证服务不仅服务于平台自身,并且对第三方系统也要提供认证,平台应提供扩展和开放的认证机制,以开放API的方式供第三方应用接入,一方应用(内部 系统服务)和三方应用(第三方应用)均采用统一机制接入。
下图是第三方的应用接入闪聚支付平台结构图:
1、第三方应用请求UAA认证服务请求认证,认证通过获取Token。
2、第三方应用携带Token访问API代理(专门针对第三方应用接入开发的微服务)。
3、API代理访问平台微服务向第三方应用返回业务数据。
分布式认证方案
选型分析
1、基于session的认证方式
在分布式的环境下,基于session的认证会出现一个问题,每个应用服务都需要在session中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用服务需要将session信息带过去,否则会重新认证。
这个时候,通常的做法有下面几种:
- Session复制:多台应用服务器之间同步session,使session保持一致,对外透明。
- Session黏贴:当用户访问集群中某台服务器后,强制指定后续所有请求均落到此机器上。
- Session集中存储:将Session存入分布式缓存中,所有服务器应用实例统一从分布式缓存中存取Session。
总体来讲,基于session认证的认证方式,可以更好的在服务端对会话进行控制,且安全性较高。但是,session机制方式基于cookie,在复杂多样的移动客户端上不能有效的使用,并且无法跨域,另外随着系统的扩展需提高session的复制、黏贴及存储的容错性。
2、基于token的认证方式
基于token的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把token 存在任意地方,并且可以实现web和app统一认证机制。其缺点也很明显,token由于自包含信息,因此一般数据量较大,而且每次请求都需要传递,因此比较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理负担。
技术方案
根据 选型的分析,决定采用基于token的认证方式,它的优点是:
1、适合统一认证的机制,客户端、一方应用、三方应用都遵循一致的认证机制。
2、token认证方式对第三方应用接入更适合,因为它更开放,使用当前有流行的开放协议Oauth2.0、JWT。
3、一般情况服务端无需存储会话信息,减轻了服务端的压力。
- 接入方(需要使用平台资源的统称为接入方)采取OAuth2.0方式请求统一认证服务(UAA)进行认证。
- 认证服务(UAA)调用统一账号服务去查询该用户信息及其权限信息。(第三方应用接入不需要该步骤)
- 认证服务(UAA)验证登录用户及第三方应用合法性。
- 若接入方身份合法,认证服务生成jwt令牌返回给接入方,其中jwt中包含了权限信息。
- 接入方携带jwt令牌对API网关内的微服务资源进行访问。
- API网关对令牌解析、并验证接入方的权限是否能够访问本次请求的微服务。
- 如果接入方的权限没问题,API网关将Token转发至微服务。
- 微服务收到请求,明文token中包含登录用户的身份和权限信息,后续微服务使用用户身份及权限信息。
流程所涉及到统一账号服务、UAA服务、API网关这三个组件职责如下
1)统一账号服务
提供商户和平台运营人员的登录账号、密码、角色、权限、资源等系统级信息的管理,不包含用户业务信息。
2)统一认证服务(UAA)
它承载了OAuth2.0接入方认证、登入用户的认证、授权以及生成令牌的职责,完成实际的用户认证、授权功能。
3)API网关
作为系统的唯一入口,API网关为接入方提供定制的API集合,它可能还具有其它职责,如身份验证、监控、负载均衡、缓存等。API网关方式的核心要点是,所有的接入方和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。
OAuth2.0
OAuth2.0介绍
OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。
Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。
参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin
Oauth协议:https://tools.ietf.org/html/rfc6749
下边分析一个Oauth2认证的例子,通过例子去理解OAuth2.0协议的认证流程,本例子是黑马程序员网站使用微信认证的过程,这个过程的简要描述如下:
用户借助微信认证登录黑马程序员网站,用户就不用单独在黑马程序员注册用户,怎么样算认证成功吗?黑马程序员网站需要成功从微信获取用户的身份信息则认为用户认证成功,那如何从微信获取用户的身份信息?用户信息的拥有者是用户本人,微信需要经过用户的同意方可为黑马程序员网站生成令牌,黑马程序员网站拿此令牌方可从微信获取用户的信息。
1、客户端请求第三方授权
用户进入黑马程序的登录页面,点击微信的图标以微信账号登录系统,用户是自己在微信里信息的资源拥有者。
点击“微信”出现一个二维码,此时用户扫描二维码,开始给黑马程序员授权。
2、资源拥有者同意给客户端授权
资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证, 验证通过后,微信会询问用户是否给授权黑马程序员访问自己的微信数据,用户点击“确认登录”表示同意授权,微信认证服务器会颁发一个授权码,并重定向到黑马程序员的网站。
3、客户端获取到授权码,请求认证服务器申请令牌
此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码。
4、认证服务器向客户端响应令牌
微信认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。此交互过程用户看不到,当客户端拿到令牌后,用户在黑马程序员看到已经登录成功。
5、客户端请求资源服务器的资源
客户端携带令牌访问资源服务器的资源。
黑马程序员网站携带令牌请求访问微信服务器获取用户的基本信息。
6、资源服务器返回受保护资源
资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。
以上认证授权详细的执行流程如下:
通过上边的例子我们大概了解了OAauth2.0的认证过程,下边我们看OAuth2.0认证流程:
引自OAauth2.0协议rfc6749 https://tools.ietf.org/html/rfc6749
OAauth2.0包括以下角色:
① 客户端
本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端(浏览器端)、微信客户端等。
② 资源拥有者
通常为用户,也可以是应用程序,即该资源的拥有者。
③ 授权服务器(也称认证服务器)
用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌(access_token),作为客户端访问资源服务器的凭据。本例为微信的认证服务器。
④ 资源服务器
存储资源的服务器,本例子为微信存储的用户信息。
现在还有一个问题,服务提供商能允许随便一个客户端就接入到它的授权服务器吗?答案是否定的,服务提供商会给准入的接入方一个身份,用于接入时的凭据:
client_id
:客户端标识 client_secret
:客户端秘钥
因此,准确来说,授权服务器对两种OAuth2.0中的两个角色进行认证授权,分别是资源拥有者、客户端。
四种模式
OAuth2.0提供了四种授权(获取令牌)方式,四种方式均采用不同的执行流程,让我们适应不同的场景。
授权码模式
授权码模式流程如下 :
(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息。如:
/uaa/oauth/authorize?client_id=p2pweb&response_type=code&scope=app&redirect_uri=http://xx.xx/notify
参数列表如下:
- client_id:客户端接入标识。
- response_type:授权码模式固定为code。
- scope:客户端权限。
- redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。
(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。
(3)授权服务器将授权码(AuthorizationCode)转经浏览器发送给client(通过redirect_uri)。
(4)客户端拿着授权码向授权服务器索要访问access_token,请求如下:
/uaa/oauth/token?client_id=p2pweb&client_secret=gdjbcd&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://xx.xx/notify
参数列表如下
- client_id:客户端准入标识。
- client_secret:客户端秘钥。
- grant_type:授权类型,填写authorization_code,表示授权码模式
- code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
- redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
(5)授权服务器返回令牌(access_token)
这种模式是四种模式中最安全的一种模式。一般用于Web服务器端应用或第三方的原生App调用资源服务的时候。因为在这种模式中access_token不会经过浏览器或移动端的App,而是直接从服务端去交换,这样就最大限度的减小了令牌泄漏的风险。
密码模式
密码模式使用较多,适应于第一方的单页面应用以及第一方的原生App,比如:闪聚支付平台运营平台用户使用此模式完成用户登录。
密码模式认证流程如下:
(1)资源拥有者将用户名、密码发送给客户端
(2)客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token),请求如下:
/uaa/oauth/token?client_id=p2pweb&client_secret=fgsdgrf&grant_type=password&username=shangsan&password=123456
参数列表如下:
- client_id:客户端准入标识。
- client_secret:客户端秘钥。
- grant_type:授权类型,填写password表示密码模式
- username:资源拥有者用户名。
- password:资源拥有者密码。
(3)授权服务器将令牌(access_token)发送给client
这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client,因此这就说明这种模式只能用于client是我们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用。
客户端模式
(1)客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)
(2)确认客户端身份无误后,将令牌(access_token)发送给client,请求如下:
/uaa/oauth/token?client_id=p2pweb&client_secret=fdafdag&grant_type=client_credentials
参数列表如下:
- client_id:客户端准入标识。
- client_secret:客户端秘钥。
- grant_type:授权类型,填写client_credentials表示客户端模式
这种模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任,而client本身也是安全的。因此这种模式一般用来提供给我们完全信任的服务器端服务。比如,合作方系统对接,拉取一组用户信息。
客户端模式适应于没有用户参与的,完全信任的一方或合作方服务器端程序接入。
简化模式
(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息。如:
/uaa/oauth/authorize?client_id=p2pweb&response_type=token&scope=app&redirect_uri=http://xx.xx/notify
参数描述同授权码模式 ,注意response_type=token,说明是简化模式。
(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。
(3)授权服务器将授权码将令牌(access_token)以Hash的形式存放在重定向uri的fargment中发送给浏览器。
注:fragment 主要是用来标识 URI 所标识资源里的某个资源,在 URI 的末尾通过 (#)作为 fragment 的开头,其中 # 不属于 fragment 的值。如 https://domain/index#L18 这个 URI 中 L18
就是 fragment 的值。大家只需要知道js通过响应浏览器地址栏变化的方式能获取到fragment 就行了。
一般来说,简化模式用于第三方单页面应用。
统一认证测试
认证接口说明
登录
功能说明: 用户登录并返回令牌,该令牌用于访问闪聚支付平台内受保护资源。
访问路径:[授权服务地址]/oauth/token
请求参数:
grant_type
: 授权类型,可以是authorization_code,implicit,client_credentials,passwordclient_id
:接入客户端idclient_secret
:接入客户端密钥username
:登录用户名,认证类型(如密码认证,短信认证,二维码认证等)
// 前提数据库表account有用户名:test02
{"username": "test02","authenticationType": "password"}
password
:登录密码
响应内容:
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiO...",//令牌"token_type": "bearer","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiO...",//刷新令牌"expires_in": 31535999,//有效期"scope": "read","jti": "83a2f72b-3132-4067-8b28-7a6474ad68c5"
}
解析令牌
功能说明:返回令牌的明文内容,描述的是当前登录的用户及接入客户端的信息。
访问路径:[授权服务地址]/oauth/check_token
请求参数:
token
: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiO…”
响应内容:
{"aud": ["shanju-resource"],"payload": {"11": {"resources": null,"user_authorities": {"r_001": ["sj_m_console","sj_m_app_list","sj_m_transaction_list","sj_m_account_check","sj_m_payment","sj_m_account_list","sj_m_enterprise_auth","sj_m_store_list","sj_m_staff_list","sj_o_member_list","sj_o_entreprise_list","sj_o_audit","sj_o_service_type","sj_o_account_check","sj_o_admin_list","sj_o_role_list","sj_m_app_create","sj_m_payment_set","sj_m_store_create","sj_m_store_query","sj_m_staff_create","sj_m_staff_query","sj_o_member_query","sj_o_enterprise_query","sj_o_enterprise_create","sj_o_service_create","sj_o_service_query","sj_o_admin_create","sj_o_admin_query","sj_o_role_create","sj_o_role_query","sj_o_role_save","sj_m_auth_apply","sj_m_console_renew","sj_m_console_upgrade","sj_m_app_save","sj_m_app_modify","sj_m_payparam_save","sj_m_pay_set","sj_m_pay_save","sj_m_c2b_qrcode","sj_m_b2c_order","sj_m_h5_view","sj_m_bundle_buy","sj_m_enterprise_info_submit","sj_m_enterprise_info_cancel","sj_o_enterprise_auth_pass","sj_o_enterprise_auth_rejection","sj_m_store_edit","sj_m_staff_edit","sj_o_admin_edit","sj_o_role_edit","sj_m_store_save","sj_m_store_del","sj_m_staff_save"],"r_002": ["sj_m_transaction_list","sj_m_account_check"]}}},"user_name": "test02","scope": ["read"],"mobile": "15105100510","exp": 1628014825,"client_authorities": ["ROLE_MERCHANT","ROLE_USER"],"jti": "83a2f72b-3132-4067-8b28-7a6474ad68c5","client_id": "merchant-platform"
}
接口测试
分别启动 shanjupay-gateway
、shanjupay-user
、shanjupay-uaa
三个工程,测试过程若有不清楚可参考前面1.2.3章节OAuth2.0理论部分和上一小节接口说明。
密码模式认证
POST http://localhost:56020/uaa/oauth/token
请求参数:
数据库表:oauth_client_details
解析token
http://localhost:56020/uaa/oauth/check_token
JWT
什么是JWT?
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。
JWT令牌的优点
- jwt基于json,非常方便解析。
- 可以在令牌中自定义丰富的内容,易扩展。
- 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
JWT令牌结构
JWT令牌由Header、Payload、Signature三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
1、Header
头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)。
一个例子:
{"alg": "HS256","typ": "JWT"
}
将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
HS256就是HMAC-SHA256,加密算法使用HMAC,摘要算法使用SHA256。
测试:
将生成的jwt令牌第一部分使用Base64还原原始内容如下:
public static void main(String[] args) {//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 令牌的头信息指令加密算法byte[] header = java.util.Base64.getDecoder().decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9");System.out.println(new String(header));
}
得到的结果是:
{"alg": "HS256","typ": "JWT"
}
2、Payload
第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比 如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。 此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。 一个例子:
{"sub": "1234567890","name": "456","admin": true
}
最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。
3、Signature
第三部分是签名,此部分用于防止jwt内容被篡改。 这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明 签名算法进行签名。 一个例子:
HMACSHA256(base64UrlEncode(header) +"."+ base64UrlEncode(payload),secret)
base64UrlEncode(header):jwt令牌的第一部分。 base64UrlEncode(payload):jwt令牌的第二部分。 secret:签名所使用的密钥。
JWT三个部分只有第三部分是加密的,通过数字签名机制,我们既可以保证数据完整性,也可以对数据来源进行身份验证。
什么是签名?
签名是数字签名,发送方将消息原文使用摘要算法生成摘要,再用私钥对摘要进行加密,生成数字签名。
传输数据时为了保证数据的完整性可以使用数字签名技术:
- 发送方使用私钥对内容进行数字签名
- 将内容附带数字签名发送给对方
- 对方收到内容和数字签名,使用公钥进行验签(相当于解密的过程),如果发现内容不一致则说明传输过程被篡改。