⼀、开放接⼝设计说明:
在开发微信授权登入,访问用户信息,就会发现,在微信开发平台调用接口的流程如下:
1. 在开发平台申请到 appid 和 app_secret
2. 通过appid 和 app_secret 访问获取access_token(一般都要带一个时间戳请求timestamp)
3.通过获取的access_token去调用开发接口 (有效期2小时)
按照这个流程,我们通过Java代码实现一下
Action:
One: 数据库表设计(记录申请访问接口的对象)
CREATE TABLE `t_open_api` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`app_name` varchar(255) DEFAULT NULL,
`app_id` varchar(255) DEFAULT NULL,
`app_secret` varchar(255) DEFAULT NULL,
`is_flag` varchar(255) DEFAULT NULL
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
// 设计 申请app名称,appid,appSecret,状态(is_flag -1停用 1使用)
two: 生成Token的方式
1. UUID.randomUUID().toString().replace("-", "");
2. 雪花算法: 使用一个64bit的long型的数字作为全局唯一ID 分布式系统中应用广泛
three: 生成appId+appSecret
Post请求api获取appId,appSecret
{
"appName": "测试app", //申请名称
"phone": xxxx //联系人
}
@RequestMapping("/getApp")
public R getApp(@Validated({AddGroup.class}) @RequestBody TOpenApi appId) {
appId.setCreateDate(new Date());
String idStr = IdWorker.getIdStr();
String uuid = IdWorker.get32UUID();
appId.setAppSecret(uuid);
appId.setAppId(idStr);
appId.setIsFlag(1);
TOpenApi app = iTOpenApiService.getOne(new QueryWrapper<TOpenApi>().eq("app_name", appId.getAppName()).eq("phone", appId.getPhone()));
// 同步redis中
BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(appKey);
if (app == null) {
iTOpenApiService.save(appId);
operations.put(appId.getAppId(), JSON.toJSONString(appId));
return R.ok().put("app", appId);
} else {
operations.put(app.getAppId(), JSON.toJSONString(app));
return R.ok().put("app", app);
}
}
这里加入了 JSR303校验,并统一返回异常提示
成功返回:appId,appSecret
校验失败提示:
four: 生成access_token
伪代码:1. Post请求传入appId(唯一)+appSecret
2. 判断商户是否存在,(优化,数据量大,放入redis中 key为appId+appSecret)
3. 生成AccessToken 更新Redis中AccessToken,2小时TLL
如果是分布式系统,需要考虑给每个appId加锁 (1单获取到锁已经生成AccessToken就直接返回)
@RequestMapping("/getAccessToken")
public R getAccessToken(@Validated({AccessGroup.class}) @RequestBody TOpenApi appEntity) {
//校验传来数据的有效性
if (checkData(appEntity)) {
return R.error("没有权限生成AccessToken");
}
// 1.生成新的AccessToken
String accessToken = UUID.randomUUID().toString().replace("-", "");
String accessKey = ACCESSKEY + appEntity.getAppId();
if (redisTemplate.hasKey(accessKey)) {
accessToken = redisTemplate.opsForValue().get(accessKey);
} else {
redisTemplate.opsForValue().set(accessKey, accessToken, timeToken, TimeUnit.SECONDS);
redisTemplate.opsForValue().set(CHECKKEY + accessToken, appEntity.getAppId(), timeToken, TimeUnit.SECONDS);
}
return R.ok().put("accessToken", accessToken);
}
/**
* 检查是否满足申请,1. 秘钥不一致,或者秘钥不存在 2 已经停用 都不能获取accessToken
*
* @param appEntity
* @return
*/
private boolean checkData(TOpenApi appEntity) {
String appId = appEntity.getAppId();
String appSecret = appEntity.getAppSecret();
//获取redis中数据校验
BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(appKey);
String str = operations.get(appId);
TOpenApi t = JSON.parseObject(str, TOpenApi.class);
String secret = t.getAppSecret();
if (!secret.equals(appSecret)) {
//标识不满足
return true;
}
//已经停用
Integer flag = t.getIsFlag();
if (1 != flag) {
return true;
}
return false;
}
成功返回:
失败返回(校验提示+拦截异常提示下面five):
five 添加拦截器 AccessTokenInterceptor 判断请求参数accessToken (定义范围)
or 使用Aop 切入每个请求处理 (注解中加入范围)
package com.whm.code.demo.interceptor;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.whm.code.demo.entity.TOpenApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Slf4j
@Component
public class AccessTokenInterceptor implements HandlerInterceptor {
private static final String CHECKKEY = "checkKey:";
private static final String appKey = "appKey";
@Autowired
private StringRedisTemplate redisTemplate;
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o)
throws Exception {
String uri = httpServletRequest.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
// // 标识除了openApi/** 的接口都拦截 + 配置WebAppConfig 重写WebMvcConfigurer WebMvcConfigurer
// boolean match = antPathMatcher.match("/openApi/**", uri);
// if (match) {
// return true;
// }
//accessToken 放入请求头中
String accessToken = httpServletRequest.getHeader("accessToken");
if (StringUtils.isEmpty(accessToken)) {
// 返回错误消息
resultError(" this is parameter accessToken null ", httpServletResponse);
return false;
}
// 验证accessToken
String appId = redisTemplate.opsForValue().get(CHECKKEY + accessToken);
if (StrUtil.isEmpty(appId)) {
resultError("this is bad accessToken", httpServletResponse);
return false;
}
//判断appId的状态
BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(appKey);
String str = operations.get(appId);
TOpenApi t = JSON.parseObject(str, TOpenApi.class);
//已经停用
Integer flag = t.getIsFlag();
if (1 != flag) {
resultError("this is bad accessToken", httpServletResponse);
return false;
}
log.info("AccessTokenInterceptor---" + uri + "--访问成功");
return true;
}
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o,
ModelAndView modelAndView) {
log.info("AccessTokenInterceptor---处理请求完成后视图渲染之前的处理操作");
}
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
log.info("AccessTokenInterceptor---视图渲染之后的操作");
}
public void resultError(String errorMsg, HttpServletResponse httpServletResponse) throws IOException {
PrintWriter printWriter = httpServletResponse.getWriter();
// setResultError为封装的返回信息,请⾃定义
printWriter.write(new JSONObject().toJSONString(errorMsg));
}
}
//配置拦截范围
@Configuration
public class MyWebConfig implements WebMvcConfigurer {
@Autowired
private AccessTokenInterceptor accessTokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//设置拦截的请求
registry.addInterceptor(accessTokenInterceptor).addPathPatterns("/api/**");
}
}
six 获取accessToken访问别的开发接口 平台验证accessToken是否有效,有效后可访问数据
这里可以做数据的加密,解密操作。RSA/ AES
1 /api/** 请求的请求头带入accesstoken
2 /api/** 请求的请求头不带入accesstoken
这里也校验accessToken的有效性
3 其他api请求不用accessToken可直接访问
详细代码可留言邮箱。