核心引擎搞定了,接下来的主要工作就是逐个开发 Handler 了。
常用的Handler包括授权(AuthHandler)、流量控制(TrafficControlHandler)、加解密(EncryptHandler)、安全(SecurityHandler)、压缩(ZipHandler)、序列化(KryoHandler)等。
其他外围功能还包括对调用方的管理功能,开放接口介绍网站等,不再冗述。
实现一个达到实用级别的授权实现需要另开一个专题来讲,这里只讲一些基本原则,并提供一个简单的实现。
每次调用接口都传递调用方id和密码,是最简单粗暴的做法。使用定时失效的token向前走了一步。
Token 可以基于 HttpSession 实现,这种实现同Servlet API极其机密地耦合了。
我们完全可以自己实现类似Session的机制,采用 Redis 等分布式缓存中间件来实现,还自动具备了分布式属性。
代码极其简单,如下所示。
/**
* @author liuhailong2008#foxmail
*/
public class ApiSession implements Serializable {
private static final long serialVersionUID = 1055965810150154404L;
/**Session ID*/
private final String id;
/**Session创建时间*/
private long creationTime;
/**Session最后一次访问时间*/
private long lastAccessedTime;
/**Session的最大空闲时间间隔*/
private int maxInactiveInterval;
/**是否是新建Session*/
private boolean newSession;
private static final String SESSION_KEY_PREFIX = "SESS_";
//private Set attrNameSet = Collections.synchronizedSet(new HashSet());
private final String sessionKey ;
/**
* 创建新的Session。
* @param maxIdleSeconds
*/
public ApiSession(int maxIdleSeconds){
id = StringUtil.getUUID();
long now = System.currentTimeMillis();
creationTime = now;
lastAccessedTime = now;
this.maxInactiveInterval = maxIdleSeconds;
newSession = true;
//this.attrNameSet.clear();
sessionKey = SESSION_KEY_PREFIX + id;
CacheBlock cb = CacheManager.getBlock(Const.CACHE_BLOCK_INDEX);
CacheElement ce = new CacheElement(sessionKey,this);
ce.setTimeToIdleSeconds(this.getMaxInactiveInterval());
cb.put(ce);
}
/**
* 通过Session id获取已经存在的Session,如果没有,返回null。
* @return
*/
public static ApiSession get(String id){
String sessionKey = SESSION_KEY_PREFIX + id;
CacheBlock cb = CacheManager.getBlock(Const.CACHE_BLOCK_INDEX);
ApiSession ret = (ApiSession) cb.get(sessionKey);
if(ret!=null){
ret.newSession = false;
ret.refresh();
}
return ret;
}
/**
* 更新 lastAccessedTime 。
*/
public void refresh() {
this.lastAccessedTime = System.currentTimeMillis();
CacheBlock cb = CacheManager.getBlock(Const.CACHE_BLOCK_INDEX);
CacheElement ce = new CacheElement(sessionKey,this);
ce.setTimeToIdleSeconds(this.getMaxInactiveInterval());
cb.put(ce);
}
/**
* 是否超时过期。
*
* @param session
* @return
*/
public boolean isExpired() {
CacheBlock cb = CacheManager.getBlock(Const.CACHE_BLOCK_INDEX);
ApiSession _this = (ApiSession) cb.get(this.sessionKey);
// 先查看缓存层面的超时控制
if(_this==null){
return false;
}
long now = System.currentTimeMillis();
long last = this.getLastAccessedTime();
long interal = now - last;
if(interal>this.getMaxInactiveInterval()){
this.invalidate();
return true;
}else{
return false;
}
}
/**
* 强制Session立即失效。
*/
public synchronized void invalidate() {
CacheBlock cb = CacheManager.getBlock(Const.CACHE_BLOCK_INDEX);
cb.remove(this.sessionKey);
}
/**
* 移除属性。
*
* @param attrName
* @return
*/
public synchronized Object removeAttribute(String attrName){
this.refresh();
String attrSessionKey = getAttrSessionKey(attrName);
CacheBlock cb = CacheManager.getBlock(Const.CACHE_BLOCK_INDEX);
Object ret = cb.remove(attrSessionKey);
return ret;
}
/**
* 设置属性。
* @param attrName
* @param attrValue
*/
public synchronized void setAttribute(String attrName,Object attrValue){
this.refresh();
String attrSessionKey = getAttrSessionKey(attrName);
CacheBlock cb = CacheManager.getBlock(Const.CACHE_BLOCK_INDEX);
CacheElement ce = new CacheElement(attrSessionKey,attrValue);
ce.setTimeToIdleSeconds(this.getMaxInactiveInterval());
cb.put(ce);
}
/**
* 获取属性的值。
* @param attrName
* @return
*/
public Object getAttribute(String attrName){
this.refresh();
String attrSessionKey = getAttrSessionKey(attrName);
CacheBlock cb = CacheManager.getBlock(Const.CACHE_BLOCK_INDEX);
Object retObject = cb.get(attrSessionKey);
return retObject;
}
private String getAttrSessionKey(String attrName){
String attrSessionKey = sessionKey + attrName;
return attrSessionKey;
}
public int getMaxInactiveInterval() {
if(maxInactiveInterval==-1){
maxInactiveInterval = 3600;
}
return maxInactiveInterval;
}
public void setMaxInactiveInterval(int maxInactiveInterval) {
this.maxInactiveInterval = maxInactiveInterval;
}
public String getId() {
return id;
}
public long getCreationTime() {
return creationTime;
}
public long getLastAccessedTime() {
return lastAccessedTime;
}
public boolean isNewSession() {
return newSession;
}
}
调用方发送加密的用户名、密码消息,调用 getToken 接口。校验通过后,服务器返回 Token 。
调用方使用临时的 Token 访问其他接口。空闲固定时间间隔后,Token失效。
简单来说,Token 可以理解为 Session Id。
可以提供3种流量控制方法,以减轻服务器压力。
避免大批量并发形成波峰。
long latestTime = parseLong(ApiCacheBlock.get(latestTimeCacheKey));//上次调用时间
long intervalLimit = (apiOrgBO.getIntervalLimit()==null)?0L:apiOrgBO.getIntervalLimit().longValue();
if(intervalLimit>0){// 需要执行时间间隔限制
// 触发事件间隔限制
if(now - latestTime < intervalLimit){
throw new ApiException(String.format("连续两次调用之间的时间间隔不能小于%d毫秒。",intervalLimit));
}
}
以下代码只是范例,需要根据实际服务器负载情况调整。
String timeString = timeFormat.format(nowDate);
String timeRangeLimitStart = StringUtil.safe2String(apiOrgBO.getTimeRangeLimitStart());
String timeRangeLimitEnd = StringUtil.safe2String(apiOrgBO.getTimeRangeLimitEnd());
if(!StringUtils.isEmpty(timeRangeLimitStart)){
if(timeString!=null && timeString.compareTo(timeRangeLimitStart)<0){
throw new ApiException(String.format("系统不早于%s提供服务。",timeRangeLimitStart));
}
}
if(!StringUtils.isEmpty(timeRangeLimitEnd)){
if(timeString!=null && timeString.compareTo(timeRangeLimitStart)>0){
throw new ApiException(String.format("系统不晚于%s提供服务。",timeRangeLimitEnd));
}
}
计数器自增操作的原子性依赖于 Redis 的 INCR 命令。
long cntInDay = cntInDayPlus(latestDate,dateString,cntInDayCacheKey);
long dayCntLimit = (apiOrgBO.getDayCntLimit()==null)?0L:apiOrgBO.getDayCntLimit().longValue();
if(dayCntLimit>0){
if(cntInDay>dayCntLimit){
throw new ApiException(String.format("同一天内调用接口不能超过%d次。",dayCntLimit));
}
}
未尽事宜,欢迎留言探讨。