暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【实战】通过令牌方式搭建API开放平台

ITSK 2020-01-29
959
简单的生活才能幸福,我们要懂得知足常乐;如此浮躁的世界,修炼一颗宁静的心灵,云淡风轻的日子里,一箪食、一瓢饮足矣。品一杯茶,读一本书,感受简单生活中的乐趣。


基于工作需求,抓住假期尾巴系统学习学习互联网开放平台设计思想。比如有个需求:现在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 {


@Autowired
private 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 {


@Autowired
private AppMapper appMapper;
@Autowired
private JedisUtils jedisUtils;


/**
* 使用appId+appSecret 生成AccessToke
* ① 获取对应的appId 和 appSecret
* ② 使用appId 和 appSecret 生成唯一对应的access_token
* ③ 删除之前的access_token
* ④ 返回最新的access_token
* (第②和③步在同一redis事物中)
*
* @param appInfoEntity
* @return
*/
@Transactional(rollbackFor = Exception.class)
@Override
public 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_token
String accessToken = appInfo.getAccessToken();
//从redis删除之前的access_token
if (!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);
// 更新表中accessToken
appMapper.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);
}


@Data
public 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的正确性
@Component
public class AccessTokenInterceptor extends BaseApiResponse implements HandlerInterceptor {
Logger logger = LoggerFactory.getLogger(AccessTokenInterceptor.class);


@Autowired
private 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");
//获取请求token
String accessToken = httpServletRequest.getParameter("accessToken");
// 判断accessToken是否空
if (StringUtils.isEmpty(accessToken)) {
// 参数Token accessToken
resultError("请求accessToken为null", httpServletResponse);
return false;
}
//获取appId
String 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:配置拦截地址
@Configuration
public class WebAppConfig {


@Autowired
private AccessTokenInterceptor accessTokenInterceptor;


/**
* 配置拦截地址
*
* @return
*/
@Bean
public 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开放接口的方式先介绍到这里,感兴趣的小伙伴可以自己动手练一练,代码很简单,我们主要学习的是一种思想。




欢迎关注ITSK,每天进步一点点,我们追求在交流中收获成长和快乐


文章转载自ITSK,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论