国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

利用緩存實現APP端與服務器接口交互的Session控制

2019-11-14 14:57:17
字體:
來源:轉載
供稿:網友

與傳統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     @PreDestroy11     protected void shutdown() {12         if (ehcache != null) {13             ehcache.getCacheManager().shutdown();14         }15     }16 17     /**18      * 保存當前登錄用戶信息19      * 20      * @param loginUser21      */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      * @return41      */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 token51      * @return52      */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 seed74      */75     public void clearLoginInfoBySeed(String seed) {76         // 根據seed找到對應的token77         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     @Override10     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 URI37      * @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 request55      * @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協議以防請求信息被竊取。


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 恩施市| 东莞市| 中西区| 当雄县| 正蓝旗| 中宁县| 盐源县| 上高县| 营山县| 万宁市| 梅州市| 武陟县| 常德市| 潍坊市| 阿鲁科尔沁旗| 庆城县| 阿拉尔市| 苏尼特左旗| 泸定县| 香河县| 罗田县| 化德县| 大洼县| 九龙坡区| 桐柏县| 旺苍县| 札达县| 新乡市| 彭山县| 贵溪市| 德兴市| 临江市| 安庆市| 聊城市| 克什克腾旗| 永平县| 新野县| 毕节市| 察隅县| 普格县| 临桂县|