API网关是一个轻量的java http 接口组件,可无缝将普通的Service方法转换成http接口。并从以下几个方卖弄来达到提高开发效率与接口质量的目的。
(1) 去掉mvc控制器,将http请求无缝接入JAVA服务接口
(2) 统一出入参格式
(3) 统一异常规范
(4) 自动检测服务接口规范
名称 | 类型 | 描述 |
---|---|---|
method | string | 方法名称 |
paramter | json | 业务参数 |
timestamp | long | 请求时间戳 |
实现技术
(1) 接口安全级别分组
(2) 基于Token安全机制认证需求
(3) Token认证整体架构
**整体架构分为Token生成与认证两部分 **
Token表结构说明
名称 | 类别 | 说明 | 约束 |
---|---|---|---|
id | number | id主键 | 主键,自增长 |
memberId | number | 会员ID | |
accessToken | varchar(50) | Token | 索引 |
secret | varchar(50) | 密钥 | |
createdTime | datetime | 创建时间 | |
expiresTime | datetime | 有效期至 | |
clientIp | varchar(50) | 客户端IP | |
clientType | varchar(50) | 客户端类别 | |
eCode | varchar(50) | 设备标识 | |
uCode | varchar(50) | 设备用户标识 |
签名规则
签名的目的
(4) 基于API网关实现安全机制
签名认证与API网关的整体认证流程
两个流程
token生成:登录成功后,生成token与secret保存到数据库
Token认证相关解决方案:
core包下类
public class ApiStore {
private ApplicationContext applicationContext;
// API 接口住的地方
private HashMap<String, ApiRunnable> apiMap = new HashMap<String, ApiRunnable>();
// spring ioc
public ApiStore(ApplicationContext applicationContext) {
Assert.notNull(applicationContext);
this.applicationContext = applicationContext;
}
/**
*
*/
public void loadApiFromSpringBeans() {
// ioc 所有BEan
// spring ioc 扫描
String[] names = applicationContext.getBeanDefinitionNames();
Class<?> type;
//反谢
for (String name : names) {
type = applicationContext.getType(name);
for (Method m : type.getDeclaredMethods()) {
// 通过反谢拿到APIMapping注解
APIMapping apiMapping = m.getAnnotation(APIMapping.class);
if (apiMapping != null) {
addApiItem(apiMapping, name, m);
}
}
}
}
public ApiRunnable findApiRunnable(String apiName) {
return apiMap.get(apiName);
}
/**
*
* 添加api
*
* @param apiMapping
* api配置
* @param beanName
* beanq在spring context中的名称
* @param method
*/
private void addApiItem(APIMapping apiMapping, String beanName, Method method) {
ApiRunnable apiRun = new ApiRunnable();
apiRun.apiName = apiMapping.value();
apiRun.targetMethod = method;
apiRun.targetName = beanName;
apiRun.apiMapping=apiMapping;
apiMap.put(apiMapping.value(), apiRun);
}
public ApiRunnable findApiRunnable(String apiName, String version) {
return (ApiRunnable) apiMap.get(apiName + "_" + version);
}
public List<ApiRunnable> findApiRunnables(String apiName) {
if (apiName == null) {
throw new IllegalArgumentException("api name must not null!");
}
List<ApiRunnable> list = new ArrayList<ApiRunnable>(20);
for (ApiRunnable api : apiMap.values()) {
if (api.apiName.equals(apiName)) {
list.add(api);
}
}
return list;
}
public List<ApiRunnable> getAll() {
List<ApiRunnable> list = new ArrayList<ApiRunnable>(20);
list.addAll(apiMap.values());
Collections.sort(list, new Comparator<ApiRunnable>() {
public int compare(ApiRunnable o1, ApiRunnable o2) {
return o1.getApiName().compareTo(o2.getApiName());
}
});
return list;
}
public boolean containsApi(String apiName, String version) {
return apiMap.containsKey(apiName + "_" + version);
}
public ApplicationContext getApplicationContext() {
return applicationContext;
}
// 用于执行对应的API方法,
public class ApiRunnable {
String apiName; //bit.api.user.getUser
String targetName; //ioc bean 名称
Object target; // UserServiceImpl 实例
Method targetMethod; // 目标方法 getUser
APIMapping apiMapping;
public Object run(Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
if (target == null) {
// spring ioc 容器里面去服务Bean 比如GoodsServiceImpl
target = applicationContext.getBean(targetName);
}
return targetMethod.invoke(target, args);
}
public Class<?>[] getParamTypes() {
return targetMethod.getParameterTypes();
}
public String getApiName() {
return apiName;
}
public String getTargetName() {
return targetName;
}
public Object getTarget() {
return target;
}
public Method getTargetMethod() {
return targetMethod;
}
public APIMapping getApiMapping() {
return apiMapping;
}
}
}
public class ApiRequest {
private String memberId;
private String accessToken;
private String sign;
private String uCode;
private String eCode;
private String timestamp;
private String clientIp;
private boolean isLogin;
private String params;
private String methodName;
public String getMemberId() {
return memberId;
}
public void setMemberId(String memberId) {
this.memberId = memberId;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getSign() {
return sign;
}
public void setSign(String sign) {
this.sign = sign;
}
public String getuCode() {
return uCode;
}
public void setuCode(String uCode) {
this.uCode = uCode;
}
public String geteCode() {
return eCode;
}
public void seteCode(String eCode) {
this.eCode = eCode;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
public String getClientIp() {
return clientIp;
}
public void setClientIp(String clientIp) {
this.clientIp = clientIp;
}
public boolean isLogin() {
return isLogin;
}
public void setLogin(boolean login) {
isLogin = login;
}
public String getParams() {
return params;
}
public void setParams(String params) {
this.params = params;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
}
public class Token implements java.io.Serializable{
private long id;
private String memberId;
private String accessToken;
private String secret;
private Date createdTime;
private Date expiresTime;
private String clientIp;
private String clientType;
private String eCode;
private String uCode;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getMemberId() {
return memberId;
}
public void setMemberId(String memberId) {
this.memberId = memberId;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public Date getCreatedTime() {
return createdTime;
}
public void setCreatedTime(Date createdTime) {
this.createdTime = createdTime;
}
public Date getExpiresTime() {
return expiresTime;
}
public void setExpiresTime(Date expiresTime) {
this.expiresTime = expiresTime;
}
public String getClientIp() {
return clientIp;
}
public void setClientIp(String clientIp) {
this.clientIp = clientIp;
}
public String getClientType() {
return clientType;
}
public void setClientType(String clientType) {
this.clientType = clientType;
}
public String geteCode() {
return eCode;
}
public void seteCode(String eCode) {
this.eCode = eCode;
}
public String getuCode() {
return uCode;
}
public void setuCode(String uCode) {
this.uCode = uCode;
}
}
public interface TokenService {
public Token createToken();
public Token getToken(String token);
}
public class ApiGatewayHand implements InitializingBean, ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(ApiGatewayHand.class);
private static final String METHOD = "method";
private static final String PARAMS = "params";
ApiStore apiSorte;
final ParameterNameDiscoverer parameterUtil;
private TokenService tokenService;
public ApiGatewayHand() {
parameterUtil = new LocalVariableTableParameterNameDiscoverer();
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
apiSorte = new ApiStore(context);
}
@Override
public void afterPropertiesSet() throws Exception {
apiSorte.loadApiFromSpringBeans();
}
public void handle(HttpServletRequest request, HttpServletResponse response) {
// 系统参数验证
String params = request.getParameter(PARAMS);
String method = request.getParameter(METHOD);
Object result;
ApiRunnable apiRun = null;
ApiRequest apiRequest = null;
try {
apiRun = sysParamsValdate(request);
// 构建apiRequest
apiRequest = buildApiRequest(request);
// 签名验证
if (apiRequest.getAccessToken() != null)
signCheck(apiRequest);
if (apiRun.getApiMapping().useLogin()) {
if (apiRequest.isLogin()) {
throw new ApiException("009","调用失败:用户未登陆");
}
}
logger.info("请求接口={" + method + "} 参数=" + params + "");
Object[] args = buildParams(apiRun, params, request, response,apiRequest);
result = apiRun.run(args);
} catch (ApiException e) {
response.setStatus(500);// 封装异常并返回
logger.error("调用接口={" + method + "}异常 参数=" + params + "", e);
result = handleError(e);
} catch (InvocationTargetException e) {
response.setStatus(500);// 封装业务异常并返回
logger.error("调用接口={" + method + "}异常 参数=" + params + "", e.getTargetException());
result = handleError(e.getTargetException());
} catch (Exception e) {
response.setStatus(500);// 封装业务异常并返回
logger.error("其他异常", e);
result = handleError(e);
}
// 统一返回结果
returnResult(result, response);
}
private ApiRequest buildApiRequest(HttpServletRequest request) {
ApiRequest apiRequest = new ApiRequest();
apiRequest.setAccessToken(request.getParameter("token"));
apiRequest.setSign(request.getParameter("sign"));
apiRequest.setTimestamp(request.getParameter("timestamp"));
apiRequest.seteCode(request.getParameter("eCode"));
apiRequest.setuCode(request.getParameter("uCode"));
apiRequest.setParams(request.getParameter("params"));
return apiRequest;
}
private ApiRequest signCheck(ApiRequest request) throws ApiException {
Token token = tokenService.getToken(request.getAccessToken());
if (token == null) {
throw new ApiException("验证失败:指定'Token'不存在");
}
if (token.getExpiresTime().before(new Date())) {
throw new ApiException("验证失败:指定'Token'已失效");
}
// 生成签名
String methodName = request.getMethodName();
String accessToken = token.getAccessToken();
String secret = token.getSecret();
String params = request.getParams();
String timestamp = request.getTimestamp();
String sign = Md5Util.MD5(secret + methodName + params + token + timestamp + secret);
if (!sign.toUpperCase().equals(request.getSign())) {
throw new ApiException("验证失败:签名非法");
}
// 时间验证
if (Math.abs(Long.valueOf(timestamp) - System.currentTimeMillis()) > 10 * 60 * 1000) {
throw new ApiException("验证失败:签名失效");
}
request.setLogin(true);
request.setMemberId(token.getMemberId());
return request;
}
private Object handleError(Throwable throwable) {
String code = "";
String message = "";
if (throwable instanceof ApiException) {
code = "0001";
message = throwable.getMessage();
} // 扩展异常规范
else {
code = "0002";
message = throwable.getMessage();
}
Map<String, Object> result = new HashMap<>();
result.put("error", code);
result.put("msg", message);
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintStream stream = new PrintStream(out);
throwable.printStackTrace(stream);
// result.put("stack", out.toString());
return result;
}
private ApiRunnable sysParamsValdate(HttpServletRequest request) throws ApiException {
String apiName = request.getParameter(METHOD);
String json = request.getParameter(PARAMS);
ApiRunnable api;
if (apiName == null || apiName.trim().equals("")) {
throw new ApiException("调用失败:参数'method'为空");
} else if (json == null) {
throw new ApiException("调用失败:参数'params'为空");
} else if ((api = apiSorte.findApiRunnable(apiName)) == null) {
throw new ApiException("调用失败:指定API不存在,API:" + apiName);
}
// 多一个签名参数
//
return api;
}
/***
* 验证业务参数,和构建业务参数对象
* @param run
* @param paramJson
* @param request
* @param response
* @param apiRequest
* @return
* @throws ApiException
*/
private Object[] buildParams(ApiRunnable run, String paramJson, HttpServletRequest request, HttpServletResponse response, ApiRequest apiRequest)
throws ApiException {
Map<String, Object> map = null;
try {
map = UtilJson.toMap(paramJson);
} catch (IllegalArgumentException e) {
throw new ApiException("调用失败:json字符串格式异常,请检查params参数 ");
}
if (map == null) {
map = new HashMap<>();
}
Method method = run.getTargetMethod();// javassist
List<String> paramNames = Arrays.asList(parameterUtil.getParameterNames(method));
// goods ,id
Class<?>[] paramTypes = method.getParameterTypes(); //反射
for (Map.Entry<String, Object> m : map.entrySet()) {
if (!paramNames.contains(m.getKey())) {
throw new ApiException("调用失败:接口不存在‘" + m.getKey() + "’参数");
}
}
Object[] args = new Object[paramTypes.length];
for (int i = 0; i < paramTypes.length; i++) {
if (paramTypes[i].isAssignableFrom(HttpServletRequest.class)) {
args[i] = request;
} else if (paramTypes[i].isAssignableFrom(ApiRequest.class)){
args[i] = apiRequest;
}else if (map.containsKey(paramNames.get(i))) {
try {
args[i] = convertJsonToBean(map.get(paramNames.get(i)), paramTypes[i]);
} catch (Exception e) {
throw new ApiException("调用失败:指定参数格式错误或值错误‘" + paramNames.get(i) + "’"
+ e.getMessage());
}
} else {
args[i] = null;
}
}
return args;
}
// 将MAP转换成具体的目标方方法参数对象
private <T> Object convertJsonToBean(Object val, Class<T> targetClass) throws Exception {
Object result = null;
if (val == null) {
return null;
} else if (Integer.class.equals(targetClass)) {
result = Integer.parseInt(val.toString());
} else if (Long.class.equals(targetClass)) {
result = Long.parseLong(val.toString());
} else if (Date.class.equals(targetClass)) {
if (val.toString().matches("[0-9]+")) {
result = new Date(Long.parseLong(val.toString()));
} else {
throw new IllegalArgumentException("日期必须是长整型的时间戳");
}
} else if (String.class.equals(targetClass)) {
if (val instanceof String) {
result = val;
} else {
throw new IllegalArgumentException("转换目标类型为字符串");
}
} else {
result = UtilJson.convertValue(val, targetClass);
}
return result;
}
private void returnResult(Object result, HttpServletResponse response) {
try {
UtilJson.JSON_MAPPER.configure(
SerializationFeature.WRITE_NULL_MAP_VALUES, true);
String json = UtilJson.writeValueAsString(result);
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html/json;charset=utf-8");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
if (json != null)
response.getWriter().write(json);
} catch (IOException e) {
logger.error("服务中心响应异常", e);
throw new RuntimeException(e);
}
}
public static void main(String args[]) {
//{"goods":{"goodsName":"daa","goodsId":"1111"},"id":19}
String mapString = "{\"goods\":{\"goodsName\":\"daa\",\"goodsId\":\"1111\"},\"id\":19}";
Map<String, Object> map = UtilJson.toMap(mapString);
System.out.print(map);
UtilJson.convertValue(map.get("goods"), Goods.class);
String str = "{\"goodsName\":\"daa\",\"goodsId\":\"1111\"}";
Goods goods = UtilJson.convertValue(str, Goods.class);
System.out.print(goods);
}
}
public class ApiGatewayServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
ApplicationContext context;
private ApiGatewayHand apiHand;
@Override
public void init() throws ServletException {
super.init();
context = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
apiHand = context.getBean(ApiGatewayHand.class);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
apiHand.handle(request, response);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
apiHand.handle(req, resp);
}
}
service包下的类
@Service
//goodsServiceImpl
public class GoodsServiceImpl {
//无缝集成
@APIMapping(value = "bit.api.goods.add",useLogin = true)
public Goods addGoods(Goods goods, Integer id, ApiRequest apiRequest){
return goods;
}
@APIMapping("bit.api.goods.get")
public Goods getGodds(Integer id){
return new Goods("vvv","1111");
}
public static class Goods implements Serializable{
private String goodsName;
private String goodsId;
public Goods(){
}
public Goods(String goodsName, String goodsId) {
this.goodsName = goodsName;
this.goodsId = goodsId;
}
public String getGoodsName() {
return goodsName;
}
public void setGoodsName(String goodsName) {
this.goodsName = goodsName;
}
public String getGoodsId() {
return goodsId;
}
public void setGoodsId(String goodsId) {
this.goodsId = goodsId;
}
}
}
common包下的类
public class ApiException extends Exception{
private String code;
public ApiException(String message) {
super(message);
}
public ApiException( String code,String message) {
super(message);
this.code = code;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
/**
* 对字符串md5加密(大写+数字)
*
* @param s 传入要加密的字符串
* @return MD5加密后的字符串
*/
public static String MD5(String s) {
char hexDigits[] = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
try {
byte[] btInput = s.getBytes();
// 获得MD5摘要算法的 MessageDigest 对象
MessageDigest mdInst = MessageDigest.getInstance("MD5");
// 使用指定的字节更新摘要
mdInst.update(btInput);
// 获得密文
byte[] md = mdInst.digest();
// 把密文转换成十六进制的字符串形式
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
String md5 = MD5("password");
}
}
import java.io.IOException;
import java.util.Map;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
public class UtilJson {
public static final ObjectMapper JSON_MAPPER = newObjectMapper(), JSON_MAPPER_WEB = newObjectMapper();
private static ObjectMapper newObjectMapper() {
ObjectMapper result = new ObjectMapper();
result.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
result.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
result.setSerializationInclusion(Include.NON_NULL);
result.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); //不输出value=null的属性
result.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
result.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
return result;
}
public static ObjectMapper getObjectMapper() {
return JSON_MAPPER;
}
public static String writeValueAsString(Object value) {
try {
return value == null ? null : JSON_MAPPER.writeValueAsString(value);
} catch (IOException e) {
throw new IllegalArgumentException(e); // TIP: 原则上,不对异常包装,这里为什么要包装?因为正常情况不会发生IOException
}
}
@SuppressWarnings("unchecked")
public static Map<String, Object> toMap(Object value) throws IllegalArgumentException {
return convertValue(value, Map.class);
}
public static <T> T convertValue(Object value, Class<T> clazz) throws IllegalArgumentException {
if (StringUtils.isEmpty(value)) return null;
try {
if (value instanceof String)
value = JSON_MAPPER.readTree((String) value);
return JSON_MAPPER.convertValue(value, clazz);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
}
(1) RSA/DES加密
对称加密: DES AES
非对称加密: RSA
(3) TOKEN令牌
史上最全的并发编程脑图:https://www.processon.com/view/5d43e6cee4b0e47199351b7f