有如下的场景:你们公司业务越做越大,有很多的机构或者客户想跟你们公司合作,这时候,你们公司就需要提供对外的开放接口,以便合作伙伴调用。这时候,如何保证 API 接口调用的安全性?
一般来说,解决办法有以下几种:
1、API 请求使用加签名方式,防止篡改数据。
2、使用Https 协议加密传输。
3、搭建OAuth2.0认证授权平台。
4、使用令牌方式 accessToken。
5、搭建网关实现黑名单和白名单。
OK,我们来学习使用令牌方式搭建搭建API开放平台。
解决思路是:客户先申请一个有效的 AppID,确保是激活状态,然后调用获取 token 的接口(token 有效期设置 2 小时,需要保存到客户的本地缓存,然后开定时任务刷新),获取到有效的 token 后,接下来的 API 请求都需要携带 token。新的 token 被刷新后,旧的 token 就不能再使用了。
1、下载脚手架代码:https://pan.baidu.com/s/1zRZLxWZ-1P_6iMXmRv38WA 提取码:755u
2、设计表,增加测试数据:
drop table if exists t_app;
CREATE TABLE `t_app` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`app_name` varchar(255) DEFAULT NULL COMMENT '机构名称',
`app_id` varchar(64) DEFAULT NULL COMMENT 'AppID,唯一(不可更改)',
`app_secret` varchar(255) DEFAULT NULL COMMENT '密码(可更改)',
`active_flag` char(1) DEFAULT NULL COMMENT '状态:1=激活,0=停用',
`access_token` varchar(255) DEFAULT NULL COMMENT '上一次的 accessToken',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='机构管理表';
INSERT INTO `t_app` VALUES (1, '流放深圳', 'good123', '123', '1', 'c4d8dcb4d8b54bdd857094e35712ebb7');
INSERT INTO `t_app` VALUES (2, '乐视网', 'ls456', '456', '0', 'ras8dcb4d8b54bdd857094e35712eecw');
说明:合作结构一旦申请成功(一般我们提供一个管理平台给他们的),我们就分配一个唯一的 AppID 给他们,这个 AppID 不可更改。他们通过 AppID 和 appSecret 来获取 accessToken(一般是2小时内有效),在调用外网开放接口的时候,必须传递有效的access_token。【上一次的 accessToken】,主要是用来根据 token 删除旧的 token。
3、controller 类:
package com.study.controller;
import com.alibaba.fastjson.JSONObject;
import com.study.entity.AppEntity;
import com.study.service.AppService;
import com.study.util.BaseResponse;
import com.study.util.CodeUtil;
import com.study.util.TokenUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author biandan
* @description
* @signature 让天下没有难写的代码
* @create 2021-06-01 下午 11:19
*/
@RestController
public class AppController {
@Autowired
private AppService appService;
@Autowired
private TokenUtil tokenUtil;
//获取token
@GetMapping(value = "/getToken")
public BaseResponse getToken(AppEntity appEntity){
BaseResponse response = new BaseResponse();
AppEntity dbEntity = appService.findByAppId(appEntity.getAppId());
if(null == dbEntity){
response.setCode(CodeUtil.appInvalid);
response.setMessage("无此账号信息");
return response;
}
if(dbEntity.getActiveFlag().equals("0")){
response.setCode(CodeUtil.appNoActive);
response.setMessage("账号未激活");
return response;
}
if(!appEntity.getAppSecret().equals(dbEntity.getAppSecret())){
response.setCode(CodeUtil.appInfoError);
response.setMessage("账号信息错误");
return response;
}
//生成 token,并存入 Redis
String token = tokenUtil.getToken(dbEntity.getAppId());
//获取上一次的 token,并删除
tokenUtil.deleteKey(dbEntity.getAccessToken());
//更新数据库的 token
dbEntity.setAccessToken(token);
appService.updateToken(dbEntity);
//返回 json 格式给调用者
JSONObject jsonObject = new JSONObject();
jsonObject.put("token",token);
response.setCode(CodeUtil.success);
response.setMessage("success");
response.setData(jsonObject);
return response;
}
//根据 token 获取信息
@GetMapping(value = "/getInfo")
public BaseResponse getInfo(String token){
BaseResponse response = new BaseResponse();
if(StringUtils.isBlank(token)){
response.setCode(CodeUtil.accessTokenInvalid);
response.setMessage("token无效");
return response;
}
//根据 token 查询 Redis
String appId = tokenUtil.findToken(token);
if(StringUtils.isNotBlank(appId)){
AppEntity appEntity = appService.findByAppId(appId);
if(null != appEntity){
//只返回重要信息
JSONObject jsonObject = new JSONObject();
jsonObject.put("appId",appEntity.getAppId());
jsonObject.put("appName",appEntity.getAppName());
jsonObject.put("activeFlag",appEntity.getActiveFlag().equals("1") ? "激活":"停用");
response.setCode(CodeUtil.success);
response.setMessage("success");
response.setData(jsonObject);
return response;
}
}
response.setCode(CodeUtil.appInvalid);
response.setMessage("无此账号");
return response;
}
}
4、MyRedisTemplate 类:
package com.study.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author biandan
* @description 封装的 Redis 操作类
* @signature 让天下没有难写的代码
* @create 2021-06-02 上午 11:46
*/
@Component
public class MyRedisTemplate {
@Autowired
private StringRedisTemplate stringRedisTemplate;
//设置 String 对象
public Boolean setString(String key,Object data,Long timeout){
if(data instanceof String){
if(null != timeout){
stringRedisTemplate.opsForValue().set(key,(String)data,timeout, TimeUnit.SECONDS);
}else{
stringRedisTemplate.opsForValue().set(key,(String)data);
}
return true;
}else{
return false;
}
}
//获取 String 对象
public Object getString(String key){
return stringRedisTemplate.opsForValue().get(key);
}
//删除某个 key
public void delKey(String key){
stringRedisTemplate.delete(key);
}
}
注意:使用 StringRedisTemplate 时,要设置 key 的有效期,需要4个参数:
stringRedisTemplate.opsForValue().set(key,(String)data,timeout, TimeUnit.SECONDS);
否则报错,数据乱码,\u0000
测试:
1、获取 token,浏览器地址输入:http://127.0.0.1/getToken?appId=good123&appSecret=123
2、根据 token 获取信息:http://127.0.0.1/getInfo?token=3f126cc35a984070a4f6b8e4e41f1eda
3、输入错误的 token:
OK,accessToken 的核心思想如上。接下来我们把 token 放在拦截器中进行判断,不然每次请求接口都需要判断,导致代码冗余。
首先,增加拦截器:
package com.study.interceptor;
import com.alibaba.fastjson.JSON;
import com.study.util.BaseResponse;
import com.study.util.CodeUtil;
import com.study.util.TokenUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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.PrintWriter;
/**
* @author biandan
* @description accessToken 拦截器
* @signature 让天下没有难写的代码
* @create 2021-06-03 下午 2:24
*/
@Component
public class AccessTokenInterceptor implements HandlerInterceptor {
@Autowired
private TokenUtil tokenUtil;
//该方法将在Controller处理之前进行调用
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
BaseResponse msg = new BaseResponse();
String token = request.getParameter("token");
if (StringUtils.isBlank(token)) {
msg.setCode(CodeUtil.accessTokenInvalid);
msg.setMessage("token无效");
writeResult(msg, response);
return false;
}
//根据 token 查询 Redis
String appId = tokenUtil.findToken(token);
if (StringUtils.isBlank(appId)) {
msg.setCode(CodeUtil.accessTokenInvalid);
msg.setMessage("token无效");
writeResult(msg, response);
return false;
}else{
request.setAttribute("appId",appId);
}
//执行正常逻辑
return true;
}
//它的执行时间是在处理器进行处理之后,也就是在Controller的方法调用之后执行,但是它会在DispatcherServlet进行视图的渲染之前执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("***处理请求完成后视图渲染之前的处理操作***");
}
//该方法将在整个请求完成之后,也就是DispatcherServlet渲染了视图执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("******视图渲染之后的操作******");
}
//相应错误信息
private void writeResult(BaseResponse msg, HttpServletResponse response) throws Exception{
response.setHeader("Content-type", "text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
try {
writer.println(JSON.toJSONString(msg));
} catch (Exception e) {
e.printStackTrace();
} finally {
writer.close();
}
}
}
将拦截器注册到 Spring 容器:
package com.study.interceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author biandan
* @description
* @signature 让天下没有难写的代码
* @create 2021-06-03 下午 3:11
*/
@Configuration
public class MyWebAppConfig implements WebMvcConfigurer{
@Autowired
private AccessTokenInterceptor accessTokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 可添加多个拦截器的对象
registry.addInterceptor(accessTokenInterceptor).addPathPatterns("/**");
}
}
controller 层增加方法;
//根据 token 获取信息(使用拦截器)
@GetMapping(value = "/findInfo")
public BaseResponse findInfo() {
BaseResponse response = new BaseResponse();
//从拦截器中获取上下文的请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String appId = (String) request.getAttribute("appId");
AppEntity appEntity = appService.findByAppId(appId);
if (null != appEntity) {
//只返回重要信息
JSONObject jsonObject = new JSONObject();
jsonObject.put("appId", appEntity.getAppId());
jsonObject.put("appName", appEntity.getAppName());
jsonObject.put("activeFlag", appEntity.getActiveFlag().equals("1") ? "激活" : "停用");
response.setCode(CodeUtil.success);
response.setMessage("success");
response.setData(jsonObject);
return response;
}
response.setCode(CodeUtil.appInvalid);
response.setMessage("无此账号信息");
return response;
}
重启服务,浏览器地址输入:http://127.0.0.1/findInfo?token=3f126cc35a984070a4f6b8e4e41f1eda
{"code":"0","message":"success","data":{"appName":"流放深圳","appId":"good123","activeFlag":"激活"}}
控制台输出:
说明进入到了我们的拦截器。
代码地址:https://pan.baidu.com/s/1Ci9Ta-PdwfvFz2bFKp5uGQ 提取码:yp5y