微服务学习系列7:开放平台接口鉴权

系列文章目录

第一章 Nacos实现配置中心

第二章 Nacos实现注册中心

第三章 Redis队列 


目录

系列文章目录

前言

一、什么是appId、appSecret、appToken

appId

appSecret

appToken

二、API 接口开发安全性 

三、实现原理

1.定义接口ParamSecret

2.鉴权处理器定义

3.BaseController 定义

四、使用步骤

1.BookListQuery 实现ParamSecret接口

2.ShareController

3.BookShareImpl

4.BookListQueryHandler

5.单元测试

总结



前言

接口如果是被我们前端项目调用,一般都是加了各种鉴权的,比如Spring Sercurity+token安全机制,shiro等框架都可以控制接口访问权限。但是如果接口是提供给外部调用,我们需要和第三方做个鉴权,比如常见的开放平台OpenApi。
 


一、什么是appId、appSecret、appToken

appId

应用的唯一标识,是用来标识客户端身份的。

appSecret

appKey 和 appSecret 是一对出现的账号,同一个 appId 可以对应多个 appKey+appSecret,这样平台就可以分配你不一样的权限,比如 appKey1 + appSecect1 只有只读权限 但是 appKey2+appSecret2 有读写权限…,这样你就可以把对应的权限放给不同的开发者,其中权限的配置都是直接跟appKey 做关联的,appKey 也需要添加数据库检索,方便快速查找。而在实际开发中,都是简单的直接将 appId = appKey,然后外加一个appSecret就够了。

appToken

客户端请求的token令牌,每次访问服务端数据,客户端需要携带token令牌。

token令牌 是 使用请求参数,appId 和 appSecret 的 md5加密串,

公式 appToken=md5(appId=100000&appSecret=12bc71&param1=value1&parma2=value2)

如:String appToken = SecureUtil.md5("appId=100000&appSecret=12bc71&bookId=1")

二、API 接口开发安全性 

三、实现原理

1.定义接口ParamSecret

作用:返回需要参加 加密的参数名和参数值 的健值对。

鉴权处理器  会对请求参数进行拦截,判断入参是否实现ParamSecret接口,如果实现ParamSecret接口 则需要对方法进行鉴权处理。

/**
 * 参数加密类定义
 *
 * @author yangyanping
 * @date 2023-03-14
 */
public interface ParamSecret {

    String Timestamp_Key = "timestamp";

    Map<String, String> getParamSecretMap();

    /**
     * 时间戳
     */
    default Long getTimestamp() {
        Map<String, String> 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);
    }
}

2.鉴权处理器定义

/**
 * 鉴权处理器类定义
 * @author yangyanping
 * @date 2022-08-26
 */
public abstract class AbstractSercurityCommandHandler<P extends Command, R> extends AbstractCommandHandler<P, R, Protocol> {
    @Resource
    protected AppSecretConfig appSecretConfig;

    /**
     * 验证权限
     */
    @Override
    protected void checkHeader(P param, Protocol header) {
        if (!(param instanceof ParamSecret)) {
            return;
        }

        //appId 和 appSecret 配置信息
        Map<String, String> 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<String, String> paramMap = paramSecret.getParamSecretMap();
        TreeMap<String, String> 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<String, String> 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 !");
        }
    }
}

3.BaseController 定义

/**
 * 基础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;
    }
}

四、使用步骤

1.BookListQuery 实现ParamSecret接口

/**
 * 书籍列表查询
 *
 * @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<String, String> getParamSecretMap() {
        Map<String, String> map = Maps.newHashMapWithExpectedSize(1);
        map.put("timestamp", Long.toString(timestamp));

        return map;
    }
}

2.ShareController

代码如下(示例):

/**
 * 开发平台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<List<BookInfoDTO>> getBookList() {
        BookListQuery query = new BookListQuery();

        return new RpcExecutor<BookListQuery, List<BookInfoDTO>>().invokeMethod(
                "shareApi",
                "BookShareController.getBookList",
                (param) -> bookShareImpl.getBookList(getProtocol(), param),
                query);
    }
}

3.BookShareImpl

代码如下(示例):

/**
 * 开发平台-书籍服务
 *
 * @author yangyanping
 * @date 2023-03-15
 */
@Service
@Validated
public class BookShareImpl {
    @Resource
    private BookListQueryHandler bookListQueryHandler;


    /**
     * 查询书籍列表
     */
    public ApiResult<List<BookInfoDTO>> getBookList(Protocol protocol, @Valid BookListQuery query){
        return bookListQueryHandler.doHandler(protocol,query);
    }
}

4.BookListQueryHandler

/**
 * 查询书籍列表
 *
 * @author yangyanping
 * @date 2023-03-16
 */
@Component
public class BookListQueryHandler extends AbstractSercurityCommandHandler<BookListQuery, List<BookInfoDTO>> {
    @Override
    protected void checkParam(BookListQuery param) {

    }

    @Override
    protected List<BookInfoDTO> doBusiness(BookListQuery param, Protocol header) {
        return null;
    }

    @Override
    protected String getUmp() {
        return "share.book.getBookList";
    }
}

5.单元测试

/**
 * 开发平台单元测试
 *
 * @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&timestamp=" + 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

猜你喜欢

转载自blog.csdn.net/yangyanping20108/article/details/129619155
今日推荐