


基于工作需求,抓住假期尾巴系统学习学习互联网开放平台设计思想。比如有个需求:现在A公司与B公司进行合作,B公司需要调用A公司开放的外网接口获取数据,如何保证外网开放接口的安全性呢?目前了解到的常用解决办法有:①使用加签名方式,防止篡改数据;②使用Https加密传输;③搭建OAuth2.0认证授权;④使用令牌方式;⑤ 搭建网关实现黑名单和白名单等方法等。小伙伴们有其他解决方案的非常欢迎留言交流哦

我们今天主要介绍第四种方式:通过令牌方式搭建API开发平台,come on一起学习喽~~
我们先说一下这种方式实现原理:API接口提供方为每个合作平台提供对应的app_id、app_secret,在访问API接口的时候,需先调用生成access_token的接口生成对应的令牌access_token(有效期eg:2小时),在调用外网开放接口的时候,必须传递有效的access_token。
我们通过设计数据库表来保存不同平台的app_id、app_secret信息,如下:
CREATE TABLE `app_info` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',`app_name` varchar(255) DEFAULT NULL COMMENT '平台名称',`app_id` varchar(255) DEFAULT NULL COMMENT '平台ID',`app_secret` varchar(255) DEFAULT NULL COMMENT '客户密钥',`authorited` tinyint(1) DEFAULT NULL COMMENT '是否有权访问(0 否 1是)',`access_token` varchar(255) DEFAULT NULL COMMENT '上一次token',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
接下来贴出实现的核心代码:该项目基于SpringBoot开发,项目工程结构如下图所示:

通过appId和appScret获取access_token接口代码如下:
@RestController@RequestMapping("/auth")public class AuthApiController {@Autowiredprivate AuthService authService;@RequestMapping("/getAccessToken")public BaseResponseVO getAccessToken(AppInfoEntity appInfoEntity) throws Exception {return authService.getAccessToken(appInfoEntity);}}public interface AuthService {/*** 根据appId 和 appScret获取access_token** @param appInfoEntity* @return* @throws Exception*/BaseResponseVO getAccessToken(AppInfoEntity appInfoEntity) throws Exception;}@Service("authService")public class AuthServiceImpl extends BaseApiResponse implements AuthService {@Autowiredprivate AppMapper appMapper;@Autowiredprivate JedisUtils jedisUtils;/*** 使用appId+appSecret 生成AccessToke* ① 获取对应的appId 和 appSecret* ② 使用appId 和 appSecret 生成唯一对应的access_token* ③ 删除之前的access_token* ④ 返回最新的access_token* (第②和③步在同一redis事物中)** @param appInfoEntity* @return*/@Transactional(rollbackFor = Exception.class)@Overridepublic BaseResponseVO getAccessToken(AppInfoEntity appInfoEntity) throws Exception {AppInfoEntity appInfo = appMapper.findAppInfo(appInfoEntity);if (null == appInfo)return this.setResultError("没有对应机构的认证信息");//获取该客户访问接口开关:关if (String.valueOf(Constants.AppAuthorited.IS_NOT_AUTHORITED.getValue()).equals(appInfo.getAuthorited()))return this.setResultError("您现在没有权限生成对应的AccessToken");//获取上一次access_tokenString accessToken = appInfo.getAccessToken();//从redis删除之前的access_tokenif (!StringUtils.isEmpty(accessToken))jedisUtils.delKey(accessToken);//获取新的token并缓存在redis中,有效期2小时String newAccessToken = getNewAccessToken(appInfo.getAppId());JSONObject jsonObject = new JSONObject();jsonObject.put("accessToken", newAccessToken);return this.setResultSuccessData(jsonObject);}/*** 根据appId 和 appScret获取access_token** @param appId* @return*/private String getNewAccessToken(String appId){// 使用appid+appsecret 生成对应的AccessToken 保存两个小时String accessToken = TokenUtils.getToken();// 生成最新的token key为accessToken value 为 appid,有效期为:2小时// 这里value存appId是因为后面查询使用,这里不能存AppInfoEntity实体,如果存实体,通过数据库修改该信息还需要同步修改redis内容,而appid是唯一不变的东西jedisUtils.setString(accessToken, appId, Constants.TOKEN_TIME_OUT);// 更新表中accessTokenappMapper.updateAccessToken(accessToken, appId);return accessToken;}}public interface AppMapper {/*** 根据appId和appSecret查询AppInfoEntity* @param appEntity* @return*/@Select("SELECT ID AS ID ,APP_NAME AS appName, app_id as appId, app_secret as appSecret ,authorited , access_token as accessToken from app_info "+ "where app_id=#{appId} and app_secret=#{appSecret} ")AppInfoEntity findAppInfo(AppInfoEntity appEntity);/*** 根据appId更新accessToken* @param accessToken* @param appId*/@Update(" update app_info set access_token =#{accessToken} where app_id=#{appId} ")void updateAccessToken(@Param("accessToken") String accessToken,@Param("appId") String appId);}@Datapublic class AppInfoEntity implements Serializable {private static final long serialVersionUID = 7997501981575442053L;private int id;/*** 平台appid(唯一不可变)*/private String appId;/*** 平台名称*/private String appName;/*** 平台secret(可变)*/private String appSecret;/*** 平台token*/private String accessToken;/*** 平台是否有权访问接口(0 否 1是)*/private int authorited;}
运行结果如下:

编写拦截器拦截请求,验证accessToken正确性:
AccessTokenInterceptor.java:拦截器:验证accessToken的正确性@Componentpublic class AccessTokenInterceptor extends BaseApiResponse implements HandlerInterceptor {Logger logger = LoggerFactory.getLogger(AccessTokenInterceptor.class);@Autowiredprivate JedisUtils jedisUtils;/*** 进入controller层之前拦截请求** @param httpServletRequest* @param httpServletResponse* @param o* @return* @throws Exception*/public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o)throws Exception {logger.info("开始进入请求地址拦截");httpServletResponse.setCharacterEncoding("utf8");//获取请求tokenString accessToken = httpServletRequest.getParameter("accessToken");// 判断accessToken是否空if (StringUtils.isEmpty(accessToken)) {// 参数Token accessTokenresultError("请求accessToken为null", httpServletResponse);return false;}//获取appIdString appId = (String) jedisUtils.getString(accessToken);if (StringUtils.isEmpty(appId)) {// accessToken 已经失效!resultError("accessToken 失效 ", httpServletResponse);return false;}// 携带有效accessToken 访问接口return true;}/*** 返回错误提示** @param errorMsg* @param httpServletResponse* @throws IOException*/public void resultError(String errorMsg, HttpServletResponse httpServletResponse) throws IOException {PrintWriter printWriter = httpServletResponse.getWriter();printWriter.write(new JSONObject().toJSONString(setResultError(errorMsg)));}public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o,ModelAndView modelAndView) throws Exception {}public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,Object o, Exception e) throws Exception {}}WebAppConfig.java:配置拦截地址@Configurationpublic class WebAppConfig {@Autowiredprivate AccessTokenInterceptor accessTokenInterceptor;/*** 配置拦截地址** @return*/@Beanpublic WebMvcConfigurer WebMvcConfigurer() {return new WebMvcConfigurer() {public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(accessTokenInterceptor).addPathPatterns("/openApi/*");};};}}@RestController@RequestMapping("/openApi")public class HelloApiController extends BaseApiResponse {@RequestMapping("/hello")public BaseResponseVO getHello(){return this.setResultSuccessData("hello:接口访问成功!");}}
携带有效access_token运行结果如下:

携带无效access_token运行结果如下:

我们设置access_token的有效期是2小时,那正常生产环境token过期以后是怎么处理呢?了解到微信开放平台是通过定时Job,比如每隔1小时50分刷新一次token,这里只提供思路,感兴趣小伙伴可以试一试哦~~
基于令牌访问API开放接口的方式先介绍到这里,感兴趣的小伙伴可以自己动手练一练,代码很简单,我们主要学习的是一种思想。








