利用缓存实现APP端与服务器接口交互的Session控制
与传统B/S模式的Web系统不同,移动端APP与服务器之间的接口交互一般是C/S模式,这种情况下如果涉及到用户登录的话,就不能像Web系统那样依赖于Web容器来管理Session了,因为APP每发一次请求都会在服务器端创建一个新的Session。而有些涉及到用户隐私或者资金交易的接口又必须确认当前用户登录的合法性,如果没有登录或者登录已过期则不能进行此类操作。
我见过一种“偷懒”的方式,就是在用户第一次登录之后,保存用户的ID在本地存储中,之后跟服务器交互的接口都通过用户ID来标识用户身份。
这种方式主要有两个弊端:
- 只要本地存储的用户ID没有被删掉,就始终可以访问以上接口,不需要重新登录,除非增加有效期的判断或者用户主动退出;
- 接口安全性弱,因为用户ID对应了数据库里的用户唯一标识,别人只要能拿到用户ID或者伪造一个用户ID就可以使用以上接口对该用户进行非法操作。
综上考虑,可以利用缓存在服务器端模拟Session管理机制来解决这个问题,当然这只是目前我所知道的一种比较简单有效的解决APP用户Session的方案。如果哪位朋友有其它好的方案,欢迎在下面留言交流。
这里用的缓存框架是Ehcache,下载地址http://www.ehcache.org/downloads/,当然也可以用Memcached或者其它的。之所以用Ehcache框架,一方面因为它轻量、快速、集成简单等,另一方面它也是Hibernate中默认的CacheProvider,对于已经集成了Hibernate的项目不需要再额外添加Ehcache的jar包了。
有了Ehcache,接着就要在Spring配置文件里添加相应的配置了,配置信息如下:
<!-- 配置缓存管理器工厂 -->
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache.xml" />
<property name="shared" value="true" />
</bean>
<!-- 配置缓存工厂,缓存名称为myCache -->
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheName" value="myCache" />
<property name="cacheManager" ref="cacheManager" />
</bean>
另外,Ehcache的配置文件ehcache.xml里的配置如下:
<?xml version="1.0" encoding="gbk"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd">
<diskStore path="java.io.tmpdir" /> <!-- 配置一个默认缓存,必须的 -->
<defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="30" timeToLiveSeconds="30" overflowToDisk="false" /> <!-- 配置自定义缓存 maxElementsInMemory:缓存中允许创建的最大对象数 eternal:缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期。
timeToIdleSeconds:缓存数据的钝化时间,也就是在一个元素消亡之前, 两次访问时间的最大时间间隔值,这只能在元素不是永久驻留时有效,
如果该值是 0 就意味着元素可以停顿无穷长的时间。 timeToLiveSeconds:缓存数据的生存时间,也就是一个元素从构建到消亡的最大时间间隔值,
这只能在元素不是永久驻留时有效,如果该值是0就意味着元素可以停顿无穷长的时间。 overflowToDisk:内存不足时,是否启用磁盘缓存。 memoryStoreEvictionPolicy:缓存满了之后的淘汰算法。 -->
<cache name="myCache" maxElementsInMemory="10000" eternal="true" overflowToDisk="true" memoryStoreEvictionPolicy="LFU" />
</ehcache>
配置好Ehcache之后,就可以直接通过@Autowired或者@Resource注入缓存实例了。示例代码如下:
@Component
public class Memory {
@Autowired
private Cache ehcache; // 注意这里引入的Cache是net.sf.ehcache.Cache public void setValue(String key, String value) {
ehcache.put(new Element(key, value));
} public Object getValue(String key) {
Element element = ehcache.get(key);
return element != null ? element.getValue() : null;
}
}
缓存准备完毕,接下来就是模拟用户Session了,实现思路是这样的:
- 用户登录成功后,服务器端按照一定规则生成一个Token令牌,Token是可变的,也可以是固定的(后面会说明);
- 将Token作为key,用户信息作为value放到缓存中,设置有效时长(比如30分钟内没有访问就失效);
- 将Token返回给APP端,APP保存到本地存储中以便请求接口时带上此参数;
- 通过拦截器拦截所有涉及到用户隐私安全等方面的接口,验证请求中的Token参数合法性并检查缓存是否过期;
- 验证通过后,将Token值保存到线程存储中,以便当前线程的操作可以通过Token直接从缓存中索引当前登录的用户信息。
综上所述,APP端要做的事情就是登录并从服务器端获取Token存储起来,当访问用户隐私相关的接口时带上这个Token标识自己的身份。服务器端要做的就是拦截用户隐私相关的接口验证Token和登录信息,验证后将Token保存到线程变量里,之后可以在其它操作中取出这个Token并从缓存中获取当前用户信息。这样APP不需要知道用户ID,它拿到的只是一个身份标识,而且这个标识是可变的,服务器根据这个标识就可以知道要操作的是哪个用户。
对于Token是否可变,处理细节上有所不同,效果也不一样。
- Token固定的情况:服务器端生成Token时将用户名和密码一起进行MD5加密,即MD5(username+password)。这样对于同一个用户而言,每次登录的Token是相同的,用户可以在多个客户端登录,共用一个Session,当用户密码变更时要求用户重新登录;
- Token可变的情况:服务器端生成Token时将用户名、密码和当前时间戳一起MD5加密,即MD5(username+password+timestamp)。这样对于同一个用户而言,每次登录的Token都是不一样的,再清除上一次登录的缓存信息,即可实现唯一用户登录的效果。
为了保证同一个用户在缓存中只有一条登录信息,服务器端在生成Token后,可以再单独对用户名进行MD5作为Seed,即MD5(username)。再将Seed作为key,Token作为value保存到缓存中,这样即便Token是变化的,但每个用户的Seed是固定的,就可以通过Seed索引到Token,再通过Token清除上一次的登录信息,避免重复登录时缓存中保存过多无效的登录信息。
基于Token的Session控制部分代码如下:
@Component
public class Memory { @Autowired
private Cache ehcache; /**
* 关闭缓存管理器
*/
@PreDestroy
protected void shutdown() {
if (ehcache != null) {
ehcache.getCacheManager().shutdown();
}
} /**
* 保存当前登录用户信息
*
* @param loginUser
*/
public void saveLoginUser(LoginUser loginUser) {
// 生成seed和token值
String seed = MD5Util.getMD5Code(loginUser.getUsername());
String token = TokenProcessor.getInstance().generateToken(seed, true);
// 保存token到登录用户中
loginUser.setToken(token);
// 清空之前的登录信息
clearLoginInfoBySeed(seed);
// 保存新的token和登录信息
String timeout = getSystemValue(SystemParam.TOKEN_TIMEOUT);
int ttiExpiry = NumberUtils.toInt(timeout) * 60; // 转换成秒
ehcache.put(new Element(seed, token, false, ttiExpiry, 0));
ehcache.put(new Element(token, loginUser, false, ttiExpiry, 0));
} /**
* 获取当前线程中的用户信息
*
* @return
*/
public LoginUser currentLoginUser() {
Element element = ehcache.get(ThreadTokenHolder.getToken());
return element == null ? null : (LoginUser) element.getValue();
} /**
* 根据token检查用户是否登录
*
* @param token
* @return
*/
public boolean checkLoginInfo(String token) {
Element element = ehcache.get(token);
return element != null && (LoginUser) element.getValue() != null;
} /**
* 清空登录信息
*/
public void clearLoginInfo() {
LoginUser loginUser = currentLoginUser();
if (loginUser != null) {
// 根据登录的用户名生成seed,然后清除登录信息
String seed = MD5Util.getMD5Code(loginUser.getUsername());
clearLoginInfoBySeed(seed);
}
} /**
* 根据seed清空登录信息
*
* @param seed
*/
public void clearLoginInfoBySeed(String seed) {
// 根据seed找到对应的token
Element element = ehcache.get(seed);
if (element != null) {
// 根据token清空之前的登录信息
ehcache.remove(seed);
ehcache.remove(element.getValue());
}
}
}
Token拦截器部分代码如下:
public class TokenInterceptor extends HandlerInterceptorAdapter {
@Autowired
private Memory memory; private List<String> allowList; // 放行的URL列表 private static final PathMatcher PATH_MATCHER = new AntPathMatcher(); @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断请求的URI是否运行放行,如果不允许则校验请求的token信息
if (!checkAllowAccess(request.getRequestURI())) {
// 检查请求的token值是否为空
String token = getTokenFromRequest(request);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.setHeader("Cache-Control", "no-cache, must-revalidate");
if (StringUtils.isEmpty(token)) {
response.getWriter().write("Token不能为空");
response.getWriter().close();
return false;
}
if (!memory.checkLoginInfo(token)) {
response.getWriter().write("Session已过期,请重新登录");
response.getWriter().close();
return false;
}
ThreadTokenHolder.setToken(token); // 保存当前token,用于Controller层获取登录用户信息
}
return super.preHandle(request, response, handler);
} /**
* 检查URI是否放行
*
* @param URI
* @return 返回检查结果
*/
private boolean checkAllowAccess(String URI) {
if (!URI.startsWith("/")) {
URI = "/" + URI;
}
for (String allow : allowList) {
if (PATH_MATCHER.match(allow, URI)) {
return true;
}
}
return false;
} /**
* 从请求信息中获取token值
*
* @param request
* @return token值
*/
private String getTokenFromRequest(HttpServletRequest request) {
// 默认从header里获取token值
String token = request.getHeader(Constants.TOKEN);
if (StringUtils.isEmpty(token)) {
// 从请求信息中获取token值
token = request.getParameter(Constants.TOKEN);
}
return token;
} public List<String> getAllowList() {
return allowList;
} public void setAllowList(List<String> allowList) {
this.allowList = allowList;
}
}
到这里,已经可以在一定程度上确保接口请求的合法性,不至于让别人那么容易伪造用户信息,即便别人通过非法手段拿到了Token也只是临时的,当缓存失效后或者用户重新登录后Token一样无效。如果服务器接口安全性要求更高一些,可以换成SSL协议以防请求信息被窃取。
利用缓存实现APP端与服务器接口交互的Session控制的更多相关文章
- 原生端与服务器通过sessionid实现session共享以及登录验证
注:原生端与服务器建立连接时产生的sessionid会变,跟上一次的不一样,为了保证sessionid一样,所以第一次服务器需要把sessionid返回给原生端,下一次与服务端会话时,原生端需要把这个 ...
- web service client端调用服务器接口
打开项目的web service client 其中wsdl URL http://www.51testing.com/html/55/67755-848510.html 去这里面查找一些公开的 ...
- APP端上传图片 - php接口
$base64="iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAbRJREFUSIntlDFPFF ...
- TCP移动端跟服务器数据交互
同一台笔记本下的客户端和服务端 TCPClient 客户端: // RootViewController.h#import <UIKit/UIKit.h>#import "As ...
- springboot+layui实现PC端用户的增删改查 & 整合mui实现app端的自动登录和用户的上拉加载 & HBuilder打包app并在手机端下载安装
springboot整合web开发的各个组件在前面已经有详细的介绍,下面是用springboot整合layui实现了基本的增删改查. 同时在学习mui开发app,也就用mui实现了一个简单的自动登录和 ...
- 移动 APP 端与服务器端用户身份认证的安全方案
最近要做一个项目是java开发后端服务,然后移动APP调用.由于之前没有接触过这块,所以在网上搜索相关的方案.然后搜到下面的一些方案做一些参考. 原文:移动 APP 端与服务器端用户身份认证的安全方案 ...
- [.net 面向对象程序设计进阶] (15) 缓存(Cache)(二) 利用缓存提升程序性能
[.net 面向对象程序设计进阶] (15) 缓存(Cache)(二) 利用缓存提升程序性能 本节导读: 上节说了缓存是以空间来换取时间的技术,介绍了客户端缓存和两种常用服务器缓布,本节主要介绍一种. ...
- 【转】 App架构设计经验谈:接口的设计
App与服务器的通信接口如何设计得好,需要考虑的地方挺多的,在此根据我的一些经验做一些总结分享,旨在抛砖引玉. 安全机制的设计 现在,大部分App的接口都采用RESTful架构,RESTFul最重要的 ...
- App架构设计经验谈:接口的设计
App与服务器的通信接口如何设计得好,需要考虑的地方挺多的,在此根据我的一些经验做一些总结分享,旨在抛砖引玉. 安全机制的设计 现在,大部分App的接口都采用RESTful架构,RESTFul最重要的 ...
随机推荐
- SharePoint 2013 搭建app本地开发环境
使用SharePoint App,如果要通过应用程序目录分发 SharePoint 相关应用程序,如具有完全控制权限的 SharePoint 相关应用程序(无法部署到 Office 365 网站),则 ...
- 列表屏幕(List Screen)
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- SuperMap iClient 7C——网络客户端GIS开发平台 产品新特性
SuperMap iClient 7C是空间信息和服务的可视化交互开发平台,是SuperMap服务器系列产品的统一客户端.产品基于统一的架构体系,面向Web端和移动端提供了多种类型的SDK开发包,帮助 ...
- [转]Design Pattern Interview Questions - Part 1
Factory, Abstract factory, prototype pattern (B) What are design patterns? (A) Can you explain facto ...
- Intent属性详解一 component属性
先看效果图 概述 在介绍Component之前,我们首先来了解ComponentName这个类:ComponentName与Intent同位于android.content包下,我们从Android官 ...
- 长链接转换成短链接(iOS版本)
首先需要将字符串使用md5加密,添加NSString的md5的类别方法如下 .h文件 #import <CommonCrypto/CommonDigest.h> @interface NS ...
- iOS 直播-获取音频(视频)数据
iOS 直播-获取音频(视频)数据 // // ViewController.m // capture-test // // Created by caoxu on 16/6/3. // Copyri ...
- iOS开发之功能模块--本地序列化
下面只展示项目开发中,本地序列化的示例代码: AuthenticationManager.h #import <Foundation/Foundation.h> #import " ...
- python文件读写操作与linux shell变量命令交互执行
python对文件的读写还是挺方便的,与linux shell的交互变量需要转换一下才能用,这比较头疼! #coding=utf-8 #!/usr/bin/python import os impor ...
- c++ 奇特的递归模板模式(CRTP)
概述 使用派生类作为模板参数特化基类. 与多态的区别 多态是动态绑定(运行时绑定),CRTP是静态绑定(编译时绑定) 在实现多态时,需要重写虚函数,因而这是运行时绑定的操作. CRTP在编译期确定通过 ...