第一章 Nacos实现配置中心
第二章 Nacos实现注册中心
第三章 Redis队列
目录
系列文章目录
前言
一、什么是appId、appSecret、appToken
appId
appSecret
appToken
二、API 接口开发安全性
三、实现原理
1.定义接口ParamSecret
2.鉴权处理器定义
3.BaseController 定义
四、使用步骤
1.BookListQuery 实现ParamSecret接口
4.BookListQueryHandler
5.单元测试
总结
接口如果是被我们前端项目调用,一般都是加了各种鉴权的,比如Spring Sercurity+token安全机制,shiro等框架都可以控制接口访问权限。但是如果接口是提供给外部调用,我们需要和第三方做个鉴权,比如常见的开放平台OpenApi。
应用的唯一标识,是用来标识客户端身份的。
appKey 和 appSecret 是一对出现的账号,同一个 appId 可以对应多个 appKey+appSecret,这样平台就可以分配你不一样的权限,比如 appKey1 + appSecect1 只有只读权限 但是 appKey2+appSecret2 有读写权限…,这样你就可以把对应的权限放给不同的开发者,其中权限的配置都是直接跟appKey 做关联的,appKey 也需要添加数据库检索,方便快速查找。而在实际开发中,都是简单的直接将 appId = appKey,然后外加一个appSecret就够了。
客户端请求的token令牌,每次访问服务端数据,客户端需要携带token令牌。
token令牌 是 使用请求参数,appId 和 appSecret 的 md5加密串,
公式 appToken=md5(appId=100000&appSecret=12bc71¶m1=value1&parma2=value2)
如:String appToken = SecureUtil.md5("appId=100000&appSecret=12bc71&bookId=1")
作用:返回需要参加 加密的参数名和参数值 的健值对。
鉴权处理器 会对请求参数进行拦截,判断入参是否实现ParamSecret接口,如果实现ParamSecret接口 则需要对方法进行鉴权处理。
/**
* 参数加密类定义
*
* @author yangyanping
* @date 2023-03-14
*/
public interface ParamSecret {
String Timestamp_Key = "timestamp";
Map getParamSecretMap();
/**
* 时间戳
*/
default Long getTimestamp() {
Map map = getParamSecretMap();
if (map == null || map.isEmpty()) {
return null;
}
String timestamp = map.get(Timestamp_Key);
if (timestamp == null || Objects.equals(timestamp, "")) {
return null;
}
return Long.valueOf(timestamp);
}
}
/**
* 鉴权处理器类定义
* @author yangyanping
* @date 2022-08-26
*/
public abstract class AbstractSercurityCommandHandler extends AbstractCommandHandler
{
@Resource
protected AppSecretConfig appSecretConfig;
/**
* 验证权限
*/
@Override
protected void checkHeader(P param, Protocol header) {
if (!(param instanceof ParamSecret)) {
return;
}
//appId 和 appSecret 配置信息
Map appMaps = appSecretConfig.getAppMaps();
if (MapUtil.isEmpty(appMaps)) {
throw new BizException("appSecretConfig is not empty !");
}
String appId = header.getAppId();
String appToken = header.getAppToken();
String appSecret = appMaps.get(appId);
if (StringUtils.isBlank(appSecret)) {
throw new BizException("appSecret is not empty !");
}
ParamSecret paramSecret = (ParamSecret) param;
Long timestamp = paramSecret.getTimestamp();
if (timestamp != null) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(timestamp);
//timestamp 和当前时间 不能大于5分钟
if (DateUtil.between(calendar.getTime(), new Date(), DateUnit.MINUTE, false) > 5) {
throw new BizException("timestamp is error !");
}
}
Map paramMap = paramSecret.getParamSecretMap();
TreeMap secretMap = Maps.newTreeMap();
if (CollectionUtil.isNotEmpty(paramMap)) {
paramMap.entrySet().stream().forEach(e -> {
secretMap.put(e.getKey(), e.getValue());
});
}
secretMap.put("appId", appId);
secretMap.put("appSecret", appSecret);
StringBuilder sb = new StringBuilder();
for (Map.Entry entry : secretMap.entrySet()) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
String str = sb.toString();
if (str.length() > 0) {
str = str.substring(0, sb.length() - 1);
}
String md5 = SecureUtil.md5(str);
//参数加密后 和 appToken比较是否一致
if (!Objects.equals(md5, appToken)) {
throw new BizException("appToken is error !");
}
}
}
/**
* 基础Controller
*
* @author yangyanping
* @date 2022-09-01
*/
@RestController
public class BaseController {
/**
* 获取请求Protocol
*/
protected Protocol getProtocol() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String appId = request.getHeader("appId");
if (StringUtils.isBlank(appId)) {
appId = request.getParameter("appId");
}
String appToken = request.getHeader("appToken");
if (StringUtils.isBlank(appToken)) {
appToken = request.getParameter("appToken");
}
Protocol protocol = new Protocol();
protocol.setAppId(appId);
protocol.setAppToken(appToken);
return protocol;
}
}
/**
* 书籍列表查询
*
* @author yangyanping
* @date 2023-03-16
*/
@Getter
@Setter
@ToString
public class BookListQuery extends Query implements ParamSecret {
@NotNull(message = "timestamp不能为空")
@Positive(message = "需要合法的timestamp")
private Long timestamp;
@Override
public Map getParamSecretMap() {
Map map = Maps.newHashMapWithExpectedSize(1);
map.put("timestamp", Long.toString(timestamp));
return map;
}
}
代码如下(示例):
/**
* 开发平台api
*
* @author yangyanping
* @date 2023-03-15
*/
@RestController
@RequestMapping("/openApi/v1/book/")
public class ShareController extends BaseController {
@Resource
private BookShareImpl bookShareImpl;
/**
* 查询书籍列表
*/
@RequestMapping("getBookList")
public ApiResult> getBookList() {
BookListQuery query = new BookListQuery();
return new RpcExecutor>().invokeMethod(
"shareApi",
"BookShareController.getBookList",
(param) -> bookShareImpl.getBookList(getProtocol(), param),
query);
}
}
代码如下(示例):
/**
* 开发平台-书籍服务
*
* @author yangyanping
* @date 2023-03-15
*/
@Service
@Validated
public class BookShareImpl {
@Resource
private BookListQueryHandler bookListQueryHandler;
/**
* 查询书籍列表
*/
public ApiResult> getBookList(Protocol protocol, @Valid BookListQuery query){
return bookListQueryHandler.doHandler(protocol,query);
}
}
/**
* 查询书籍列表
*
* @author yangyanping
* @date 2023-03-16
*/
@Component
public class BookListQueryHandler extends AbstractSercurityCommandHandler> {
@Override
protected void checkParam(BookListQuery param) {
}
@Override
protected List doBusiness(BookListQuery param, Protocol header) {
return null;
}
@Override
protected String getUmp() {
return "share.book.getBookList";
}
}
/**
* 开发平台单元测试
*
* @author yangyanping
* @date 2023-03-16
*/
public class ShareBookImplTest extends BaseTest {
@Resource
private BookShareImpl shareBookImpl;
@Test
public void getBookInfo() {
Protocol protocol = new Protocol();
protocol.setAppId("100000");
Long timestamp = System.currentTimeMillis();
String appToken = SecureUtil.md5("appId=100001&appSecret=12bc71&bookId=1×tamp=" + timestamp);
protocol.setAppToken(appToken);
BookListQuery query = new BookListQuery();
query.setTimestamp(timestamp);
ApiResult apiResult = shareBookImpl.getBookList(protocol, query);
System.out.println(JSON.toJSONString(apiResult));
}
}
如果接口是提供给外部调用,肯定是不需要登录的,需要在自身的权限控制中放开该接口的token校验,这样就会造成安全问题,我们一般采取拦截器的方式,和第三方做个鉴权。鉴权采用固定参数同样存在安全问题,容易被抓包获取到。所以一般带入动态的时间戳来鉴权,常用的鉴权逻辑是:两边各存一 份appId和appSecret。向服务器请求授权时,在请求Header中传递appId 和 appToken。
参考:开放api接口平台:appid、appkey、appsecret_51CTO博客_appid appkey appsecret