キャッシュによるAPP側とサーバインタフェースの相互作用を実現するSession制御

18561 ワード

従来のB/SモードのWebシステムとは異なり、モバイル端末APPとサーバ間のインタフェースのインタラクションは一般的にC/Sモードである.この場合、ユーザ登録にかかわると、WebシステムのようにWebコンテナに依存してSessionを管理することはできない.APPは要求するたびにサーバ側に新しいSessionを作成するからである.ユーザーのプライバシーや資金取引に関連するインタフェースの中には、現在のユーザーログインの合法性を確認する必要があるものもあります.ログインがないか、ログインが期限切れになっている場合は、このような操作はできません.ユーザーが初めてログインした後、ユーザーのIDをローカルストレージに保存し、サーバとインタラクティブなインタフェースがユーザーIDでユーザーIDを識別する「サボる」方法を見たことがあります.
この方法には主に2つの弊害があります.
  • ローカルに記憶されているユーザIDが削除されていない限り、常に以上のインタフェースにアクセスすることができ、有効期間の判断を増加したり、ユーザが自発的に終了したりしない限り、再ログインする必要はありません.
  • インタフェースはセキュリティが弱い.ユーザーIDはデータベース内のユーザー固有の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が実現されます.実現の構想は次のとおりです.
  • ユーザーがログインに成功した後、サーバー側は一定の規則に従ってTokenトークンを生成し、Tokenは可変であり、固定であってもよい(後述);
  • Tokenをkeyとし、ユーザー情報をvalueとしてキャッシュに入れ、有効時間を設定する(例えば30分以内にアクセスしないと失効する);
  • TokenをAPP側に戻し、APPをローカルストレージに保存してインタフェースを要求するときにこのパラメータを持参する.
  • は、ユーザのプライバシーセキュリティなどに関連するすべてのインタフェースをブロックすることによって、要求中のTokenパラメータの正当性を検証し、キャッシュが期限切れであるかどうかを確認する.
  • 検証が通過した後、現在のスレッドの操作がキャッシュから現在ログインしているユーザ情報を直接インデックスできるように、Token値をスレッド記憶に保存する.

  • 以上のように、アプリ側がすべきことは、ログインしてサーバ側からTokenストレージを取得し、ユーザーのプライバシーに関するインタフェースにアクセスするときにこのTokenを持って自分のアイデンティティを識別することです.サーバ側が行うべきことは,ユーザのプライバシーに関するインタフェースをブロックしてTokenとログイン情報を検証し,検証後にスレッド変数にTokenを保存した後,他の操作でこのTokenを取り出してキャッシュから現在のユーザ情報を取得することである.このようにAPPはユーザーIDを知る必要はなく、取得したのはアイデンティティIDだけで、このIDは可変で、サーバはこのIDに基づいて操作するユーザーを知ることができます.
    Tokenが可変かどうかについては,処理の詳細が異なり,効果も異なる.
  • Tokenが固定されている場合:サーバ側がTokenを生成する際にユーザ名とパスワードを一緒にMD 5(username+password)を暗号化する.これにより、同じユーザにとって、ログインするたびにTokenは同じであり、ユーザは複数のクライアントにログインし、1つのセッションを共有することができ、ユーザのパスワードが変更されたときにユーザに再ログインを要求することができる.
  • Tokenが可変の場合:サーバ側がTokenを生成すると、ユーザ名、パスワードと現在のタイムスタンプとともにMD 5が暗号化されます.すなわち、MD 5(username+password+timestamp).これにより,同じユーザに対してログイン毎のTokenが異なり,前回ログインしたキャッシュ情報をクリアすることで,一意のユーザログインの効果を実現できる.

  • 同じユーザがキャッシュに1つのログイン情報しかないことを保証するために、サーバ側はTokenを生成した後、ユーザ名に対してMD 5をSeed、すなわちMD 5(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プロトコルに変更できます.