微服务学习系列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

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
微服务架构中,网关作为服务的入口,扮演着非常重要的角色。对于网关的认证和鉴权,可以通过以下几种方式进行设计: 1. 使用JWT令牌认证:JWT令牌是一种轻量级的认证方式,可以在用户登录后颁发一个包含用户信息的JWT令牌,然后将该令牌存储在客户端中。当客户端请求微服务时,将JWT令牌携带在请求头中,网关可以通过验证JWT令牌中的签名和有效期来判断令牌是否有效,从而进行认证和鉴权。 2. 使用OAuth2认证:OAuth2是一种标准的认证协议,可以通过OAuth2服务器颁发访问令牌,然后将访问令牌存储在客户端中。当客户端请求微服务时,将访问令牌携带在请求头中,网关可以通过OAuth2服务器验证访问令牌的有效性,从而进行认证和鉴权。 3. 使用API密钥认证:API密钥是一种简单的认证方式,可以为每个微服务颁发一个唯一的API密钥,然后将API密钥存储在客户端中。当客户端请求微服务时,将API密钥携带在请求头中,网关可以通过验证API密钥的有效性来进行认证和鉴权。 4. 使用服务代理认证:服务代理是一种将多个微服务封装在一起的方式,可以通过服务代理对微服务进行统一的认证和鉴权。服务代理可以在请求微服务之前进行认证和鉴权,从而保证请求的合法性。 总之,在微服务中,网关的认证和鉴权非常重要,可以根据业务需求和安全要求,选择合适的认证方式进行设计。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值