2013-09-01
在网站开发中,所谓session,也是一个特殊的cookie,常用于记录用户的登录状态。例如,当用户A正常登录启用的Sessin的网站B时候,网站B并不会将用户名、密码等显式的作为cookie,而是根据用户名、登录时间、登录IP等信息生成一个唯一的ID作为cookie,而在网站的服务器端则会记录该ID对应的用户名、失效时间等信息。这样,服务器要识别用户就是靠这个ID了。
以PHP为例,其使用session时候相应session信息是保存在文件中的,当然也可以设置以保存在数据库中。由于最近对redis有了些许的了解,同时考虑使用Java练练手,所以萌生了使用Redis做分布式会话管理系统的想法。
开发环境
Linux Mint 15,运行多个Redis实例;Windows 7,运行PostgreSQL 9.2,以及管理系统。
Redis配置
最好的方式是这些服务器全在内网中运行,由于懒得去找路由器,故做如下布局:4个Redis实例所在的linux mint使用ip 202.201.13.186,分别使用这些端口6301、6302、6303、6304。
首先在mint中安装redis-server:
sudo apt-get install redis-server
这同时会把redis-cli等工具安装了。 在某处新建一个目录redis,在redis目录下依次建立以下文件:
redis-01.conf redis-01.pid redis-01.log
redis-02.conf redis-02.pid redis-02.log
redis-03.conf redis-03.pid redis-03.log
redis-04.conf redis-04.pid redis-04.log
redis-01.conf内容如下:
daemonize yes
pidfile ./redis-01.pid
port 6301
timeout 300
loglevel debug
logfile ./redis-01.log
requirepass 111
redis-02.conf内容如下:
daemonize yes
pidfile ./redis-02.pid
port 6302
timeout 300
loglevel debug
logfile ./redis-02.log
requirepass 222
redis-03.conf内容如下:
daemonize yes
pidfile ./redis-03.pid
port 6303
timeout 300
loglevel debug
logfile ./redis-03.log
requirepass 333
redis-04.conf内容如下:
daemonize yes
pidfile ./redis-04.pid
port 6304
timeout 300
loglevel debug
logfile ./redis-04.log
requirepass 444
关于这些配置文件,以redis-04.conf为例,daemonize
指定redis是否以DAEMON形式运行,pidfile
指定保存redis实例的pid的文件,port
指定端口,timeout
指定超时时间(以秒为单位),loglevel
指定日志记录等级,logfile
指定日志输入位置,requirepass
指定连接redis需要的密码。
配置文件完成后,依次启动四个redis server实例:
user@myhost ~/Desktop/redis $ redis-server redis-01.conf
user@myhost ~/Desktop/redis $ redis-server redis-02.conf
user@myhost ~/Desktop/redis $ redis-server redis-03.conf
user@myhost ~/Desktop/redis $ redis-server redis-04.conf
随机测试一个启动的server:
user@myhost ~/Desktop/redis $ redis-cli -h 127.0.0.1 -p 6301 -a 111
redis 127.0.0.1:6301> set mykey myvalue
OK
redis 127.0.0.1:6301> get mykey
"myvalue"
redis 127.0.0.1:6301> del mykey
(integer) 1
redis 127.0.0.1:6301> get mykey
(nil)
redis 127.0.0.1:6301> exit
使用PostgreSQL保存用户信息
我们使用PostgreSQL保存用户的信息,下面是一些必要的数据库、表创建语句:
CREATE DATABASE test_db ENCODING='UTF8';
CREATE SEQUENCE increment_num INCREMENT 1 START 1;
CREATE TABLE session (
user_id INT DEFAULT NEXTVAL('increment_num'),
user_name VARCHAR(20),
user_email VARCHAR(40),
user_passwd VARCHAR(50),
CONSTRAINT primary_key PRIMARY KEY (user_id),
CONSTRAINT unique_email UNIQUE (user_email),
CONSTRAINT unique_name UNIQUE (user_name)
);
CREATE USER test_user PASSWORD '123456';
GRANT ALL ON session TO test_user;
GRANT ALL ON increment_num TO test_user;
在数据库test_db
中创建表session,而用户test_user
具有对session表的所有权限。
插入三条数据:
INSERT INTO session (user_name, user_email, user_passwd) VALUES ('hei','hei@163.com',md5('111'));
INSERT INTO session (user_name, user_email, user_passwd) VALUES ('xiaoming','xiaoming@163.com',md5('111'));
INSERT INTO session (user_name, user_email, user_passwd) VALUES ('obama','obama@163.com',md5('111'));
注意不可写成下面的形式:
INSERT INTO session (user_name, user_email, user_passwd) VALUES ("hei","hei@163.com",md5("111"));
这会产生找不到某某字段的错误。
使用redis分布式存储session信息
用户登陆时候需要输入用户名,密码和登录IP。当然在实际生活中我们并不需要输入IP,你的机子已经帮你做了,这里要输入IP是为了简化代码。
假定所有用户名都是以字母开头,设该字母为firstLetter。
如果firstLetter介于a和g以及A和G之间,则该用户登陆时候将其session信息保存在port为6301的redis server中;
如果firstLetter介于h和n以及H和N之间,则该用户登陆时候将其session信息保存在port为6302的redis server中;
如果firstLetter介于o和t以及O和T之间,则该用户登陆时候将其session信息保存在port为6303的redis server中;
如果firstLetter介于u和z以及U和Z之间,则该用户登陆时候将其session信息保存在port为6304的redis server中。
每个session为一条记录,出于简化目的,key是IP和用户名和登录时间连在一起,例如可能是"192.168.1.222obama20130505120343",当然在实际项目中可不能这样;value是一个map,map中三个key分别是user_id
,user_name
,user_email
。
Java如何操作redis中的数据
采用的是jedis包,下载见点这里。
APP测试
源文件布局如下:
运行User类:
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>4
请输入您的name>>>obama
请输入您的session ID>>> } else if (4 == choose) {
userLogout();姓名:obama
成功退出
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>1
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>1
开始登录吧
请输入用户名>>>obama
请输入密码>>>111
请输入登录IP>>>192.168.1.123
信息正确,进行登录。。。
登录时间:201383117367
用户名为:obama ,获取相应的server
姓名:obama
redis server信息:ip:202.201.13.186;port:6303
登录成功,请记住你的session id:192.168.1.123obama201383117367
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>4
请输入您的name>>>obama
请输入您的session ID>>>192.168.1.123obama201383117367
姓名:obama
成功退出
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>2
请输入您的name>>>obama
请输入您的session ID>>>192.168.1.123obama201383117367
姓名:obama
尚未登录 或者 会话过期
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>1
开始登录吧
请输入用户名>>>obama
请输入密码>>>111
请输入登录IP>>>192.168.1.222
信息正确,进行登录。。。
登录时间:2013831173751
用户名为:obama ,获取相应的server
姓名:obama
redis server信息:ip:202.201.13.186;port:6303
登录成功,请记住你的session id:192.168.1.222obama2013831173751
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>3
请输入您的name>>>obama
请输入您的session ID>>>192.168.1.222obama2013831173751
姓名:obama
您已经登录
姓名:obama
会话失效时间已经更新
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>
运行User期间在redis server中观测数据情况:
user@myhost ~/Desktop/redis $ redis-cli -h 127.0.0.1 -p 6303 -a 333
redis 127.0.0.1:6303> keys *
(empty list or set)
redis 127.0.0.1:6303> keys *
1) "192.168.1.222obama2013831173751"
redis 127.0.0.1:6303> ttl 192.168.1.222obama2013831173751
(integer) 1169
redis 127.0.0.1:6303> ttl 192.168.1.222obama2013831173751
(integer) 1168
redis 127.0.0.1:6303> ttl 192.168.1.222obama2013831173751
(integer) 1196
redis 127.0.0.1:6303>
源码
User.java:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Calendar;
import java.util.Scanner;
public class User {
/*
* 模拟登陆
*/
public static void userLogin() throws IOException {
String userName = "";
String userPasswd = "";
String userIp = "";
String loginTime = "";
RedisAdmin myRedisAdmin = new RedisAdmin();
// 输入信息
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.printf("请输入用户名>>>");
userName = br.readLine();
System.out.printf("请输入密码>>>");
userPasswd = br.readLine();
System.out.printf("请输入登录IP>>>");
userIp = br.readLine();
// br.close(); //若不注释,那么多次调用userLogin()函数时候就会有错误,why?
// 原因见:http://zhidao.baidu.com/question/25112752.html
// 判断用户是否存在
if (PsqlAdmin.isUserExists(userName, userPasswd)) {
System.out.println("信息正确,进行登录。。。");
} else {
System.out.println("对不起,用户名或密码错误!");
}
// 获取登录时间
Calendar now = Calendar.getInstance();
loginTime = "" + now.get(Calendar.YEAR) + (now.get(Calendar.MONTH) + 1)
+ now.get(Calendar.DAY_OF_MONTH)
+ now.get(Calendar.HOUR_OF_DAY) + now.get(Calendar.MINUTE)
+ now.get(Calendar.SECOND);
System.out.println("登录时间:" + loginTime);
// 基本信息已经获得,现在就把它放在redis中
if (myRedisAdmin.addSession(userIp, userName, userPasswd, loginTime)) {
System.out.println("登录成功,请记住你的session id:"
+ myRedisAdmin.genSessionID(userIp, userName, loginTime));
} else {
System.out.println("登录失败");
}
}
/*
* 判断是否已经登录
*/
public static void isLogin() throws IOException {
String userName;
String sessionID;
RedisAdmin myRedisAdmin = new RedisAdmin();
// 输入信息
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.printf("请输入您的name>>>");
userName = br.readLine();
System.out.printf("请输入您的session ID>>>");
sessionID = br.readLine();
if (myRedisAdmin.existsSession(userName, sessionID)) {
System.out.println("您已经登录");
} else {
System.out.println("尚未登录 或者 会话过期");
}
}
/*
* 用户做些动作,更新session的expire time
*/
public static void userAction() throws IOException {
String userName = "";
String sessionID = "";
RedisAdmin myRedisAdmin = new RedisAdmin();
// 输入信息
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.printf("请输入您的name>>>");
userName = br.readLine();
System.out.printf("请输入您的session ID>>>");
sessionID = br.readLine();
if (myRedisAdmin.existsSession(userName, sessionID)) {
System.out.println("您已经登录");
if (myRedisAdmin.updateSession(userName, sessionID)) {
System.out.println("会话失效时间已经更新");
} else {
System.out.println("会话失效时间更新失败%>_<%");
}
} else {
System.out.println("尚未登录 或者 会话过期");
}
}
/*
* 用户注销
*/
public static void userLogout() throws IOException {
String userName = "";
String sessionID = "";
RedisAdmin myRedisAdmin = new RedisAdmin();
// 输入信息
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.printf("请输入您的name>>>");
userName = br.readLine();
System.out.printf("请输入您的session ID>>>");
sessionID = br.readLine();
if (myRedisAdmin.delSession(userName, sessionID)) {
System.out.println("成功退出");
} else {
System.out.println("尚未登录 或者 会话过期");
}
}
public static void main(String[] args) throws IOException {
int choose = 0;
Scanner input;
while (true) {
System.out.println("help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出");
System.out.printf(">>");
choose = 0;
input = new Scanner(System.in);
if (input.hasNextInt()) {
choose = input.nextInt();
}
if (1 == choose) {
System.out.println("开始登录吧");
userLogin();
} else if (2 == choose) {
isLogin();
} else if (3 == choose) {
userAction();
} else if (4 == choose) {
userLogout();
} else if (5 == choose) {
break;
} else { // do nothing
}
}
input.close();
}
}
RedisAdmin.java:
import java.util.HashMap;
import java.util.Map;
import redis.clients.jedis.Jedis;
public class RedisAdmin {
private int EXPIRE_TIME = 1200;
private HashMap<String, String> redisServer01, redisServer02,
redisServer03, redisServer04;
/*
* 初始化各个redis server的信息
*/
public RedisAdmin() {
redisServer01 = new HashMap<String, String>();
redisServer01.put("ip", "202.201.13.186");
redisServer01.put("port", "6301");
redisServer01.put("pass", "111");
redisServer02 = new HashMap<String, String>();
redisServer02.put("ip", "202.201.13.186");
redisServer02.put("port", "6302");
redisServer02.put("pass", "222");
redisServer03 = new HashMap<String, String>();
redisServer03.put("ip", "202.201.13.186");
redisServer03.put("port", "6303");
redisServer03.put("pass", "333");
redisServer04 = new HashMap<String, String>();
redisServer04.put("ip", "202.201.13.186");
redisServer04.put("port", "6304");
redisServer04.put("pass", "444");
}
/*
* 根据用户名判断该用户的session信息应该放在哪个redis server中
*/
public HashMap<String, String> getServer(String name) {
System.out.println("姓名:" + name);
char firstLetter = name.charAt(0);
if (('a' <= firstLetter && firstLetter <= 'g')
|| ('A' <= firstLetter && firstLetter <= 'G')) {
return this.redisServer01;
} else if (('h' <= firstLetter && firstLetter <= 'n')
|| ('H' <= firstLetter && firstLetter <= 'N')) {
return this.redisServer02;
} else if (('o' <= firstLetter && firstLetter <= 't')
|| ('O' <= firstLetter && firstLetter <= 'T')) {
return this.redisServer03;
} else {
return this.redisServer04;
}
}
/*
* 生成sessionId
*/
public String genSessionID(String ip, String userName, String loginTime) {
String sessionID = ip + userName + loginTime;
return sessionID;
}
/*
* 判断sessionId是否存在
*/
public boolean existsSession(String userName, String sessionID) {
try {
HashMap<String, String> server = this.getServer(userName);
Jedis redis = new Jedis(server.get("ip"), Integer.parseInt(server
.get("port")));// 连接redis
redis.auth(server.get("pass"));// 验证密码
return redis.exists(sessionID);
} catch (Exception ee) {
ee.printStackTrace();
}
return false;
}
/*
* 向redis集群中添加session
*/
public boolean addSession(String ip, String userName, String passwd,
String loginTime) {
try {
System.out.println("用户名为:" + userName + " ,获取相应的server");
HashMap<String, String> server = this.getServer(userName);
System.out.println("redis server信息:ip:" + server.get("ip")
+ ";port:" + server.get("port"));
HashMap<String, String> userInfo = PsqlAdmin.getUserInfo(userName,
passwd);
Map<String, String> session = new HashMap<>();
session.put("user_id", userInfo.get("user_id"));
session.put("user_name", userInfo.get("user_name"));
session.put("user_email", userInfo.get("user_email"));
String sessionID = genSessionID(ip, userName, loginTime);
Jedis redis = new Jedis(server.get("ip"), Integer.parseInt(server
.get("port")));// 连接redis
redis.auth(server.get("pass"));// 验证密码
redis.hmset(sessionID, session);
redis.expire(sessionID, EXPIRE_TIME);
return true;
} catch (Exception ee) {
ee.printStackTrace();
}
return false;
}
/*
* 更新EXPIRE TIME(过期时间)
*/
public boolean updateSession(String userName, String sessionID) {
try {
HashMap<String, String> server = this.getServer(userName);
Jedis redis = new Jedis(server.get("ip"), Integer.parseInt(server
.get("port")));// 连接redis
redis.auth(server.get("pass"));// 验证密码
redis.expire(sessionID, EXPIRE_TIME);
return true;
} catch (Exception ee) {
ee.printStackTrace();
}
return false;
}
/*
* 删除session
*/
public boolean delSession(String userName, String sessionID) {
try {
HashMap<String, String> server = this.getServer(userName);
Jedis redis = new Jedis(server.get("ip"), Integer.parseInt(server
.get("port")));// 连接redis
redis.auth(server.get("pass"));// 验证密码
redis.expire(sessionID, 0); // 0 秒后过期
return true;
} catch (Exception ee) {
ee.printStackTrace();
}
return false;
}
}
PsqlAdmin.java:
/*
* 代码结构不够好啊
*/
import java.sql.*;
import java.util.HashMap;
public class PsqlAdmin {
public PsqlAdmin() {
}
/*
* 向数据库中添加用户
*/
public static boolean addUser(String userName, String userEmail,
String userPasswd) {
String sql = " INSERT INTO session (user_name, user_email, user_passwd) VALUES ('"
+ userName
+ "','"
+ userEmail
+ "',"
+ "md5('"
+ userPasswd
+ "'))"; // 别用这个引号“"”
System.out.println(sql);
try {
Class.forName("org.postgresql.Driver").newInstance();
String url = "jdbc:postgresql://localhost:5432/test_db";
Connection con = DriverManager.getConnection(url, "test_user",
"123456");
Statement st = con.createStatement();
int count = st.executeUpdate(sql); // 这句有问题
System.out.println(sql);
System.out.println("插入记录的数目:" + count);
st.close();
con.close();
return true;
} catch (Exception ee) {
ee.printStackTrace();
return false;
}
}
/*
* 显示数据库中已有的用户的信息
*/
public static boolean showUser() {
try {
Class.forName("org.postgresql.Driver").newInstance();
String url = "jdbc:postgresql://localhost:5432/test_db";
Connection con = DriverManager.getConnection(url, "test_user",
"123456");
Statement st = con.createStatement();
String sql = "SELECT user_name, user_email FROM session";
ResultSet rs = st.executeQuery(sql);
while (rs.next()) {
System.out.println("姓名:" + rs.getString("user_name"));
System.out.println("电邮:" + rs.getString("user_email"));
}
rs.close();
st.close();
con.close();
return true;
} catch (Exception ee) {
return false;
}
}
/*
* 根据用户名和密码判断数据库中是否存在相应的记录
*/
public static boolean isUserExists(String name, String passwd) {
try {
Class.forName("org.postgresql.Driver").newInstance();
String url = "jdbc:postgresql://localhost:5432/test_db";
Connection con = DriverManager.getConnection(url, "test_user",
"123456");
Statement st = con.createStatement();
String sql = "SELECT COUNT(*) AS num FROM session WHERE user_name = \'"
+ name + "\' and user_passwd=md5(\'" + passwd + "\')";
// System.out.println(sql);
ResultSet rs = st.executeQuery(sql);
int exist = 0;
while (rs.next()) {
exist = rs.getInt("num");
}
rs.close();
st.close();
con.close();
if (0 == exist) {
return false;
} else {
return true;
}
} catch (Exception ee) {
ee.printStackTrace();
return false;
}
}
/*
* 根据用户名和密码从数据库中获取详尽的用户信息
*/
public static HashMap<String, String> getUserInfo(String name, String passwd) {
HashMap<String, String> userInfo = new HashMap<>();
try {
Class.forName("org.postgresql.Driver").newInstance();
String url = "jdbc:postgresql://localhost:5432/test_db";
Connection con = DriverManager.getConnection(url, "test_user",
"123456");
Statement st = con.createStatement();
String sql = "SELECT * FROM session WHERE user_name = \'"
+ name + "\' and user_passwd=md5(\'" + passwd + "\')";
// System.out.println(sql);
ResultSet rs = st.executeQuery(sql);
while (rs.next()) {
userInfo.put("user_id", String.valueOf(rs.getInt("user_id")));
userInfo.put("user_name", rs.getString("user_name"));
userInfo.put("user_email", rs.getString("user_email"));
}
rs.close();
st.close();
con.close();
} catch (Exception ee) {
ee.printStackTrace();
}
return userInfo;
}
/*
* public static void main(String[] args) { boolean state = addUser("obama",
* "obama@163.com", "111"); if (state == false) {
* System.out.println("插入失败"); } state = showUser(); if (state == false) {
* System.out.println("查询失败"); } }
*/
}