如何将应用接入Hiper统一登录平台

统一登录平台(CAS)包含了统一登录与用户管理、应用授权管理两块的功能,本文也会从这两块分别介绍如何接入CAS。

注册APP

接入前需要在CAS注册APP, 补充应用编号(后文以APP_CODE作为缩写)、名称、产品号、应用URL、应用版本等信息。
注册APP配置示例
对于统一登录来说最重要的是APP_CODE 和 应用URL这两个参数:
  • APP_CODE 是应用标识,不可更改,后续接入的API中会到这个参数; 
  • URL 对应系统访问地址,从控制台点击对应APP卡片或者登录后跳转的地方。
对于授权管理主要会用到APP_CODE、应用版本号这两个参数,  应用版本号对应应用的具体版本,不同版本所含的服务列表可能不一样。

统一登录与用户管理

登录基本流程及部署架构

CAS登陆流程说明
典型的两个登陆流程:
  1. 用户CAS登录之后,从CAS带Token跳转到APP系统就行登录
  2. 或者用户直接访问APP系统,但是未携带Token,此时重定向到CAS进行登录之后跳回应用。
总之如果没有有效的token,会登陆之后带Token访问APP系统的前端,如:http://10.10.32.32:9030/?castoken=eyJ0IjoiNGFlZmZhMGE0ZDI2NDcwOGE3NWEyYzcxYTAyYzU0ZmQiLCJ1IjoiMSJ9
带token到APP前端后,前端存储token并在后续请求中带token访问应用,后端收到请求之后会拦截并做校验。
Token校验流程:
  1. 判断token是否有效(reids)
  2. 按需延长token有效期 (http接口)
  3. 获取token对应用户信息(redis)
CAS的关键接口如Token校验、获取Token对应的用户信息是通过Redis获取的,其他非关键信息是直接通过HTTP接口提供的,目的是为了提高系统的可用性,即使CAS挂了,不影响已登陆的用户继续使用。
为了进一步提高系统可用性,cas的token校验、用户信息获取还增加了本地缓存,在redis挂的时候会fallback到本地缓存继续服务(这个功能默认没有开启,需要手工打开)
用户信息后续的传递流程
  1. cas默认的filter会把用户信息放入http header,然后继续向下透传
  2. 下层应用可以直接从filter获取用户信息  虽然也可以通过token调用cas的服务、redis获取用户信息,不过推荐从header直接获取,这种方式的性能和可用性都更好。
整体部署架构如下图所示:
CAS整体部署架构
基本的登录流程到这里就基本说清楚了,不过在应用接入的过程中遇到的一个问题是用户信息怎么同步。
对于简单的应用来说,可以不维护自己的用户表,直接走CAS的接口即可,不过对于MES、IOT这种复杂的应用来说这么做可能不够, 比如因为要维护用户组织、权限等功能,用户表和其他表之间需要做连接查询,这种情况冗余用户表实现成本会小一些。
因此会涉及用户的同步,目前覆盖以下两个“同步时机”就可以满足需要:
  1. 用户登录时,这个时候可以检查cas的用户是否在APP中存在及信息是否一致,不一致则同步。
  2. 查看用户列表时,主动或被动同,只在用户登陆的时候做同步可能不够,管理员把用户分配给APP之后,可能马上就进入APP内部做一些权限、工单分配之类的操作,此时用户尚未登陆app,管理员会发现再APP中无法看到这个用户。因此需要在APP中增加主动或被动的用户同步功能。

统一登录后端CAS Filter 接入方案

在应用中引入cas-sso-filter
<dependency> <groupId>com.hvisions</groupId> <artifactId>cas-sso-client-starter</artifactId> <version>2.2.1-SNAPSHOT</version> </dependency>
客户端引入后,会自动配置相关filter:
应用配置的filter功能
Spring Cloud GateWaycom.hvisions.cas.client.sso.filter.CasSpringCloudGateWayAuthFilter登录验证、token刷新、登出、及用户信息透传
Netflix Zuul GateWaycom.hvisions.cas.client.sso.filter.CasZuulGateWayAuthFilter登录验证、token刷新、登出、及用户信息透传zuulgateway 需要手工启用cas login相关feigin client@EnableFeignClients(basePackages = {"com.hvisions.cas.client" })
普通spirng boot应用com.hvisions.cas.client.sso.filter.SsoUserFilter当前登录用户信息获取。
相关配置:
参数含义其他
h-visions.appCode接入cas后的appCode,sso接入和授权都会用到。必需
h-visions.sso.exclusionPatterns在gateway校验时跳过的filter默认值:/actuator,/v2/api-docs,/swagger-ui
h-visions.sso.authFilter.ordergateway中配置的filter的order默认值:10
h-visions.sso.userFilter.order除gateway之外应用中配置的filter的order默认值:100
h-visions.sso.redis.*sso所需redis集群信息,支持和spring.reids.* 一样的参数必需
h-visions.sso.enabled是否开启sso登录功能,true/false默认值:true
注意调整下gateway中的cors filter的优先级,需要先于 sso.authFilter 执行,避免authFilter校验不通过导致的跨域错误, 同时Cors Filter需要设置下ExposedHeaders, 允许'HVISIONS-LICENSE-STATUS'授权校验状态透传(授权管理会用到,后续给出详细说明,先写在这里了)。
应用配置参考(以IOT为例)
h-visions: appCode: hiper-matrix-xxx sso: exclusionPatterns: "/cas, /actuator,/v2/api-docs,/swagger-ui,/webjars,/swagger-ui.html,/swagger-resources" redis: host: 10.10.32.34 port: 6379
在非gateway应用获取当前用户使用 com.hvisions.cas.client.sso.SSOUtil#getCurrentUser即可获取到当前用户信息,用户信息目前包括:
// 用户id private Long id; // 创建时间 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date gmtCreate; // 账号 private String userAccount; // 昵称 private String userNick; // 状态 private Byte status; // 邮箱 private String email; // 手机 private String mobilePhone; // private String dingTalkId; // 钉钉unionId 钉钉三方登录使用 private String dingTalkUnionId; // 最近一次登录时间 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date gmtLastLogin; // 是否是appAdmin private boolean appAdmin; // 扩展字段 private Map<String, String> extAttrs;
主要就是登录相关字段,后续可能会扩展或者调整。

统一登录登录相关API

这一部分会展开介绍CAS Filter接入方案中的实现用到的类及相关API,如果Cas提供的Filter无法满足需求,APP可以基于此定制。

SSOAuthManager

SSO登录验证管理的核心类,封装了对Redis的访问和实现了本地缓存功能,对外提供根据token校验和根据token获取用户的API。

Token校验

从token解析用户id,然后通过authManager.getSessionInfo获取session,然后还要对sessionInfo.getExpiredTs做校验。
参考:
Long userId = LoginUtil.parseUserId(token); if (userId == null) { return buildErrResult(exchange, HttpStatus.UNAUTHORIZED, "Token错误"); } // 检查登录状态 SessionInfo sessionInfo = authManager.getSessionInfo(userId, token); if (sessionInfo == null || sessionInfo.getExpiredTs() < System.currentTimeMillis()) { return buildErrResult(exchange, HttpStatus.UNAUTHORIZED, "Token错误或登录过期"); }

获取token对应用户

通过userId、appCode获取当前用户信息
AppUserDTO appUserDTO = authManager.getAppUserDTO(userId, appCode); if (appUserDTO == null) { return buildErrResult(exchange, HttpStatus.FORBIDDEN, "没有权限或无法找到用户"); }

缓存功能

缓存功能默认不开启,不过推荐开启,开启之后在redis挂的情况依然可以部分提供服务(看缓存大小和token有效期)。
h-visions: appCode: hiper-matrix sso: exclusionPatterns: "/cas, /actuator,/v2/api-docs,/swagger-ui,/webjars,/swagger-ui.html,/swagger-resources" redis: host: 10.10.32.34 port: 6379 circuit-breaker: enabled: true sessionCacheSize: 500 userCacheSize: 200 userCacheExpiredTimeInMinutes: 720 failureRateThreshold: 10
增加circuit-breaker相关配置即可
配置项含义备注
h-visions.sso.circuit-breaker.enabled是否开启默认值false
h-visions.sso.circuit-breaker.sessionCacheSizesessionInfo size大小默认值500
h-visions.sso.circuit-breaker.userCacheSizeuserCache大小默认值200
h-visions.sso.circuit-breaker.userCacheExpiredTimeInMinutes用户缓存过期时间,单位分钟默认值720分钟
h-visions.sso.circuit-breaker.failureRateThreshold短路保护器开关失败率阈值失败率超过多少则进入断路保护器模式,具体参考https://resilience4j.readme.io/docs/circuitbreaker
sessionCache的过期时间默认为sesssion的expiredTs,因为一个人可能同时登录多次,建议大小和userCacheSize大小成一定比例,比如2~3。

Token刷新

 cas注册到了一个应用的nacos,提供了rpc接口可供调用,并提供了feignClient接口。
Webclient调用例子
private Mono<Void> tryRefreshToken(String token, Long expiredTs) { // 为开启自动刷新 if (!refreshTokenConfig.getAutoRenewToken()) { return Mono.empty(); } // 可以延长的时间太少了,忽略这次调用 long now = System.currentTimeMillis(); long newExpiredTs = now + TimeUnit.MINUTES.toMillis(refreshTokenConfig.getRenewTime()); if (newExpiredTs - expiredTs < TimeUnit.MINUTES.toMillis(refreshTokenConfig.getRenewInterval())) { return Mono.empty(); } return webClient .get() .uri(uriBuilder -> uriBuilder.path("/rpc/login/refreshToken") .queryParam("appCode", appCode) .queryParam("token", token) .build()) .retrieve() .toEntity(new ParameterizedTypeReference<ResultVO<Long>>() { }) .flatMap(refreshResponseEntity -> { if (refreshResponseEntity.getStatusCode() != HttpStatus.OK) { // 只打warning错误日志, token刷新失败不影响后续执行,直到token真正的过期才会导致重新登录 log.warn("token refresh failed, token:{} expiredTs:{} statusCode:{}", token, expiredTs, refreshResponseEntity.getStatusCode()); } return Mono.empty(); }); }
WebClient初始化参考
@Bean @LoadBalanced public WebClient.Builder casLoadBalancedWebClientBuilder() { return WebClient.builder(); } @Bean CasSpringCloudGateWayAuthFilter casGateWayAuthFilter(WebClient.Builder casLoadBalancedWebClientBuilder, CasRedisProperties casRedisProperties, SSOCircuitBreakerConfig circuitBreakerConfig) { SSOAuthManager ssoAuthManager = new SSOAuthManager(casRedisProperties, circuitBreakerConfig); return new CasSpringCloudGateWayAuthFilter(appCode, exclusionPatterns, forbiddenPatterns, order, casLoadBalancedWebClientBuilder.baseUrl(CasConstants.SSO_APP_BASE_URL).build(), ssoAuthManager); }
FeignClient接口参考:com.hvisions.cas.client.LoginClient#refreshToken

Logout

RPC接口登出例子
private Mono<Void> doLogout(String token) { return webClient .get() .uri(uriBuilder -> uriBuilder.path("/rpc/login/logout") .queryParam("appCode", appCode) .queryParam("token", token) .build()) .retrieve() .toEntity(Void.class) .flatMap( voidResponseEntity -> { if (HttpStatus.OK == voidResponseEntity.getStatusCode()) { return Mono.empty(); } else { return Mono.error(BAD_REQUEST); } }); }
FeignClient接口参考:com.hvisions.cas.client.LoginClient#logout
注:因为cas会直接注册到应用nacos,前端也可直接调用http://gateway/cas/auth/logout 接口。

统一登录前端接入

如前文“典型登陆流程”部分所说,登录相关主要是处理登录token。

获取token

当用户访问APP前端页面时候,前端发现没有活着token失效,需要跳转到cas获取token,同时把当前页面的地址设置到redirect_url参数,从cas获取token后(登录或者cas前端缓存有有效token),会带token跳回APP。为了按校验跳转地址的合法性,同时需要带上appCode参数。
如访问IOT开发环境时,假如没有有效token,IOT前端会跳转到CAS开发环境:
http://10.10.32.34:8060/login?redirect_url=http://10.10.32.32:9030&appCode=hiper-matrix
获取有效token后,会带上casToken跳回IOT页面:
http://10.10.32.32:9030/?castoken=eyJ0IjoiZDY4MTM1ZmE4ZTg4NDNkZDhkZDE3MGVmY2U2YzUzMDkiLCJ1IjoiMSJ9

后续带Token访问后端

Token通过header “token”传递, 如:
curl 'http://10.10.32.32:9030/api/edge-management/connection/edge?timestamp=1703489412687' \ -H 'token: eyJ0IjoiZDY4MTM1ZmE4ZTg4NDNkZDhkZDE3MGVmY2U2YzUzMDkiLCJ1IjoiMSJ9'
后端校验到token失效或者无APP访问权限时候,会返回错误,并设置Htpp状态码401或403,如:
➜ ~ curl -v 'http://10.10.32.32:9030/api/auth/module/getModuleListWithModuleButtonListByUserId/1977' \ -H 'token: eyJ0IjoiZDY4MTM1ZmE4ZTg4NDNkZDhkZDE3MGVmY2U2YzUzMDkiLCJ1IjoiMSJ9' * Trying 10.10.32.32:9030... * Connected to 10.10.32.32 (10.10.32.32) port 9030 (#0) > GET /api/auth/module/getModuleListWithModuleButtonListByUserId/1977 HTTP/1.1 > Host: 10.10.32.32:9030 > User-Agent: curl/7.87.0 > Accept: */* > token: eyJ0IjoiZDY4MTM1ZmE4ZTg4NDNkZDhkZDE3MGVmY2U2YzUzMDkiLCJ1IjoiMSJ9 > * Mark bundle as not supporting multiuse < HTTP/1.1 401 < Server: nginx/1.19.2 < Date: Mon, 25 Dec 2023 07:41:29 GMT < Content-Type: application/json;charset=utf-8 < Transfer-Encoding: chunked < Connection: keep-alive < Vary: Origin < Vary: Access-Control-Request-Method < Vary: Access-Control-Request-Headers < {"code":401,"message":"Token错误或登录过期"}

登出

以直接调用cas接口为例给出说明。
curl 'http://10.10.32.32:9030/api/cas/auth/logout?timestamp=1703490143509' \ -X 'POST' \ -H 'Accept: application/json, text/plain, */*' \ -H 'token: eyJ0IjoiM2IzNTAzMmY1Njc0NDEwMWFmMWE0ZThjYjZlMWU5YTciLCJ1IjoiMSJ9'

其他

获取APP配置

根据appCode获取应用配置 logo、主题、多语言开关配置。
curl 'http://10.10.32.32:9030/api/cas/sysConfig/getAppConfig?appCode=hiper-matrix' \ -H 'token: eyJ0IjoiNmI4MWQ4YzNmNGM4NDY2MmJhNjQzMzg0ZmVkMjc0MzUiLCJ1IjoiMSJ9' \ { "data": { "appCode": "hiper-matrix", "logo": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzQiIGhlaWdodD0iNzQiIHZpZXdCb3g9IjAgMCA3NCA3NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzFfMTcyKSI+CjxwYXRoIGQ9Ik00Ny4zNCAxMS4wNkwzNi44NCAxNy4xMlY1TDkuMDMgMjEuMDVWNTMuMTZMMzYuODEgMzcuMTFMNjQuNjQgNTMuMTZWMjEuMDVMNDcuMzQgMTEuMDZaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfMV8xNzIpIi8+CjxwYXRoIGQ9Ik0zNi44MiA1Ny4yMlY2OS4yMUw2NC42NCA1My4xNkwzNi44MSAzNy4xMUw5IDUzLjE2TDI2LjQyIDYzLjIyTDM2LjgyIDU3LjIyWiIgZmlsbD0idXJsKCNwYWludDFfbGluZWFyXzFfMTcyKSIvPgo8cGF0aCBkPSJNNTcgMTdIMTdWNTdINTdWMTdaIiBmaWxsPSIjMUYxRjFGIi8+CjxwYXRoIGQ9Ik00MS4xNTAzIDUxLjQ0ODZDNDEuMTUwMyA1MC42MTc0IDQwLjQ3NjUgNDkuOTQzNiAzOS42NDUzIDQ5Ljk0MzZDMzguODE0MSA0OS45NDM2IDM4LjE0MDMgNTAuNjE3NCAzOC4xNDAzIDUxLjQ0ODZDMzguMTQwMyA1Mi4yNzk4IDM4LjgxNDEgNTIuOTUzNiAzOS42NDUzIDUyLjk1MzZDNDAuNDc2NSA1Mi45NTM2IDQxLjE1MDMgNTIuMjc5OCA0MS4xNTAzIDUxLjQ0ODZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNDcuMTYwMyA1MS40NDg2QzQ3LjE2MDMgNTAuNjE3NCA0Ni40ODY1IDQ5Ljk0MzYgNDUuNjU1MyA0OS45NDM2QzQ0LjgyNDEgNDkuOTQzNiA0NC4xNTAzIDUwLjYxNzQgNDQuMTUwMyA1MS40NDg2QzQ0LjE1MDMgNTIuMjc5OCA0NC44MjQxIDUyLjk1MzYgNDUuNjU1MyA1Mi45NTM2QzQ2LjQ4NjUgNTIuOTUzNiA0Ny4xNjAzIDUyLjI3OTggNDcuMTYwMyA1MS40NDg2WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTUzLjE3MDMgNTEuNDQ4NkM1My4xNzAzIDUwLjYxNzQgNTIuNDk2NSA0OS45NDM2IDUxLjY2NTMgNDkuOTQzNkM1MC44MzQxIDQ5Ljk0MzYgNTAuMTYwMyA1MC42MTc0IDUwLjE2MDMgNTEuNDQ4NkM1MC4xNjAzIDUyLjI3OTggNTAuODM0MSA1Mi45NTM2IDUxLjY2NTMgNTIuOTUzNkM1Mi40OTY1IDUyLjk1MzYgNTMuMTcwMyA1Mi4yNzk4IDUzLjE3MDMgNTEuNDQ4NloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik00My4wMSAyMS4wNkgzMy40NlYyMi45N0gzNy4yOFYzMy43OUgzOS4yVjIyLjk3SDQzLjAxVjIxLjA2WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTIyLjU3IDIxLjA1SDIwLjY2VjMzLjc5SDIyLjU3VjIxLjA1WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTI5LjAyIDIxLjA2SDI3Ljc1QzI1Ljk5IDIxLjA2IDI0LjU3IDIyLjQ4IDI0LjU3IDI0LjI0VjMwLjYxQzI0LjU3IDMyLjM3IDI1Ljk5IDMzLjc5IDI3Ljc1IDMzLjc5SDI5LjAyQzMwLjc4IDMzLjc5IDMyLjIgMzIuMzYgMzIuMiAzMC42MVYyNC4yNEMzMi4yIDIyLjQ4IDMwLjc4IDIxLjA2IDI5LjAyIDIxLjA2Wk0zMC4yOSAzMC42MUMzMC4yOSAzMS4zMSAyOS43MyAzMS44NyAyOS4wMyAzMS44N0gyNy43M0MyNy4wMyAzMS44NyAyNi40NyAzMS4zMSAyNi40NyAzMC42MVYyNC4yMUMyNi40NyAyMy41MSAyNy4wNSAyMi45NiAyNy43NCAyMi45N0gyOS4wMUMyOS43MSAyMi45NyAzMC4yOCAyMy41MiAzMC4yOCAyNC4yMVYzMC42MUgzMC4yOVoiIGZpbGw9IndoaXRlIi8+CjwvZz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl8xXzE3MiIgeDE9IjExLjMyIiB5MT0iNTQuMDMiIHgyPSI1Ny4yNyIgeTI9IjIzLjU2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiM2RjQ3RkQiLz4KPHN0b3Agb2Zmc2V0PSIwLjE4IiBzdG9wLWNvbG9yPSIjODI0OUZCIi8+CjxzdG9wIG9mZnNldD0iMC41MSIgc3RvcC1jb2xvcj0iI0ExNENGQSIvPgo8c3RvcCBvZmZzZXQ9IjAuOCIgc3RvcC1jb2xvcj0iI0I0NEVGOSIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNCQjRGRjkiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDFfbGluZWFyXzFfMTcyIiB4MT0iMzQuNTkiIHkxPSI0NC4wNiIgeDI9IjM5LjMxIiB5Mj0iNjMuMjkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzZGNDdGRCIvPgo8c3RvcCBvZmZzZXQ9IjAuMTgiIHN0b3AtY29sb3I9IiM4MjQ5RkIiLz4KPHN0b3Agb2Zmc2V0PSIwLjUxIiBzdG9wLWNvbG9yPSIjQTE0Q0ZBIi8+CjxzdG9wIG9mZnNldD0iMC44IiBzdG9wLWNvbG9yPSIjQjQ0RUY5Ii8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0JCNEZGOSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzFfMTcyIj4KPHJlY3Qgd2lkdGg9IjU1LjY0IiBoZWlnaHQ9IjY0LjIxIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOSA1KSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=", "submenuLogo": "data:image/png;base64,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", "localConfigs": { "locale_switch": "true" }, "themeConfigs": { "@input-hover-border-color": "#24A1C8", "@primary-color": "#24A1C8", "@ne-layout-menu-logo-color": "#009bbf", "@ne-primary-bg-color": "rgba(36,161,200,0.15)", "@link-color": "#24A1C8", "@ne-layout-secondary-menu-bg-color": "rgba(36,161,200,0.15)", "@ne-layout-header-background": "#def1f7" } }, "code": 200, "message": null }

用户管理相关API

提供了获取APP用户列表和修改用户信息的API,不做展开描述,具体可以参考cas的swagger-ui。

其他

如何把CAS注册到应用的naocs

在cas的配置文件中增加如下配置,然后重启cas生效,可以按指定ip把cas注册到对应nacos的namespace。
h-visions: sso: discoveryNacos: - server-addr: 10.10.32.32:8848 namespace: dev ip: 10.10.32.34 port: 9015 - server-addr: 192.168.13.14:8849 namespace: dev ip: 192.168.10.13 port: 9015 - server-addr: 192.168.13.14:8849 namespace: test ip: 192.168.10.13 port: 9015

CasToken格式

base64之后的json字符串,字符串包含userId和uuid。
生成token的具体方法:com.hvisions.cas.client.utils.LoginUtil#generateToken
public static String generateToken(Long userId) { TokenInfo tokenInfo = new TokenInfo(); tokenInfo.setT(UUID.randomUUID().toString().replace("-", "")); tokenInfo.setU(String.valueOf(userId)); return new String(Base64.getUrlEncoder().withoutPadding().encode(Json.toBytes(tokenInfo))); }
com.hvisions.cas.client.utils.LoginUtil 提供了从解析userId的工具方法,有需要的可以参考。

授权管理

授权管理基本流程

授权管理包括授权激活、迁出及验证等功能。
下图是一个部署结构图:
授权管理部署结构图
其中有两个关键流程:
  1. 用户(管理员)对APP进行激活、授权迁出等操作  通过授权码从license-remote换回激活码,对应用进行激活,涉及到以下几个步骤:根据授权码结合本地机器信息生成请求码把请求码发送到服务器,服务器对请求码中的授权码、产品版本信息进行校验,通过之后发回激活码从激活码解析出产品信息,加密存储在本地授权文件中  如果是在线激活,这个流程是通过后台API完成的,在用户看来就是根据授权码进行激活;若是走离线激活,操作的过程和上述的步骤就是一一对应的。
  2. 根据授权码结合本地机器信息生成请求码
  3. 把请求码发送到服务器,服务器对请求码中的授权码、产品版本信息进行校验,通过之后发回激活码
  4. 从激活码解析出产品信息,加密存储在本地授权文件中
  5. APP部署之后按APP_CODE对服务进行授权校验
这个是通过license-plugin完成的,会连接license-local进行校验,大致的过程:
  • 使用应用appCode、服务名称和 一个uuid(当前应用的标识) 构造一个参数连接license-local,问它授权是不是有效;
  • license-local收到请求之后会对appCode、serviceCode、授权数量、授权有效期 进行校验然后返回校验结果。
  • license-plugin收到返回会针对校验结果做处理并返回Hvisions-License-Status状态码。

授权管理后端接入

License-local-plugin

java客户端:
<dependency> <groupId>com.hvisions</groupId> <artifactId>hvisions-license-plugin</artifactId> <version>1.0.6-SNAPSHOT</version> </dependency>
目前仅支持springboot应用,会基于springbootautoconfig做自动配置。相关参数
参数含义说明
h-visions.appCode应用code必须,在cas上的appCode
h-visions.license.local.serverlicense-local server地址必须,localserver地址
spring.application.nameservice名称必须,按service名称验证是否包含对服务的授权
h-visions.license.local.checkIntervals验证间隔非必需,默认值60s,最大180s,间隔时间的长短影响license-local清理失效节点的时机, 间隔越大授权被占用导致的服务不可用时间越大
hvisions.license.local.server(旧)license-local server地址兼容性参数,和h-visions.license.local.server保留其一即可
hvisions.license.local.checkIntervals(旧)验证间隔兼容性参数,和h-visions.license.local.checkIntervals保留其一即可

授权校验状态

license-plugin 会根据校验结果影响返回的HTTP数据,通过Http Header Hvisions-License-Status返回。
Hvisions-License-Status可能取值:
含义备注
OK正常
EXPIRING_SOON授权即将过期15天之后即将过期,临时会遇到, 不影响请求正常返回数据
INVALID过期或没有授权http状态码为601, 并且无数据返回
FAILED_CONN_TO_LICENSE_LOCAL本地授权服务器连接失败不影响请求正常返回数据
INVALID_FAILED_CONN_TO_LICENSE_LOCAL无效超过7天连不上LicenseLocalhttp状态码为601, 并且无数据返回
状态例子:
授权状态透出例子

跨域透传HVISIONS-LICENSE-STATUS 配置

允许'HVISIONS-LICENSE-STATUS'授权校验状态跨域透传一般配置在Gateway中,比如(IOT中的配置):
@Bean public FilterRegistrationBean<CorsFilter> corsFilter() { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); final CorsConfiguration config = new CorsConfiguration(); // 允许cookies跨域 config.setAllowCredentials(gatewayConfiguration.isAllowCredentials()); // 允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin config.setAllowedOrigins(gatewayConfiguration.getAllowedOrigin()); // #允许访问的头信息,*表示全部 config.setAllowedHeaders(gatewayConfiguration.getAllowedHeader()); // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了 config.setMaxAge(gatewayConfiguration.getMaxAge()); // 允许提交请求的方法,*表示全部允许 config.setAllowedMethods(gatewayConfiguration.getAllowedMethod()); // 允许透传HVISIONS-LICENSE-STATUS Header config.setExposedHeaders(Collections.singletonList("HVISIONS-LICENSE-STATUS")); source.registerCorsConfiguration(gatewayConfiguration.getCorsPath(), config); FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source)); //设置执行顺序,数字越小越先执行 bean.setOrder(0); return bean;

授权管理前端接入

前端需要对返回授权状态做处理
含义处理方式
OK正常无需处理
EXPIRING_SOON授权即将过期提示授权即将过期;每人每天提醒一次
INVALID过期或没有授权提醒用户授权无效
FAILED_CONN_TO_LICENSE_LOCAL本地授权服务器连接失败提示用户无法连接本地授权服务,请联系管理员处理;每人每天提醒一次。
INVALID_FAILED_CONN_TO_LICENSE_LOCAL无效超过7天连不上LicenseLocal提示用户长时间无法连接本地授权服务,请联系管理员处理;跳转到cas home页
2024-11-08
0