通过系统应用服务总会与三方服务商进行对接,既然有对接,就会有回调。但是此应用服务由于部署在公网访问,为了考虑系统安全系以及防止报文被篡改,这就意味着我们需要跟三方服务商进行鉴权技术方案设计。此文章,就是一个具体典型的案例,由于此应用服务有两个不同的场景,但是鉴权设计上又有不同差异之处,所以在总体程序设计上巧妙的满足场景的需求前提下,又能尽可能做到更好的扩展维护。
此次涉及到对接三方的两个不同场景,暂且定位场景1和场景2。场景1的鉴权方案就是通过http接口回调,在请求头+请求报文上做鉴权处理,具体鉴权机制:请求头中的签名=md5(base64(报文)+回调url+私钥+时间戳)。
而场景2的鉴权就是在请求报文中增加鉴权字段,该鉴权字段=md5(秘钥+字段1+字段2+字段3+…)。
总而言之,都是通过md5加密,只不过加密的数据步骤有些区别。
为了考虑减少代码的耦合度,同时尽可能提高后续的扩展性,在程序设计上引入了设计模式。
从上图可以看出,依然采用定义一个上下文对象BaseAuthenticateContext
,该类定义一个泛型,意味着需要子类来继承,并指定请求参数类。通过AbstractAuthenticateHandler
它来封装鉴权的共性逻辑,比如鉴权流程,以及相关复用的代码。相关子类来继承它,实现相关抽象方法即可。AuthenticateDispatcher
这个类来对外暴露,外部调用无需晓得具体使用哪个Handler
来处理,还需要委托给它即可。
定义一个枚举,来维护所有的鉴权场景类型,这里把场景抽象成Action
。
/**
* 鉴权活动枚举类型
*
* @author : Sieg Heil
* @since 2022/11/25 10:30 AM
*/
@Getter
@ToString
public enum AuthenticateActionEnum {
/**
* 企业直播活动变更
*/
ACTIVITY_STATUS_CHANGE("企业直播活动变更"),
/**
* 视频点播事件通知
*/
VOD_EVENT_NOTIFY("视频点播事件通知");
/**
* 构造函数
*
* @param desc 描述
*/
AuthenticateActionEnum(String desc) {
this.desc = desc;
}
/**
* 描述
*/
private final String desc;
}
通过@Autowired
这个注解,把AbstractAuthenticateHandler
的子类集合自动装配,作为该类的一个成员。同时,提供一个分发的方法。
/**
* 鉴权处理分发器
*
* @author : Sieg Heil
* @since 2022/11/25 10:28 AM
*/
@Component
public class AuthenticateDispatcher {
@Autowired
private List<AbstractAuthenticateHandler> handlerList;
/**
* 执行处理
*
* @param context 上下文对象
*/
public void execute(BaseAuthenticateContext context) {
handlerList.stream()
.filter(handler -> handler.getAction() == context.getAction())
.forEach(handler -> handler.execute(context));
}
}
定义一个上下文类。该类,包含一个内部静态类Response
,并作为它的成员属性,来封装鉴权执行结果。
/**
* 回调鉴权上下文对象
*
* @author : Sieg Heil
* @since 2022/11/25 10:08 AM
*/
@ToString
@Getter
@Setter
public abstract class BaseAuthenticateContext<Request> {
/**
* 活动类型
*/
private AuthenticateActionEnum action;
/**
* 请求参数
*/
private Request request;
/**
* 响应结果
*/
private Response response;
@ToString
@Getter
@Setter
public static class Response {
/**
* 静态变量
*/
public static String SUCCESS = "鉴权成功";
/**
* 鉴权是否成功
*/
private boolean success;
/**
* 鉴权结果
*/
private String result;
/**
* 静态方法
*
* @param result 鉴权结果
* @return 响应对象
*/
public static Response buildSuccess(String result) {
Response response = new Response();
response.setResult(result);
response.setSuccess(Boolean.TRUE);
return response;
}
/**
* 静态方法
*
* @param result 鉴权结果
* @return 响应对象
*/
public static Response buildFailure(String result) {
Response response = new Response();
response.setResult(result);
response.setSuccess(Boolean.FALSE);
return response;
}
}
}
具体的一个场景子类
/**
* [企业直播活动变更]回调鉴权上下文对象
*
* @author : Sieg Heil
* @since 2022/11/25 10:08 AM
*/
@ToString(callSuper = true)
@Getter
@Setter
public class ActivityStatusChangeAuthenticateContext extends BaseAuthenticateContext<SubscribeLiveActivityStatusChangeRequest> {
}
具体的一个场景子类
/**
* [视频点播事件通知]回调鉴权上下文对象
*
* @author : Sieg Heil
* @since 2022/11/25 10:08 AM
*/
@ToString(callSuper = true)
@Getter
@Setter
public class VodEventNotifyAuthenticateContext extends BaseAuthenticateContext<VolcVodRequestContext> {
}
鉴权处理类的基类,外部暴露的公共方法为public void execute(Context context)
。该方法内部封装了具体鉴权的相关步骤,相关子类只需要实现相关抽象方法即可。
三个重要抽象方法:
abstract String getTraceId(Context context)
:用于获取请求的traceId,便于日志打印,后续方便追踪问题。abstract void doExecute(Context context)
:用于做具体的鉴权执行逻辑abstract AuthenticateConfig getConfig()
:获取处理类场景的鉴权配置,该配置可以通过yml配置文件或者apollo实现,管理维护相关鉴权配置参数。/**
* 抽象鉴权处理器
*
* @author : Sieg Heil
* @since 2022/11/25 10:14 AM
*/
@Slf4j
public abstract class AbstractAuthenticateHandler<Context extends BaseAuthenticateContext> implements LoggerService {
@Autowired
protected VolcAuthenticateApolloConfig volcAuthenticateApolloConfig;
@PostConstruct
void init() {
getLog().info("AuthenticateApolloConfig={}", JsonUtils.toJson(volcAuthenticateApolloConfig));
}
/**
* 鉴权活动类型
*/
protected AuthenticateActionEnum action;
/**
* 活动名称
*/
protected String actionName;
/**
* 构造函数
*
* @param action 活动类型
*/
public AbstractAuthenticateHandler(AuthenticateActionEnum action) {
this.action = action;
if (Objects.nonNull(action)) {
this.actionName = action.name();
}
}
/**
* 对外部方法
*
* @param context
*/
public void execute(Context context) {
String traceId = getTraceId(context);
if (logDebug()) {
getLog().info("[{}|{}],context={}", traceId, actionName, JsonUtils.toJson(context));
}
AuthenticateConfig config = getConfig();
if (null == config) {
context.setResponse(BaseAuthenticateContext.Response.buildSuccess(SUCCESS));
return;
}
boolean enableSwitch = Optional.ofNullable(config.getEnableSwitch()).orElse(Boolean.FALSE);
//如果没有开启鉴权,则不执行鉴权
if (!enableSwitch) {
context.setResponse(BaseAuthenticateContext.Response.buildSuccess(SUCCESS));
return;
}
doExecute(context);
BaseAuthenticateContext.Response response = context.getResponse();
getLog().info("[{}|{}]{}", traceId, actionName, JsonUtils.toJson(response));
if (!response.isSuccess()) {
throw new ForbiddenException("鉴权失败[" + response.getResult() + "]", response.getResult());
}
}
@Override
public boolean logDebug() {
Boolean enableLogDebug = volcAuthenticateApolloConfig.getEnableLogDebug();
Boolean enable = Optional.ofNullable(enableLogDebug).orElse(Boolean.TRUE);
return enable.booleanValue();
}
/**
* 获取一个traceId,用于问题排查使用
*
* @param context 上下文对象
* @return traceId
*/
protected abstract String getTraceId(Context context);
/**
* 执行鉴权
* 需要子类实现此方法,完成具体的健全处理
*
* @param context 上下文对象
*/
protected abstract void doExecute(Context context);
/**
* 获取鉴权配置
*
* @return 鉴权配置
*/
protected abstract AuthenticateConfig getConfig();
public AuthenticateActionEnum getAction() {
return action;
}
}
鉴权场景1的具体鉴权逻辑。
/**
* [企业直播活动变更]回调鉴权处理器
*
* @author : Sieg Heil
* @since 2022/11/25 10:20 AM
*/
@Component
@Slf4j
public class ActivityStatusChangeAuthenticateHandler extends AbstractAuthenticateHandler<ActivityStatusChangeAuthenticateContext> {
/**
* 构造函数
*/
public ActivityStatusChangeAuthenticateHandler() {
super(AuthenticateActionEnum.ACTIVITY_STATUS_CHANGE);
}
@Override
public Logger getLog() {
return log;
}
@Override
protected String getTraceId(ActivityStatusChangeAuthenticateContext context) {
return context.getRequest().getActivityID();
}
@Override
protected AuthenticateConfig getConfig() {
return volcAuthenticateApolloConfig.getActivityStatusChange();
}
@Override
protected void doExecute(ActivityStatusChangeAuthenticateContext context) {
SubscribeLiveActivityStatusChangeRequest request = context.getRequest();
String sign = request.getSign();
String signature = getSignature(context);
if (Objects.equals(sign, signature)) {
context.setResponse(BaseAuthenticateContext.Response.buildSuccess(SUCCESS));
} else {
String traceId = getTraceId(context);
if (logDebug()) {
getLog().info("[{}|{}],ts={},encrypted={}", traceId, actionName, request.getTimestamp(), signature);
}
String debug = MessageFormat.format("activityId={0},signature={1},md5={2}", traceId, sign, signature);
context.setResponse(BaseAuthenticateContext.Response.buildFailure(debug));
}
}
/**
* 获取报文加密后的密文
*
* @param context 上下文对象
* @return 密文
*/
private String getSignature(ActivityStatusChangeAuthenticateContext context) {
SubscribeLiveActivityStatusChangeRequest request = context.getRequest();
String privateKey = volcAuthenticateApolloConfig.getActivityStatusChange().getPrivateKey();
StringBuilder content = new StringBuilder(privateKey);
content.append(request.getActivityID()).append(request.getEventType())
.append(request.getStatus()).append(request.getTimestamp());
String original = content.toString();
String encrypted = Md5Util.encrypt(original);
return encrypted;
}
}
鉴权场景2的具体鉴权逻辑。
/**
* [视频点播事件通知]回调鉴权处理器
*
* @author : Sieg Heil
* @since 2022/11/25 10:20 AM
*/
@Component
@Slf4j
public class VodEventNotifyAuthenticateHandler extends AbstractAuthenticateHandler<VodEventNotifyAuthenticateContext> {
/**
* 构造函数
*/
public VodEventNotifyAuthenticateHandler() {
super(AuthenticateActionEnum.VOD_EVENT_NOTIFY);
}
@Override
public Logger getLog() {
return log;
}
@Override
protected String getTraceId(VodEventNotifyAuthenticateContext context) {
return context.getRequest().getRequest().getRequestId();
}
@Override
protected AuthenticateConfig getConfig() {
return volcAuthenticateApolloConfig.getVodEventNotify();
}
@Override
protected void doExecute(VodEventNotifyAuthenticateContext context) {
VolcVodRequestContext requestContext = context.getRequest();
String sign = requestContext.getSignature();
String original = getMd5Content(context);
String signature = Md5Util.encrypt(original);
if (Objects.equals(sign, signature)) {
context.setResponse(BaseAuthenticateContext.Response.buildSuccess(SUCCESS));
} else {
String traceId = getTraceId(context);
if (logDebug()) {
getLog().info("[{}|{}],encrypted={}", traceId, actionName, signature);
}
String debug = MessageFormat.format("requestId={0},signature={1},md5={2}", traceId, sign, signature);
context.setResponse(BaseAuthenticateContext.Response.buildFailure(debug));
}
}
private String getMd5Content(VodEventNotifyAuthenticateContext context){
VolcVodRequestContext requestContext = context.getRequest();
String requestBody = requestContext.getRequestBody();
String privateKey = volcAuthenticateApolloConfig.getVodEventNotify().getPrivateKey();
String callbackUrl = volcAuthenticateApolloConfig.getVodEventNotify().getCallbackUrl();
String callbackContent = encode(requestBody);
StringBuilder original = new StringBuilder(callbackUrl).append("|")
.append(requestContext.getTimestamp()).append("|")
.append(privateKey).append("|")
.append(callbackContent);
return original.toString();
}
private String encode(String value) {
Base64.Encoder encoder = Base64.getEncoder();
return encoder.encodeToString(value.getBytes(StandardCharsets.UTF_8));
}
}
鉴权配置类
/**
* 鉴权配置类
*
* @author : Sieg Heil
* @since 2022/11/25 11:47 AM
*/
@ToString(callSuper = true)
@Getter
@Setter
public class AuthenticateConfig {
/**
* 鉴权开关
*/
private Boolean enableSwitch;
/**
* 鉴权私钥
*/
private String privateKey;
/**
* 回调url
*/
private String callbackUrl;
/**
* 鉴权策略
*/
private StrategyEnum strategy;
/**
* 鉴权策略类型
*/
public enum StrategyEnum {
/**
* 对报文进行MD5加密,防止报文被篡改
*/
MD5
}
}
所有回调场景鉴权配置类
/**
* 回调鉴权配置
*
* @author : Sieg Heil
* @since 2022/11/25 11:55 AM
*/
@Component
@RefreshScope
@ConfigurationProperties(prefix = "xxx.xxx.authenticate.volc")
@ToString
@Getter
@Setter
public class VolcAuthenticateApolloConfig {
/**
* 是否启用日志输出,便于追踪问题
*/
private Boolean enableLogDebug;
/**
* 企业直播活动变更
*/
private AuthenticateConfig activityStatusChange;
/**
* 视频点播事件通知
*/
private AuthenticateConfig vodEventNotify;
}
yml配置文件,可以通过diamond或者apollo,当前应用服务对接了apollo。
xxx:
xxx:
# 回调配置 true|false
callback:
# 订阅企业直播活动状态变更
subscribeVolcActivityStatusChange:
# 启用日志输出
logDebug: true
# 启用日志输出
enableHandle: false
# 订阅视频点播事件通知
subscribeVolcVodEventNotify:
# 启用日志输出
logDebug: true
# 启用日志输出
enableHandle: false
# 鉴权配置
authenticate:
# 鉴权配置
volc:
# 是否启用日志输出,便于追踪问题 true|false
enableLogDebug: true
# 企业直播活动变更
activityStatusChange:
# 鉴权开关
enableSwitch: true
# 鉴权私钥
privateKey: xxxxx
# 回调url
callbackUrl: xxxx
# 鉴权策略
strategy: MD5
# 视频点播事件通知
vodEventNotify:
# 鉴权开关
enableSwitch: true
# 鉴权私钥
privateKey: xxxx
# 回调url
callbackUrl: xxxx
# 鉴权策略
strategy: MD5
/**
* Created at 2022/5/24 11:01 AM
*
* @author : Sieg Heil
*/
@ThriftService(service = "volcEngineCallback")
@Validated
@Slf4j
public class VolcEngineCallbackServiceImpl implements VolcEngineCallbackService{
@Autowired
private VolcEngineCallbackConverter volcEngineCallbackConverter;
@Autowired
private CallbackEnableSwitch callbackEnableSwitch;
@Autowired
private SubscribeLiveStatusEventDispatcher subscribeLiveStatusEventDispatcher;
@Autowired
private SubscribeVodEventDispatcher subscribeVodEventDispatcher;
@Autowired
private AuthenticateDispatcher authenticateDispatcher;
@Override
public void subscribeLiveActivityStatusChange(SubscribeLiveActivityStatusChangeRequest request) {
ActivityStatusChangeAuthenticateContext authenticateContext = volcEngineCallbackConverter.convertToActivityStatusChangeAuthenticateContext(request);
authenticateDispatcher.execute(authenticateContext);
if (!callbackEnableSwitch.subscribeVolcActivityStatusChange()) {
String traceId = request.getActivityID();
log.info("[SubscribeLiveActivityStatusChange|{}]业务处理开关关闭|{}", traceId, callbackEnableSwitch.subscribeVolcActivityStatusChange());
return;
}
SubscribeLiveStatusEventContext context = volcEngineCallbackConverter.convertToSubscribeLiveStatusEventContext(request);
subscribeLiveStatusEventDispatcher.execute(context);
}
}