1. 程式人生 > >分散式環境下用redis模擬session

分散式環境下用redis模擬session

首先為什麼使用redis?

因為分散式有不同伺服器的緣故,如果你安照一般方式儲存session,那麼你的session會儲存在某一臺伺服器上,如果你的下一個請求並不是訪問這臺伺服器,那麼會發生讀取不到session的情況

redis儲存的實現方案:

第一種 是使用容器拓展來實現,一般都是通過容器外掛來實現,例如基於Tomcat的tomcat-redis-session-manager,基於Jetty的jetty-session-redis等等。好處是對專案來說是透明的,無需更改程式碼,但是目前還不支援Tomcat8。個人覺得由於過於依賴容器,,一旦更換容器或者容器升級,那又得重新來過。而且程式碼並不在專案中,對於開發者的維護也是個麻煩。

第二種 是自定義會話管理的工具類,這樣的話靈活性很大,可以根據自身需求來實現,但是需要額外的開發時間

第三種是使用框架的會話管理工具,例如spring-session,shiro等,可以理解是替換了servlet那一套會話管理,不依賴容器,不用改動程式碼。如果採用spring-session的話,使用的是spring-data-redis那一套連線池,prefect,不過前提是你得用spring框架。至於shiro,那是一個十分成熟,強大易用的安全框架,學習成本比spring-session來的要多一些。

下面我們介紹一下第二種方式的實現

要注意的是為什麼前端用ajax的方式登入,因為把模擬的session資訊用存到redis後,需要在本地存入userId和token來作為使用者的標識,通過這個標識去redis裡驗證該使用者是否登入,從而獲取redis中的使用者登入資訊,但是分散式中多個系統對應多個domain,所以login模組生成的userId和token要想每個系統都用,必須每個系統都生成自己的cookie資訊,java端無法為每個系統生成一份cookie所以只能在前端用iframe的方式為每個系統生成一份cookie

下面是詳細程式碼

redis的配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mongo="http://www.springframework.org/schema/data/mongo" xmlns:p
="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/data/mongo http://www.springframework.org/schema/data/mongo/spring-mongo-1.7.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- redis快取部分 --> <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxIdle" value="${redis.maxIdle}" /> <property name="maxTotal" value="${redis.maxTotal}" /> <property name="maxWaitMillis" value="${redis.maxWaitMillis}" /> <property name="testOnBorrow" value="${redis.testOnBorrow}" /> </bean> <bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" p:host-name="${redis.host}" p:port="${redis.port}" p:password="${redis.pass}" p:pool-config-ref="poolConfig" /> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="redisConnectionFactory" /> </bean> <!--載入redis --> <bean id="redisService" class="com.maigangle.b2b.common.redis.RedisSpringServiceImpl"> <!-- 控制redis開關 --> <property name="isUse" value="true"></property> </bean> </beans>

RedisSpringServiceImpl.java

package com.maigangle.b2b.common.redis;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;

import com.alibaba.fastjson.JSON;
import com.maigangle.b2b.common.exception.CommonException;

/**
 * redis介面實現
 * 
 * @author 
 * @since 
 * @version 1.0
 *
 */
public class RedisSpringServiceImpl implements RedisSpringService {
    private static String redisCode = "utf-8";
    private boolean isUse; // redis開關

    private byte[] getBytes(String str) {
        try {
            return str.getBytes(redisCode);
        } catch (UnsupportedEncodingException e) {
            return str.getBytes();
        }
    }

    public boolean isUse() {
        return isUse;
    }

    public void setIsUse(boolean isUse) {
        this.isUse = isUse;
    }

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public void pub(String channel, String param) {
        if (!isUse)
            return;
        redisTemplate.convertAndSend(channel, param);
    }

    /**
     * @param key
     */
    public Long del(final String... keys) {
        if (!isUse)
            return null;
        try {
            long re = redisTemplate.execute(new RedisCallback<Long>() {
                public Long doInRedis(RedisConnection connection) throws DataAccessException {
                    long result = 0;
                    for (int i = 0; i < keys.length; i++) {
                        result = connection.del(keys[i].getBytes());
                        result++;
                    }
                    return result;
                }
            });
            return re;
        } catch (Exception e) {
            return null;
        }

    }

    /**
     * @param key
     * @param value
     * @param liveTime
     */
    public void set(final byte[] key, final byte[] value, final long liveTime) {
        if (!isUse)
            return;
        try {
            redisTemplate.execute(new RedisCallback<Long>() {
                public Long doInRedis(RedisConnection connection) throws DataAccessException {
                    connection.set(key, value);
                    if (liveTime > 0) {
                        connection.expire(key, liveTime);
                    }
                    return 1L;
                }
            });
        } catch (Exception e) {
            closeSwitch(e);
            e.printStackTrace();
        }
    }

    /**
     * @param key
     * @param value
     * @param liveTime
     */
    public void set(String key, String value, long liveTime) {
        this.set(getBytes(key), getBytes(value), liveTime);
    }

    /**
     * @param key
     * @param liveTime
     */
    public boolean expire(String key, long liveTime) {
        if (!isUse)
            return false;
        try {
            return redisTemplate.execute(new RedisCallback<Boolean>() {
                public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                    return connection.expire(key.getBytes(), liveTime);
                }
            });
        } catch (Exception e) {
            closeSwitch(e);
            e.printStackTrace();
            return false;
        }
    }

    /**
     * @param key
     * @param value
     */
    public void set(String key, String value) {
        this.set(key, value, 0L);
    }

    /**
     * @param key
     * @param value
     */
    public void set(byte[] key, byte[] value) {
        this.set(key, value, 0L);
    }

    /**
     * @param key
     * @param value
     */
    public void set(String key, byte[] value) {
        this.set(getBytes(key), value, 0L);
    }

    /**
     * 
     */
    @Override
    public void setOjb(String key, Object value, long time) {
        this.set(getBytes(key), getBytes(JSON.toJSONString(value)), time);
    }

    /**
     * @param key
     * @return
     */
    public String get(final String key) {
        if (!isUse)
            return null;
        try {
            return redisTemplate.execute(new RedisCallback<String>() {
                public String doInRedis(RedisConnection connection) throws DataAccessException {
                    try {
                        byte[] bytes = connection.get(getBytes(key));
                        if (bytes == null || bytes.length == 0) {
                            return null;
                        }
                        return new String(bytes, redisCode);
                    } catch (UnsupportedEncodingException e) {
                        e.printStackTrace();
                    } catch (Exception e1) {
                        e1.printStackTrace();
                        return null;
                    }
                    return "";
                }
            });
        } catch (Exception e) {
            closeSwitch(e);
            e.printStackTrace();
            return null;
        }
    }

    public byte[] get4byte(final String key) {
        if (!isUse)
            return null;
        try {
            return redisTemplate.execute(new RedisCallback<byte[]>() {
                public byte[] doInRedis(RedisConnection connection) throws DataAccessException {
                    try {
                        return connection.get(getBytes(key));
                    } catch (Exception e1) {
                        e1.printStackTrace();
                        return null;
                    }
                }
            });
        } catch (Exception e) {
            closeSwitch(e);
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public <T> T getObj(String key, Class<T> elementType) {
        String jsonString = this.get(key);
        T obj;
        obj = JSON.parseObject(jsonString, elementType);
        if (obj == null) {
            try {
                obj = elementType.newInstance();// 防止空指標異常
            } catch (InstantiationException | IllegalAccessException e) {
                throw new CommonException("get redis error:", e);
            }
        }
        return obj;
    }

    /**
     * @param pattern
     * @return
     */
    public void Setkeys(String pattern) {
        if (!isUse)
            return;
        try {
            redisTemplate.keys(pattern);
        } catch (Exception e) {
            closeSwitch(e);
            e.printStackTrace();
        }

    }

    /**
     * @param key
     * @return
     */
    public boolean exists(final String key) {
        if (!isUse)
            return false;
        try {
            return redisTemplate.execute(new RedisCallback<Boolean>() {
                public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                    return connection.exists(getBytes(key));
                }
            });
        } catch (Exception e) {
            closeSwitch(e);
            e.printStackTrace();
            return false;
        }
    }

    /**
     * @return
     */
    // public String flushDB() {
    // if(!isUse) return null;
    // return redisTemplate.execute(new RedisCallback<String>() {
    // public String doInRedis(RedisConnection connection) throws
    // DataAccessException {
    // connection.flushDb();
    // return "ok";
    // }
    // });
    // }
    public boolean flushDB() {
        if (!isUse)
            return false;
        try {
            return redisTemplate.execute(new RedisCallback<Boolean>() {
                public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                    connection.flushDb();
                    return true;
                }
            });
        } catch (Exception e) {
            closeSwitch(e);
            e.printStackTrace();
            return false;
        }
    }

    /**
     * @return
     */
    public long dbSize() {
        if (!isUse)
            return 0;
        try {
            return redisTemplate.execute(new RedisCallback<Long>() {
                public Long doInRedis(RedisConnection connection) throws DataAccessException {
                    return connection.dbSize();
                }
            });
        } catch (Exception e) {
            closeSwitch(e);
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * @return
     */
    public String ping() {
        if (!isUse)
            return null;
        try {
            return redisTemplate.execute(new RedisCallback<String>() {
                public String doInRedis(RedisConnection connection) throws DataAccessException {
                    return connection.ping();
                }
            });
        } catch (Exception e) {
            closeSwitch(e);
            e.printStackTrace();
            return null;
        }
    }

    private void closeSwitch(Exception e) {
        if (e instanceof RedisConnectionFailureException) {
            this.isUse = false;
        }
    }

    /**
     * submit check token
     * 
     * @param token
     * @return
     */
    public boolean checkToken(String token) {
        if (StringUtils.isBlank(token)) {
            return false;
        }
        Object tk = this.get(token);
        if (tk != null) {
            this.del(token);
            return true;
        }
        return false;
    }

    @Override
    public void setHm(String key, Map<String, String> map, long liveTime) {
        if (!isUse)
            return;
        final Map<byte[], byte[]> hashes = new LinkedHashMap<byte[], byte[]>(map.size());
        for (Map.Entry<String, String> entry : map.entrySet()) {
            hashes.put(getBytes(entry.getKey()), getBytes(entry.getValue()));
        }
        redisTemplate.execute(new RedisCallback<Object>() {

            public Object doInRedis(RedisConnection connection) {
                connection.hMSet(getBytes(key), hashes);
                if (liveTime > 0) {
                    connection.expire(getBytes(key), liveTime);
                }
                return null;
            }
        }, true);
    }

    @Override
    public Map<String, String> getHm(String key) {
        final byte[] rawKey = getBytes(key);
        Map<byte[], byte[]> entries = redisTemplate.execute(new RedisCallback<Map<byte[], byte[]>>() {
            public Map<byte[], byte[]> doInRedis(RedisConnection connection) {
                return connection.hGetAll(rawKey);
            }
        }, true);
        Map<String, String> map = new LinkedHashMap<String, String>(entries.size());
        for (Map.Entry<byte[], byte[]> entry : entries.entrySet()) {
            try {
                map.put(new String(entry.getKey(), redisCode), new String(entry.getValue(), redisCode));
            } catch (UnsupportedEncodingException e) {
                return new HashMap<String, String>();
            }
        }
        return map;
    }

    @Override
    public void setList(String key, List<String> list, long liveTime) {
        if (!isUse)
            return;
        final List<byte[]> listes = new ArrayList<byte[]>(list.size());
        for (String value : list) {
            listes.add(getBytes(value));
        }
        redisTemplate.execute(new RedisCallback<Object>() {
            public Object doInRedis(RedisConnection connection) {
                for (byte[] bs : listes) {
                    connection.rPush(getBytes(key), bs);
                }
                if (liveTime > 0) {
                    connection.expire(getBytes(key), liveTime);
                }
                return null;
            }
        }, true);
    }

    @Override
    public List<String> getList(String key) {
        final byte[] rawKey = getBytes(key);
        List<byte[]> entries = redisTemplate.execute(new RedisCallback<List<byte[]>>() {
            public List<byte[]> doInRedis(RedisConnection connection) {
                return connection.lRange(rawKey, 0, -1);
            }
        }, true);
        List<String> list = new ArrayList<String>(entries.size());
        for (byte[] entry : entries) {
            try {
                list.add(new String(entry, redisCode));
            } catch (UnsupportedEncodingException e) {
                return new ArrayList<String>();
            }
        }
        return list;
    }
}

介面: RedisSpringService.java

package com.maigangle.b2b.common.redis;

import java.util.List;
import java.util.Map;

/**
 * redis對外提供服務介面,如需使用請先注入(基於spring)
 * 
 *
 */
public interface RedisSpringService {
    /**
     * 釋出訊息
     * 
     */
    public abstract void pub(String channel, String param);

    /**
     * 通過key刪除
     * 
     * @param key
     */
    public abstract Long del(String... keys);

    /**
     * 新增key value 並且設定存活時間(byte)
     * 
     * @param key
     * @param value
     * @param liveTime
     */
    public abstract void set(byte[] key, byte[] value, long liveTime);

    /**
     * 新增key value 並且設定存活時間
     * 
     * @param key
     * @param value
     * @param liveTime
     *            單位秒
     */
    public abstract void set(String key, String value, long liveTime);

    /**
     * 設定key過期時間
     * 
     * @param key
     * @param liveTime
     * @return
     */
    public abstract boolean expire(String key, long liveTime);

    /**
     * 新增key value
     * 
     * @param key
     * @param value
     */
    public abstract void set(String key, String value);

    /**
     * 新增key value
     * 
     * @param key
     * @param value
     */
    public abstract void set(String key, byte[] value);

    /**
     * 新增key value (位元組)(序列化)
     * 
     * @param key
     * @param value
     */
    public abstract void set(byte[] key, byte[] value);

    /**
     * 儲存物件
     * 
     * @param key
     * @param value
     */
    public abstract void setOjb(final String key, Object value, long time);

    /**
     * 獲取redis value (String)
     * 
     * @param key
     * @return
     */
    public abstract String get(String key);

    public abstract byte[] get4byte(String key);

    /**
     * 得到物件
     * 
     * @param key
     * @param elementType
     * @return
     */
    public abstract <T> T getObj(final String key, Class<T> elementType);

    /**
     * 通過正則匹配keys
     * 
     * @param pattern
     * @return
     */
    public abstract void Setkeys(String pattern);

    /**
     * 檢查key是否已經存在
     * 
     * @param key
     * @return
     */
    public abstract boolean exists(String key);

    /**
     * 清空redis 所有資料
     * 
     * @return
     */
    // public abstract String flushDB();
    public abstract boolean flushDB();

    /**
     * 檢視redis裡有多少資料
     */
    public abstract long dbSize();

    /**
     * 檢查是否連線成功
     * 
     * @return
     */
    public abstract String ping();

    /**
     * submit check token
     * 
     * @param token
     * @return
     */
    public boolean checkToken(String token);

    /**
     * 
     * @param key
     * @param map
     * @param liveTime
     */
    public void setHm(String key, Map<String, String> map, long liveTime);

    /**
     * 
     * @param key
     * @return
     */
    public Map<String, String> getHm(String key);

    /**
     * 
     * @param key
     * @param list
     * @param liveTime
     */
    public void setList(String key, List<String> list, long liveTime);

    /**
     * 
     * @param key
     * @return
     */
    public List<String> getList(String key);
}

前端:

login.js

$('#loginIn').click(function(event) { 
        var $this = $(this);
        var acc = $('#account').val();
        var pw = $('#password').val();
        var loadUrl = util.get('redirectURL');
        var loginValid = $("#loginForm").valid();
        var setHtml = '';
        var loginUrl = '';

        // 如果返回false 設定為空
        if (loadUrl === false) {
            loadUrl = '';
        }

        if(loginValid) {
            $this.text('正在登入...');
            $this.attr('disabled', true);

            $.ajax({
                url: apiPath + '/auth/ajaxDoLogin',
                type: 'post',
                data: {
                    account: acc,
                    passwd: pw,
                    redirectURL: loadUrl
                },
                success: function(data) {
                    if (data.isVaild) {
                        if(data.role === 'A'){
                            setHtml += '<iframe src="' + gangUrlBase + '/api/common/setCookie?_it_=' + data._it_ + '&_token_='+ data._token_ +'"></iframe>';
                            setHtml += '<iframe src="' + payUrlBase + '/api/common/setCookie?_it_=' + data._it_ + '&_token_='+ data._token_  + '"></iframe>';
                            setHtml += '<iframe src="' + orderUrlBase + '/api/common/setCookie?_it_=' + data._it_ + '&_token_='+ data._token_  + '"></iframe>';
                            setHtml += '<iframe src="' + homeUrlBase + '/api/common/setCookie?_it_=' + data._it_ + '&_token_='+ data._token_  + '"></iframe>';
                            setHtml += '<iframe src="' + cartUrlBase + '/api/common/setCookie?_it_=' + data._it_ + '&_token_='+ data._token_  + '"></iframe>';
                            setHtml += '<iframe src="' + cangkuUrlBase + '/api/common/setCookie?_it_=' + data._it_ + '&_token_='+ data._token_  + '"></iframe>';
                            setHtml += '<iframe src="' + fundUrlBase + '/api/common/setCookie?_it_=' + data._it_ + '&_token_='+ data._token_  + '"></iframe>';
                            setHtml += '<iframe src="' + regUrlBase + '/api/common/setCookie?_it_=' + data._it_ + '&_token_='+ data._token_  + '"></iframe>';
                            setHtml += '<iframe src="' + baikeUrlBase + '/api/common/setCookie?_it_=' + data._it_ + '&_token_='+ data._token_  + '"></iframe>';
                            setHtml += '<iframe src="' + helpUrlBase + '/api/common/setCookie?_it_=' + data._it_ + '&_token_='+ data._token_  + '"></iframe>';
                            setHtml += '<iframe src="' + indexUrlBase + '/api/common/setCookie?_it_=' + data._it_ + '&_token_='+ data._token_  + '"></iframe>';
                        }else if(data.role === 'B'){
                             setHtml += '<iframe src="' + cangkuUrlBase + '/api/common/setCookie?_it_=' + data._it_ + '&_token_='+ data._token_  + '"></iframe>';
                        }else if(data.role === 'C'){
                             setHtml += '<iframe src="' + mcUrlBase + '/api/common/setCookie?_it_=' + data._it_ + '&_token_='+ data._token_  + '"></iframe>';
                        }
                        if(data.isNotBindPhone) {
                            window.location.href = data.redirectURL + '?firmId=' + data.firmId;
                        } else {
                            // 新增iframe執行寫入cookie
                            $('#setCookieView').html(setHtml);
                            setTimeout(function() {
                               window.location.href = data.redirectURL;
                            }, 2000);
                        }
                    } else {
                        $('#accError').show();
                        $('#accError').text(data.msg);
                        $this.text('我同意交易規則,登入');
                        $this.attr('disabled', false);
                    }
                },
                error: function(msg){
                    $('#accError').show();
                    $('#accError').text('登入失敗!');
                    $this.text('我同意交易規則,登入');
                    $this.attr('disabled', false);
                }
            });
        }
    });

後端java程式碼

@RequestMapping(value = "/ajaxDoLogin")
    @ResponseBody
    public Map<String, Object> ajaxDoLogin( HttpServletRequest request,
                                            HttpServletResponse response,
                                            String account,
                                            String passwd,
                                            String redirectURL) {
        String logBatch = LogBatchUtil.getLogUUID(EnumMonitorLog.LOGSYSTEM_LOGIN.getKey());
        Map<String, Object> resultMap = new HashMap<String, Object>();
        try {
            super.logInfo(  request,
                            logBatch,
                            "登入",
                            "進入登入相關資料,account:" + account + ",redirectURL:" + redirectURL,
                            EnumMonitorLog.LOGSYSTEM_LOGIN.getKey());
            account = account.replaceAll(" ", "");
            // 登入驗證
            Map<String, Object> vaildMap = validateLogin(request, account, passwd);
            boolean isVaild = (boolean) vaildMap.get("isVaild");
            if (!isVaild) {
                return vaildMap;
            }
            // 手機登入/交易商登入
            if (loginService.countByMobile(account) == 0) {
                resultMap = firmIdLogin(request, response, logBatch, account, passwd, redirectURL);// 賬號為交易號
            } else {
                resultMap = mobileLogin(request, response, logBatch, account, passwd, redirectURL);// 賬號為手機號碼
            }
            return resultMap;
        }
        catch (Exception e) {
            e.printStackTrace();
            super.logError( request,
                            logBatch,
                            "賬號為:" + account + "會員登入失敗",
                            "交易商ID為:" + getFirmId(request) + "錯誤資訊為:" + ExceptionUtils.getStackTrace(e),
                            Boolean.getBoolean(redisSpringService.get(EnumMonitorLog.LOG_NOTIFY_SWITCH.getKey())),
                            EnumMonitorLog.LOGSYSTEM_LOGIN.getKey());
            resultMap.put("msg", "非法登入");
            resultMap.put("isVaild", false);
            return resultMap;
        }
    }



private Map<String, Object> firmIdLogin(HttpServletRequest request,
                                            HttpServletResponse response,
                                            String logBatch,
                                            String account,
                                            String passwd,
                                            String redirectURL) {
        AcntUserBasic acntUserBasic = null;
        Map<String, Object> resultMap = null;
        // 賬號為交易號
        if (!loginApi(request, logBatch, account, passwd)) {
            return this.setLoginError(account, passwd);
        }
        acntUserBasic = loginService.selectByFirmId(account);
        if (acntUserBasic != null && !StringUtils.equals(acntUserBasic.getIsLock(), IsLockEnum.IS_LOCK_N.getKey())) {
            return this.setLockError(account, passwd, "登入帳號" + account + IsLockEnum.getLockInfo(acntUserBasic.getIsLock()));
        }
        // 使用者基本資訊不存在或手機號為空(後臺新增使用者)
        if (acntUserBasic == null || StringUtils.isBlank(acntUserBasic.getMobile())) {
            // 繫結手機號
            resultMap = new HashMap<String, Object>();