订阅和处理用户信息变更
概述
通过订阅用户信息变更,您可以接收有关用户及其账户的重要更新。当用户及其账户信息发生变更时,华为账号服务器会发送通知到应用服务端,应用服务端可以根据通知消息进行自身业务处理。
用户信息变更事件介绍
| 消息名称 | 事件类型 | 事件描述 |
|---|---|---|
| tokens-revoked | https://schemas.openid.net/secevent/oauth/event-type/tokens-revoked | 用户取消应用的授权 |
| account-purged | https://schemas.openid.net/secevent/risc/event-type/account-purged | 用户注销华为账号 |
| phone-modified | https://schemas.openid.net/secevent/oauth/event-type/phone-modified | 用户授权手机号变更 |
订阅用户信息变更
订阅步骤如下:
-
登录华为开发者联盟,选择“管理中心 > API服务 > API库”。
-
选择项目,然后在API名称搜索框检索关键字“RISC”,找到RISC点击进入详情。

-
点击启用按钮。

-
点击订阅通知按钮,在弹窗中配置回调地址及订阅范围。

回调地址:在开启订阅通知后,若华为用户信息发生变更,会发送通知消息到该地址。
订阅范围:订阅的用户信息变更事件,详见用户信息变更事件介绍。
处理通知消息
华为账号服务器向开发者应用服务端投递消息。开发者应用服务端接收到消息后需要先对消息头中的令牌进行验签,确保消息的完整有效性后解析并获取用户信息变更事件详情。具体步骤如下:
-
验证消息头中的令牌签名。
您可通过任何JWT库(例如:jwt.io)对其进行解析与验证。
无论使用哪种库,您均需完成如下操作:
- 调用接口(
https://risc.cloud.huawei.com/v1beta/public/risc/.well-known/risc-configuration),获取发行者标识(issuer)与签名密钥证书URI(jwks\_uri)。 - 通过依赖的JWT库,对消息头中的令牌进行解析,获取签名的KeyId。
- 通过签名的KeyId,从签名密钥证书URI中获取到JWT签名的公钥。
- 校验JWT签名中的aud与订阅用户信息变更中提供的Client ID一致。
- 校验JWT签名中的issuer与发行者标识(issuer)一致。
具体验签逻辑,请参考如下示例代码:
Maven依赖配置
<dependencies><dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>2.9.3</version> <!--此处替换为您项目需要的版本--></dependency><dependency><groupId>com.auth0</groupId><artifactId>jwks-rsa</artifactId><version>0.21.2</version> <!--此处替换为您项目需要的版本--></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version> <!--此处替换为您项目需要的版本--></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version> <!--此处替换为您项目需要的版本--></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.51</version> <!--此处替换为您项目需要的版本--></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.6</version> <!--此处替换为您项目需要的版本--></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.26</version> <!--此处替换为您项目需要的版本--></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.11</version> <!--此处替换为您项目需要的版本--></dependency></dependencies>Java验签代码示例
import com.alibaba.fastjson2.JSON;import com.alibaba.fastjson2.JSONObject;import com.auth0.jwk.JwkProvider;import com.auth0.jwk.UrlJwkProvider;import com.github.benmanes.caffeine.cache.CacheLoader;import com.github.benmanes.caffeine.cache.Caffeine;import com.github.benmanes.caffeine.cache.LoadingCache;import io.jsonwebtoken.Claims;import io.jsonwebtoken.IncorrectClaimException;import io.jsonwebtoken.JwsHeader;import io.jsonwebtoken.Jwt;import io.jsonwebtoken.JwtParser;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SigningKeyResolver;import io.jsonwebtoken.security.SignatureException;import lombok.Data;import lombok.extern.slf4j.Slf4j;import org.apache.http.HttpEntity;import org.apache.http.HttpStatus;import org.apache.http.client.config.RequestConfig;import org.apache.http.client.methods.CloseableHttpResponse;import org.apache.http.client.methods.HttpGet;import org.apache.http.config.Registry;import org.apache.http.config.RegistryBuilder;import org.apache.http.conn.socket.ConnectionSocketFactory;import org.apache.http.conn.socket.PlainConnectionSocketFactory;import org.apache.http.conn.ssl.SSLConnectionSocketFactory;import org.apache.http.impl.client.CloseableHttpClient;import org.apache.http.impl.client.HttpClients;import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;import org.apache.http.util.EntityUtils;import org.checkerframework.checker.nullness.qual.NonNull;import org.checkerframework.checker.nullness.qual.Nullable;import javax.net.ssl.SSLContext;import javax.net.ssl.TrustManagerFactory;import java.io.IOException;import java.net.URL;import java.security.Key;import java.security.KeyManagementException;import java.security.KeyStore;import java.security.KeyStoreException;import java.security.NoSuchAlgorithmException;import java.security.PublicKey;import java.util.Objects;import java.util.concurrent.TimeUnit;/*** 订阅和处理用户信息变更*/@Slf4jpublic class RiscDemo {/*** 公开配置信息地址*/private static final String PUBLIC_CONFIGURATION_URL = "https://risc.cloud.huawei.com/v1beta/public/risc/.well-known/risc-configuration";/*** 公开信息缓存*/private final LoadingCache<String, PublicConfiguration> publicConfigurationCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).build(key -> {HttpGet request = new HttpGet(PUBLIC_CONFIGURATION_URL);try (CloseableHttpResponse response = getClient().execute(request)) {HttpEntity responseEntity = response.getEntity();String ret = responseEntity != null ? EntityUtils.toString(responseEntity) : null;EntityUtils.consume(responseEntity);int statusCode = response.getStatusLine().getStatusCode();// http状态码不是200,抛出异常if (statusCode != HttpStatus.SC_OK) {throw new IOException("call failed! http status code: " + statusCode + ", response data: " + ret);}JSONObject configJson = (JSONObject) JSON.parse(ret);if (configJson == null) {throw new IllegalArgumentException("response param error! http status code: " + statusCode + ", response data: " + ret);}String issuer = configJson.getString("issuer");String jwksUri = configJson.getString("jwks_uri");if (Objects.isNull(issuer) || Objects.isNull(jwksUri)) {throw new IllegalArgumentException("response param error! http status code: " + statusCode + ", response data: " + ret);}PublicConfiguration publicConfiguration = new PublicConfiguration();publicConfiguration.setIssuer(issuer);publicConfiguration.setJwksUri(jwksUri);return publicConfiguration;}});/*** 公钥信息缓存*/private final LoadingCache<String, PublicKey> publicKeyCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader<String, PublicKey>() {@Overridepublic @Nullable PublicKey load(@NonNull String key) throws Exception {PublicConfiguration publicConfiguration = getPublicConfiguration();JwkProvider huaweiCerts = new UrlJwkProvider(new URL(publicConfiguration.getJwksUri()), null, null);return huaweiCerts.get(key).getPublicKey();}});/*** 调试方法入口* @param args main方法入参*/public static void main(String[] args) {// 消息请求头中Authorization: Bearer <token>中的<token>String token = "<token>";// Client IDString clientId = "<Client ID>";Jwt<?, ?> jwt = new RiscDemo().validateSecurityEventToken(token, clientId);if (Objects.isNull(jwt)) {// 验签失败log.error("verify sign failed");} else {// 验签成功log.info("verify sign success");}}/*** 对Authorization头域中的token进行验签** @param token 消息请求头中Authorization: Bearer <token>中的<token>* @param clientId clientId* @return 返回为null,则表示验签失败,否则表示验证成功*/public Jwt<?, ?> validateSecurityEventToken(String token, String clientId) {try {// 公开配置信息中的issuer值String issuer = getPublicConfiguration().getIssuer();SigningKeyResolver signingKeyResolver = new SigningKeyResolver() {private PublicKey getPublicKey(JwsHeader<?> jwsHeader) {try {return publicKeyCache.get(jwsHeader.getKeyId());} catch (Exception e) {throw new RuntimeException(e);}}@Overridepublic Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {return getPublicKey(jwsHeader);}@Overridepublic Key resolveSigningKey(JwsHeader jwsHeader, String s) {return getPublicKey(jwsHeader);}};// 验证并解析消息内容JwtParser parser = Jwts.parserBuilder().requireIssuer(issuer).requireAudience(clientId).setAllowedClockSkewSeconds(60).setSigningKeyResolver(signingKeyResolver).build();return parser.parse(token);} catch (IncorrectClaimException e) {// 消息的claim无效,针对异常进行处理(如:日志记录)log.error("claim invalid", e);} catch (SignatureException e) {// 验签失败,针对异常进行处理(如:日志记录)log.error("verify signature failed", e);} catch (Exception e) {// 其他异常,业务自行处理log.error("valid event token failed", e);}return null;}private PublicConfiguration getPublicConfiguration() {PublicConfiguration publicConfiguration = this.publicConfigurationCache.get("DEFAULT");if (publicConfiguration == null) {throw new IllegalArgumentException("public configuration get failed!");}return publicConfiguration;}private static CloseableHttpClient getClient() {PoolingHttpClientConnectionManager connectionManager = buildConnectionManager(new String[] {"TLSv1.2"}, new String[] {"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"});connectionManager.setMaxTotal(400);connectionManager.setDefaultMaxPerRoute(400);RequestConfig config = RequestConfig.custom().setConnectionRequestTimeout(100).setRedirectsEnabled(false).build();return HttpClients.custom().useSystemProperties().setConnectionManager(connectionManager).setDefaultRequestConfig(config).build();}private static PoolingHttpClientConnectionManager buildConnectionManager(String[] supportedProtocols,String[] supportedCipherSuites) {PoolingHttpClientConnectionManager connectionManager = null;try {SSLContext sc = SSLContext.getInstance("TLSv1.2");TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());tmf.init((KeyStore) null);sc.init(null, tmf.getTrustManagers(), null);SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sc, supportedProtocols,supportedCipherSuites, SSLConnectionSocketFactory.getDefaultHostnameVerifier());Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register("http", new PlainConnectionSocketFactory()).register("https", sslsf).build();connectionManager = new PoolingHttpClientConnectionManager(registry);} catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {log.error("build connect manager failed", e);}return connectionManager;}@Datastatic class PublicConfiguration {private String issuer;private String jwksUri;}} - 调用接口(
-
处理消息体。
-
JSON对象格式消息体
消息示例:用户注销华为账号
{"iss": "id.cloud.huawei.com","aud": "<Client ID>","iat": 1727619834,"jti": "6672ed7d5c5e4c3c92f343ecac40f326","events": {"https://schemas.openid.net/secevent/risc/event-type/account-purged": {"subject": {"extra": "<触发事件用户的OpenID>","iss": "id.cloud.huawei.com","sub": "<触发事件用户的UnionID>","subject_type": "iss_sub"}}}}消息示例:用户取消应用的授权
{"iss": "id.cloud.huawei.com","aud": "<Client ID>","iat": 1750403661,"jti": "97af1abdbbcd4f00a6d8b74c9b1bbb56","events": {"https://schemas.openid.net/secevent/oauth/event-type/tokens-revoked": {"subject": {"extra": "<触发事件用户的OpenID>","iss": "id.cloud.huawei.com","sub": "<触发事件用户的UnionID>","subject_type": "iss_sub"},"scopes": ["phone","userConsent","openid","email"]}}}消息示例:用户授权手机号变更
{"iss": "id.cloud.huawei.com","aud": "<Client ID>","iat": 1750385669,"jti": "c27c197ba5c94081aa32b8dbc52389f3","events": {"https://schemas.openid.net/secevent/oauth/event-type/phone-modified": {"subject": {"extra": "<触发事件用户的OpenID>","iss": "id.cloud.huawei.com","sub": "<触发事件用户的UnionID>","subject_type": "iss_sub"}}}} -
JSON数组格式消息体
消息示例:用户注销华为账号
[{"iss": "id.cloud.huawei.com","aud": "<Client ID>","iat": 1750385669,"jti": "6672ed7d5c5e4c3c92f343ecac40f326","events": {"https://schemas.openid.net/secevent/risc/event-type/account-purged": {"subject": {"extra": "<触发事件用户的OpenID>","iss": "id.cloud.huawei.com","sub": "<触发事件用户的UnionID>","subject_type": "iss_sub"}}}},{"iss": "id.cloud.huawei.com","aud": "<Client ID>","iat": 1750385669,"jti": "6672ed7d5c5e4c3c92f343ecac40f325","events": {"https://schemas.openid.net/secevent/risc/event-type/account-purged": {"subject": {"extra": "<触发事件用户的OpenID>","iss": "id.cloud.huawei.com","sub": "<触发事件用户的UnionID>","subject_type": "iss_sub"}}}}]消息示例:用户取消应用的授权
[{"iss": "id.cloud.huawei.com","aud": "<Client ID>","iat": 1750403661,"jti": "97af1abdbbcd4f00a6d8b74c9b1bbb56","events": {"https://schemas.openid.net/secevent/oauth/event-type/tokens-revoked": {"subject": {"extra": "<触发事件用户的OpenID>","iss": "id.cloud.huawei.com","sub": "<触发事件用户的UnionID>","subject_type": "iss_sub"},"scopes": ["phone","userConsent","openid","email"]}}},{"iss": "id.cloud.huawei.com","aud": "<Client ID>","iat": 1750403661,"jti": "97af1abdbbcd4f00a6d8b74c9b1bbb57","events": {"https://schemas.openid.net/secevent/oauth/event-type/tokens-revoked": {"subject": {"extra": "<触发事件用户的OpenID>","iss": "id.cloud.huawei.com","sub": "<触发事件用户的UnionID>","subject_type": "iss_sub"},"scopes": ["phone","userConsent","openid","email"]}}}]消息示例:用户授权手机号变更
[{"iss": "id.cloud.huawei.com","aud": "<Client ID>","iat": 1750385669,"jti": "c27c197ba5c94081aa32b8dbc52389f3","events": {"https://schemas.openid.net/secevent/oauth/event-type/phone-modified": {"subject": {"extra": "<触发事件用户的OpenID>","iss": "id.cloud.huawei.com","sub": "<触发事件用户的UnionID>","subject_type": "iss_sub"}}}},{"iss": "id.cloud.huawei.com","aud": "<Client ID>","iat": 1750385669,"jti": "c27c197ba5c94081aa32b8dbc52389f4","events": {"https://schemas.openid.net/secevent/oauth/event-type/phone-modified": {"subject": {"extra": "<触发事件用户的OpenID>","iss": "id.cloud.huawei.com","sub": "<触发事件用户的UnionID>","subject_type": "iss_sub"}}}}]
其中,各字段含义如下:
参数 描述 aud Client ID(与订阅用户信息变更中提供的Client ID一致)。 iss 消息投递者标识,固定值“id.cloud.huawei.com”。 iat 生成该事件的UTC时间戳(秒级)。 jti 唯一随机字符串,用于标识此消息体,开发者可根据此字段来识别重投消息体。 events 用户信息变更事件与事件消息体,格式为json。key是用户信息变更事件类型,value为其对应事件消息信息。 subject 用户信息变更事件对应的消息体,格式为json,包含字段说明如下: - sub:触发事件用户的UnionID(用户在同一个开发者下的所有应用中,此值唯一)。具体格式要求请参考OpenID和UnionID的格式说明。 - subject_type:固定为“iss_sub”。 - extra:触发事件用户的OpenID(用户在同一个应用中,此值唯一)。具体格式要求请参考OpenID和UnionID的格式说明。 - iss:标识消息投递者,固定为“id.cloud.huawei.com”。 scopes 取消授权的scope列表,格式为json数组。在事件类型为【 https://schemas.openid.net/secevent/oauth/event-type/tokens-revoked】时才存在此字段。 -