利用缓存实现APP端与服务器接口交互的Session控制

与传统B/S模式的Web系统不同,移动端APP与服务器之间的接口交互一般是C/S模式,这种情况下如果涉及到用户登录的话,就不能像Web系统那样依赖于Web容器来管理Session了,因为APP每发一次请求都会在服务器端创建一个新的Session。而有些涉及到用户隐私或者资金交易的接口又必须确认当前用户登录的合法性,如果没有登录或者登录已过期则不能进行此类操作。
我见过一种“偷懒”的方式,就是在用户第一次登录之后,保存用户的ID在本地存储中,之后跟服务器交互的接口都通过用户ID来标识用户身份。

这种方式主要有两个弊端:

  1. 只要本地存储的用户ID没有被删掉,就始终可以访问以上接口,不需要重新登录,除非增加有效期的判断或者用户主动退出;
  2. 接口安全性弱,因为用户ID对应了数据库里的用户唯一标识,别人只要能拿到用户ID或者伪造一个用户ID就可以使用以上接口对该用户进行非法操作。

综上考虑,可以利用缓存在服务器端模拟Session管理机制来解决这个问题,当然这只是目前我所知道的一种比较简单有效的解决APP用户Session的方案。如果哪位朋友有其它好的方案,欢迎在下面留言交流。

这里用的缓存框架是Ehcache,下载地址http://www.ehcache.org/downloads/,当然也可以用Memcached或者其它的。之所以用Ehcache框架,一方面因为它轻量、快速、集成简单等,另一方面它也是Hibernate中默认的CacheProvider,对于已经集成了Hibernate的项目不需要再额外添加Ehcache的jar包了。

有了Ehcache,接着就要在Spring配置文件里添加相应的配置了,配置信息如下:

 1 <!-- 配置缓存管理器工厂 -->
 2 <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
 3     <property name="configLocation" value="classpath:ehcache.xml" />
 4     <property name="shared" value="true" />
 5 </bean>
 6 <!-- 配置缓存工厂,缓存名称为myCache -->
 7 <bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
 8     <property name="cacheName" value="myCache" />
 9     <property name="cacheManager" ref="cacheManager" />
10 </bean>

另外,Ehcache的配置文件ehcache.xml里的配置如下:

 1 <?xml version="1.0" encoding="gbk"?>
 2 <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3     xsi:noNamespaceSchemaLocation="ehcache.xsd">
 4     <diskStore path="java.io.tmpdir" />
 5
 6     <!-- 配置一个默认缓存,必须的 -->
 7     <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="30" timeToLiveSeconds="30" overflowToDisk="false" />
 8
 9     <!-- 配置自定义缓存 maxElementsInMemory:缓存中允许创建的最大对象数 eternal:缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期。
10         timeToIdleSeconds:缓存数据的钝化时间,也就是在一个元素消亡之前, 两次访问时间的最大时间间隔值,这只能在元素不是永久驻留时有效,
11         如果该值是 0 就意味着元素可以停顿无穷长的时间。 timeToLiveSeconds:缓存数据的生存时间,也就是一个元素从构建到消亡的最大时间间隔值,
12         这只能在元素不是永久驻留时有效,如果该值是0就意味着元素可以停顿无穷长的时间。 overflowToDisk:内存不足时,是否启用磁盘缓存。 memoryStoreEvictionPolicy:缓存满了之后的淘汰算法。 -->
13     <cache name="myCache" maxElementsInMemory="10000" eternal="true" overflowToDisk="true" memoryStoreEvictionPolicy="LFU" />
14 </ehcache>

配置好Ehcache之后,就可以直接通过@Autowired或者@Resource注入缓存实例了。示例代码如下:

 1 @Component
 2 public class Memory {
 3     @Autowired
 4     private Cache ehcache; // 注意这里引入的Cache是net.sf.ehcache.Cache
 5
 6     public void setValue(String key, String value) {
 7         ehcache.put(new Element(key, value));
 8     }
 9
10     public Object getValue(String key) {
11         Element element = ehcache.get(key);
12         return element != null ? element.getValue() : null;
13     }
14 }

缓存准备完毕,接下来就是模拟用户Session了,实现思路是这样的:

  1. 用户登录成功后,服务器端按照一定规则生成一个Token令牌,Token是可变的,也可以是固定的(后面会说明);
  2. 将Token作为key,用户信息作为value放到缓存中,设置有效时长(比如30分钟内没有访问就失效);
  3. 将Token返回给APP端,APP保存到本地存储中以便请求接口时带上此参数;
  4. 通过拦截器拦截所有涉及到用户隐私安全等方面的接口,验证请求中的Token参数合法性并检查缓存是否过期;
  5. 验证通过后,将Token值保存到线程存储中,以便当前线程的操作可以通过Token直接从缓存中索引当前登录的用户信息。

综上所述,APP端要做的事情就是登录并从服务器端获取Token存储起来,当访问用户隐私相关的接口时带上这个Token标识自己的身份。服务器端要做的就是拦截用户隐私相关的接口验证Token和登录信息,验证后将Token保存到线程变量里,之后可以在其它操作中取出这个Token并从缓存中获取当前用户信息。这样APP不需要知道用户ID,它拿到的只是一个身份标识,而且这个标识是可变的,服务器根据这个标识就可以知道要操作的是哪个用户。

对于Token是否可变,处理细节上有所不同,效果也不一样。

  1. Token固定的情况:服务器端生成Token时将用户名和密码一起进行MD5加密,即MD5(username+password)。这样对于同一个用户而言,每次登录的Token是相同的,用户可以在多个客户端登录,共用一个Session,当用户密码变更时要求用户重新登录;
  2. Token可变的情况:服务器端生成Token时将用户名、密码和当前时间戳一起MD5加密,即MD5(username+password+timestamp)。这样对于同一个用户而言,每次登录的Token都是不一样的,再清除上一次登录的缓存信息,即可实现唯一用户登录的效果。

为了保证同一个用户在缓存中只有一条登录信息,服务器端在生成Token后,可以再单独对用户名进行MD5作为Seed,即MD5(username)。再将Seed作为key,Token作为value保存到缓存中,这样即便Token是变化的,但每个用户的Seed是固定的,就可以通过Seed索引到Token,再通过Token清除上一次的登录信息,避免重复登录时缓存中保存过多无效的登录信息。

基于Token的Session控制部分代码如下:

 1 @Component
 2 public class Memory {
 3
 4     @Autowired
 5     private Cache ehcache;
 6
 7     /**
 8      * 关闭缓存管理器
 9      */
10     @PreDestroy
11     protected void shutdown() {
12         if (ehcache != null) {
13             ehcache.getCacheManager().shutdown();
14         }
15     }
16
17     /**
18      * 保存当前登录用户信息
19      *
20      * @param loginUser
21      */
22     public void saveLoginUser(LoginUser loginUser) {
23         // 生成seed和token值
24         String seed = MD5Util.getMD5Code(loginUser.getUsername());
25         String token = TokenProcessor.getInstance().generateToken(seed, true);
26         // 保存token到登录用户中
27         loginUser.setToken(token);
28         // 清空之前的登录信息
29         clearLoginInfoBySeed(seed);
30         // 保存新的token和登录信息
31         String timeout = getSystemValue(SystemParam.TOKEN_TIMEOUT);
32         int ttiExpiry = NumberUtils.toInt(timeout) * 60; // 转换成秒
33         ehcache.put(new Element(seed, token, false, ttiExpiry, 0));
34         ehcache.put(new Element(token, loginUser, false, ttiExpiry, 0));
35     }
36
37     /**
38      * 获取当前线程中的用户信息
39      *
40      * @return
41      */
42     public LoginUser currentLoginUser() {
43         Element element = ehcache.get(ThreadTokenHolder.getToken());
44         return element == null ? null : (LoginUser) element.getValue();
45     }
46
47     /**
48      * 根据token检查用户是否登录
49      *
50      * @param token
51      * @return
52      */
53     public boolean checkLoginInfo(String token) {
54         Element element = ehcache.get(token);
55         return element != null && (LoginUser) element.getValue() != null;
56     }
57
58     /**
59      * 清空登录信息
60      */
61     public void clearLoginInfo() {
62         LoginUser loginUser = currentLoginUser();
63         if (loginUser != null) {
64             // 根据登录的用户名生成seed,然后清除登录信息
65             String seed = MD5Util.getMD5Code(loginUser.getUsername());
66             clearLoginInfoBySeed(seed);
67         }
68     }
69
70     /**
71      * 根据seed清空登录信息
72      *
73      * @param seed
74      */
75     public void clearLoginInfoBySeed(String seed) {
76         // 根据seed找到对应的token
77         Element element = ehcache.get(seed);
78         if (element != null) {
79             // 根据token清空之前的登录信息
80             ehcache.remove(seed);
81             ehcache.remove(element.getValue());
82         }
83     }
84 }

Token拦截器部分代码如下:

 1 public class TokenInterceptor extends HandlerInterceptorAdapter {
 2     @Autowired
 3     private Memory memory;
 4
 5     private List<String> allowList; // 放行的URL列表
 6
 7     private static final PathMatcher PATH_MATCHER = new AntPathMatcher();
 8
 9     @Override
10     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
11         // 判断请求的URI是否运行放行,如果不允许则校验请求的token信息
12         if (!checkAllowAccess(request.getRequestURI())) {
13             // 检查请求的token值是否为空
14             String token = getTokenFromRequest(request);
15             response.setContentType(MediaType.APPLICATION_JSON_VALUE);
16             response.setCharacterEncoding("UTF-8");
17             response.setHeader("Cache-Control", "no-cache, must-revalidate");
18             if (StringUtils.isEmpty(token)) {
19                 response.getWriter().write("Token不能为空");
20                 response.getWriter().close();
21                 return false;
22             }
23             if (!memory.checkLoginInfo(token)) {
24                 response.getWriter().write("Session已过期,请重新登录");
25                 response.getWriter().close();
26                 return false;
27             }
28             ThreadTokenHolder.setToken(token); // 保存当前token,用于Controller层获取登录用户信息
29         }
30         return super.preHandle(request, response, handler);
31     }
32
33     /**
34      * 检查URI是否放行
35      *
36      * @param URI
37      * @return 返回检查结果
38      */
39     private boolean checkAllowAccess(String URI) {
40         if (!URI.startsWith("/")) {
41             URI = "/" + URI;
42         }
43         for (String allow : allowList) {
44             if (PATH_MATCHER.match(allow, URI)) {
45                 return true;
46             }
47         }
48         return false;
49     }
50
51     /**
52      * 从请求信息中获取token值
53      *
54      * @param request
55      * @return token值
56      */
57     private String getTokenFromRequest(HttpServletRequest request) {
58         // 默认从header里获取token值
59         String token = request.getHeader(Constants.TOKEN);
60         if (StringUtils.isEmpty(token)) {
61             // 从请求信息中获取token值
62             token = request.getParameter(Constants.TOKEN);
63         }
64         return token;
65     }
66
67     public List<String> getAllowList() {
68         return allowList;
69     }
70
71     public void setAllowList(List<String> allowList) {
72         this.allowList = allowList;
73     }
74 }

到这里,已经可以在一定程度上确保接口请求的合法性,不至于让别人那么容易伪造用户信息,即便别人通过非法手段拿到了Token也只是临时的,当缓存失效后或者用户重新登录后Token一样无效。如果服务器接口安全性要求更高一些,可以换成SSL协议以防请求信息被窃取。

时间: 2024-10-24 14:56:41

利用缓存实现APP端与服务器接口交互的Session控制的相关文章

《操作系统安全》-利用缓存的网络凭据入侵服务器

#被入侵B 入侵名y 入侵密码123#查计算机名称 hostname#更改密码 net user administrator xxx#创建用户 net usr xxxxx(名) xxx(密码) /add#删除缓存net use \\(名) del #在别人电脑登录自己的账号 win+R,然后 \\名 #测试工具 : win7+2008服务器 网络要在同一网段 #<比如对方(A)在你的电脑登录了他(B)的自己的电脑账号,这样你的电脑A就有电脑B的缓存>所以在有对方B的缓存的前提下想在对方电脑建立

TCP移动端跟服务器数据交互

同一台笔记本下的客户端和服务端 TCPClient 客户端: //  RootViewController.h#import <UIKit/UIKit.h>#import "AsyncSocket.h"  //封装了基于tcp协议的socket编程//tcp协议是位于网络传输层的协议,规定客户端与服务端之间.或者是客户端与客户端之间数据通信的方式//每个客户端或者服务端通过ip地址+端口来标识/*客户端与服务端基于tcp协议进行数据通信 *1.客户端需要通过ip+端口连接指

app与php后台接口登录认证、验证(seesion和token)

简要:随着电商的不断发展,APP也层次不穷,随着科技的发展主要登录形式(微信.QQ.账号/密码):为此向大家分享一下"app与php后台接口登录认证.验证"想法和做法:希望能够帮助困惑的伙伴们,如果有不对或者好的建议告知下:*~*!  一.登录机制 粗略分析:登录可分为三个阶段(登录验证.登录持续.退出登录):登录验证指客户端提供账号/密码(或第三方平台(微信.qq)获取openid/unionid)向服务器提出登录请求,服务器应答请求判断能否登录并返回相应数据:登录持续指客户端登录后

[.net]手机APP与IIS服务器联调配置

前端时间写过一段时间接口,在后期的时候,出现了一些无法通过查看日志来找出问题所在的bug.于是,将手机APP连接到IIS服务器上进行调试,下面是配置的具体步骤 1. 配置IIS  添加网站,将物理路径配置为你项目的文件路径,IP地址设置为APP调用API的地址,其他的默认. 2.配置Visual Studio 选中项目,右键选择属性,并在弹出的页面中选择Web:   关键的一步,将原来默认的IISExpress修改为本地IIS, 并修改项目URL为上一步的IP地址,点击创建虚拟目录: 保存设置.

[.net 面向对象程序设计进阶] (15) 缓存(Cache)(二) 利用缓存提升程序性能

[.net 面向对象程序设计进阶] (15) 缓存(Cache)(二) 利用缓存提升程序性能 本节导读: 上节说了缓存是以空间来换取时间的技术,介绍了客户端缓存和两种常用服务器缓布,本节主要介绍一种.NET中特别重要的缓布技术Cache.利用Cache提升程序性能. 1. 缓存Cache的命名空间 .NET中对缓存有两个命名空间 命名空间1:System.Web.Caching 命名空间2:System.Runtime.Caching 引用范围:这两个命名空间,都可以在Web和非WEB应用程序中

App架构设计经验谈:接口的设计

App与服务器的通信接口如何设计得好,需要考虑的地方挺多的,在此根据我的一些经验做一些总结分享,旨在抛砖引玉. 安全机制的设计 现在,大部分App的接口都采用RESTful架构,RESTFul最重要的一个设计原则就是,客户端与服务器的交互在请求之间是无状态的,也就是说,当涉及到用户状态时,每次请求都要带上身份验证信息.实现上,大部分都采用token的认证方式,一般流程是: 用户用密码登录成功后,服务器返回token给客户端: 客户端将token保存在本地,发起后续的相关请求时,将token发回给

APP开发实战8-API接口设计

3.1接口设计 (1)需要确定APP和服务器间用什么格式传输数据,常用的有两种:XML和Json.XML文件中存在大量的描述信息,会大大增加网络传输数据:同样的内容,用Json格式,传输的数据比较少,首选Json格式. 还有一种Protocol Buffers 格式,以二进制的方式传输存储数据,网络传输数据量比Json还少,但要使用proto文件作为格式验证,各语言需要整一堆pb runtime以及对应的代码生成,增加复杂度较高,目前使用的不多. (2)需要设计Json数据的具体格式: 发送请求

私有云车牌识别支持app端、pc端、H5端接口调用

关键词 私有云车牌识别技术.服务器端车牌识别技术.云端车牌识别技术.私有化车牌识别技术.服务器端车牌识别.私有化车牌识别.私有云车牌识别. 什么叫私有云车牌识别呢? 私有云车牌识别即服务器端车牌识别,是一款基于服务器平台的车牌OCR识别服务程序,企业可以将该识别服务部署在自有服务器上(云服务器或本地服务器),部署完成后,APP端.PC客户端.web端.微信H5端等均可发送识别请求,通过Web Service接口调用该识别服务,上传车牌图像在服务器端完成识别后,返回标准XML识别结果,整个识别过程

浅谈 PHP 与手机 APP 开发(API 接口开发)

本文内容转载自:http://www.thinkphp.cn/topic/5023.html 这个帖子写给不太了解PHP与API开发的人 一.先简单回答两个问题: 1.PHP 可以开发客户端?答:不可以,因为PHP是脚本语言,是负责完成 B/S架构 或 C/S架构 的S部分,即:服务端的开发.(别去纠结 GTK.WinBinder) 2.为什么选择 PHP 作为开发服务端的首选?答:跨平台(可以运行在UNIX.LINUX.WINDOWS.Mac OS下).低消耗(PHP消耗相当少的系统资源).运