分散式環境下用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>();