`
mhtbbx
  • 浏览: 36390 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

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

 
阅读更多

利用缓存实现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 }
分享到:
评论

相关推荐

    android app与服务器json交互

    本篇将详细阐述Android App如何使用JSON与服务器进行交互,以及相关的实现技术和注意事项。 首先,理解JSON(JavaScript Object Notation)的基本结构至关重要。JSON是一种文本格式,易于人阅读和编写,同时也易于...

    Android客户端与服务器端交互

    在Android应用开发中,客户端与服务器端的交互是不可或缺的一部分,尤其对于涉及到用户数据存储、网络请求、实时更新等功能的应用更是如此。本知识点主要聚焦于Android客户端如何与服务器端进行有效的通信,通过IDEA...

    Android服务器端交互服务器端源码

    在Android应用开发中,服务器端交互是至关重要的一个环节,它涉及到客户端与服务器之间的数据交换,使得用户可以在移动设备上实现各种功能,如登录、注册、数据同步等。本资源提供的"Android服务器端交互服务器端...

    Android 客户端与服务器端进行数据交互Demo(包含服务器端和客户端)

    在Android开发中,客户端与服务器端的数据交互是应用程序的核心功能之一。这个Demo涵盖了从客户端到服务器端的基本数据发送和接收,以及验证过程。下面将详细解释这个Demo涉及到的知识点。 1. **Android网络请求库*...

    session简介.doc

    与cookie不同,session存储在服务器端,减少了暴露用户信息的风险。服务器为每个用户创建一个唯一的session ID,并将其发送给客户端。之后,客户端在每次请求时通过cookie将session ID返回给服务器,服务器根据ID...

    jsp(服务器)与android(客户端)交互

    在IT领域,特别是移动应用开发和Web开发中,实现服务器端与客户端的交互是一项核心技能。本文将深入探讨如何在JSP(Java Server Pages)作为服务器端与Android作为客户端之间进行有效的数据通信,涵盖从环境搭建到...

    软件工程,客户端与服务器端交互

    在软件工程领域,客户端与服务器端交互是构建网络应用程序的核心组成部分。这种交互模式通常被称为“客户端-服务器”架构,它是互联网应用的基础。在这个架构中,客户端(通常是用户使用的设备,如电脑、手机或平板...

    app与javaweb 交互数据

    总结,App与Java Web之间的数据交互是通过HTTP协议进行的,数据格式通常为JSON,客户端使用HTTP客户端库发送请求并解析响应,服务器端则处理请求并返回JSON数据。整个过程涉及网络通信、数据序列化、服务器端业务...

    APP架构之后端接口设计方案之初探.zip

    在移动应用开发中,后端接口设计是连接前端与服务器端的重要桥梁,它定义了APP如何与服务端进行数据交互。本篇文章将深入探讨APP架构之后端接口设计方案,旨在为开发者提供一个清晰的设计思路和实践指导。 首先,...

    商城app客户端+服务器端.zip

    本开源项目“商城app客户端+服务器端.zip”提供了完整的Android商城项目的源代码,旨在帮助开发者进行二次开发,进一步理解和实践客户端与服务器端的交互逻辑。下面将详细阐述该项目中的关键知识点。 首先,我们...

    Andorid APP与JS交互demo

    总的来说,"Android APP与JS交互demo"是一个实用的例子,它帮助开发者理解如何在Android应用中实现原生与JS的双向通信,从而在混合式应用开发中充分利用两者的优势。通过学习这个示例,你可以更好地掌握如何在...

    Android通过get,post方式客户端与服务器端交互实例

    总结,Android客户端与服务器端交互涉及HTTP协议中的GET和POST方法,可以通过原生的HttpURLConnection或者第三方库如OkHttp实现。GET用于获取资源,POST则用于提交数据。在实际应用中,还需要考虑安全性和性能优化。...

    webapi接口缓存组件

    本文将详细介绍这个自定义的WebAPI接口缓存组件的设计原理、实现方式及其优势。 一、设计原理 WebAPI接口缓存的核心思想是基于请求-响应模式,当一个请求被发送到API接口时,如果该请求的参数与之前某次请求完全...

    跨服务器session应用详解

    与cookie相比,session在服务器端存储用户信息,降低了数据暴露的风险。服务器为每个用户分配一个唯一的session ID,并将其通过cookie发送给客户端。客户端在后续请求中携带这个ID,服务器根据ID查找对应的session...

    小京东app服务器端源码.rar_app源码_ecshop app_小京东_小京东 app_小京东APP

    首先,服务器端源码通常包括数据库交互、业务逻辑处理、API接口设计以及安全性控制等多个部分。小京东APP作为电商应用,其服务器端的核心功能可能包括: 1. **用户管理**:用户注册、登录、权限控制、密码安全等。...

    DWR3实现服务器端向客户端精确推送消息

    在“DWR3实现服务器端向客户端精确推送消息”这一主题中,我们将深入探讨如何利用DWR3进行服务器到客户端的消息推送,以及这种技术的优势和应用。 首先,理解DWR3的工作原理是至关重要的。DWR3通过建立一个安全的...

    安卓app与后台交互源码

    总的来说,"安卓app与后台交互源码"这个资源涵盖了安卓应用开发中的网络请求、数据序列化、服务器端的API设计、数据处理以及安全策略等多个核心知识点。通过深入学习和理解这些内容,开发者可以更好地构建出功能丰富...

    redis缓存服务器Nginx+Tomcat+redis+MySQL实现session会话共享

    "redis缓存服务器Nginx+Tomcat+redis+MySQL实现session会话共享"的主题旨在探讨如何利用这些技术组件来实现这一目标。以下是相关知识点的详细说明: **Redis**:Redis是一个高性能的键值数据存储系统,常用于做缓存...

    Android平台手机访问Web服务器的有效数据交互方法

    在Android与Web服务器的交互中,服务器端往往采用SSH架构来处理请求和响应,其中Servlet作为核心组件,负责接收客户端的请求,并将处理结果返回给客户端。Servlet可以根据不同的请求类型(如GET或POST)执行相应的...

    APP接口开发例子

    在IT行业中,APP接口开发是构建移动应用与服务器端数据交互的重要环节。接口如同一座桥梁,使得客户端(APP)能够发送请求,获取所需数据或执行特定操作,而服务器则通过响应这些请求来提供服务。本篇文章将深入探讨...

Global site tag (gtag.js) - Google Analytics