基于cookie的简易单点登录
一.前言
本篇博客只是对于初学单点登录的一个认识,可以算作一个伪单点登录。后续有时间会更新SSO技术。
二.正文
因为是一个简单的demo,所以我用的是springboot+mybatis+redis,redis用来保存cookies值
1.pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.9</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.application.yml
spring:
redis:
host: 122.51.35.190
port: 6379
password:
jedis:
pool:
max-active: 8
max-idle: 500
min-idle: 0
database: 1
datasource:
url: jdbc:mysql://localhost:3306/user?characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: zhouwei
thymeleaf:
mode: HTML
cache: false
suffix: .html
prefix: classpath:/templates/
mybatis:
mapper-locations: classpath*:/mapper/*.xml
REDIS_KEY: userinfo
3.这里我用到了几个工具类,JedisUtil,JsonUtils,SimpleResponse,CookieUtils
JedisUtil.java
@Component
@ConfigurationProperties (prefix = "spring.redis")
public class JedisUtil {
private String host;
private int port;
/**
* 链接redis的操作工具类
*
* @return
*/
@Bean
public JedisPool jedisPool() {
JedisPool jedisPool = new JedisPool(host, port);
System.out.println("redis初始化连接成功... \t host:" + host + "\tport:" + port);
return jedisPool;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
JsonUtils.java
/**
* 自定义响应结构
*/
public class JsonUtils {
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 将对象转换成json字符串。
* <p>Title: pojoToJson</p>
* <p>Description: </p>
*
* @param data
* @return
*/
public static String objectToJson(Object data) {
try {
String string = MAPPER.writeValueAsString(data);
return string;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
/**
* 将json结果集转化为对象
*
* @param jsonData json数据
* @param beanType 对象中的object类型
* @return
*/
public static <T> T jsonToPojo(String jsonData, Class<T> beanType) {
try {
T t = MAPPER.readValue(jsonData, beanType);
return t;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 将json数据转换成pojo对象list
* <p>Title: jsonToList</p>
* <p>Description: </p>
*
* @param jsonData
* @param beanType
* @return
*/
public static <T> List<T> jsonToList(String jsonData, Class<T> beanType) {
JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
try {
List<T> list = MAPPER.readValue(jsonData, javaType);
return list;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
SimpleResponse.java
/**
* 自定义响应类
*
* @author lt
*/
public class SimpleResponse {
private boolean success;
private Object data;
private String errCode;
private String errMsg;
private String errDesc;
public SimpleResponse(Object data) {
this.data = data;
success = true;
}
public SimpleResponse(boolean success) {
this.success = success;
}
public SimpleResponse(String errCode, String errMsg, String errDesc) {
this.errCode = errCode;
this.errMsg = errMsg;
this.errDesc = errDesc;
success = false;
}
public static SimpleResponse success(boolean success) {
return new SimpleResponse(success);
}
public static SimpleResponse success(Object data) {
return new SimpleResponse(data);
}
public static SimpleResponse error(String errCode, String errMsg, String errDesc) {
return new SimpleResponse(errCode, errMsg, errDesc);
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getErrCode() {
return errCode;
}
public void setErrCode(String errCode) {
this.errCode = errCode;
}
public String getErrMsg() {
return errMsg;
}
public void setErrMsg(String errMsg) {
this.errMsg = errMsg;
}
public String getErrDesc() {
return errDesc;
}
public void setErrDesc(String errDesc) {
this.errDesc = errDesc;
}
}
CookieUtils.java
/**
* Cookie 工具类
*/
public final class CookieUtils {
/**
* 得到Cookie的值, 不编码
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName) {
return getCookieValue(request, cookieName, false);
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
if (isDecoder) {
retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
} else {
retValue = cookieList[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
* 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue) {
setCookie(request, response, cookieName, cookieValue, -1);
}
/**
* 设置Cookie的值 在指定时间内生效,但不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage) {
setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
}
/**
* 设置Cookie的值 不设置生效时间,但编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, boolean isEncode) {
setCookie(request, response, cookieName, cookieValue, -1, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效, 编码参数
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, boolean isEncode) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, String encodeString) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
}
/**
* 删除Cookie带cookie域名
*/
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName) {
doSetCookie(request, response, cookieName, "", -1, false);
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxage cookie生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
try {
if (cookieValue == null) {
cookieValue = "";
} else if (isEncode) {
cookieValue = URLEncoder.encode(cookieValue, "utf-8");
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0) {
cookie.setMaxAge(cookieMaxage);
}
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
System.out.println(domainName);
if (!"localhost".equals(domainName)) {
//cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxage cookie生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
try {
if (cookieValue == null) {
cookieValue = "";
} else {
cookieValue = URLEncoder.encode(cookieValue, encodeString);
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0) {
cookie.setMaxAge(cookieMaxage);
}
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
System.out.println(domainName);
if (!"localhost".equals(domainName)) {
//本地测试的时候不要写.实际发布时在打开
//cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 得到cookie的域名
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;
String serverName = request.getRequestURL().toString();
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
int end = serverName.lastIndexOf("/");
serverName = serverName.substring(0, end);
String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3) {
// www.xxx.com.cn
domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// xxx.com or xxx.cn
domainName = "." + domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
if (domainName != null && domainName.indexOf(":") > 0) {
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
return domainName;
}
}
4.自定义封装的JedisDao及其实现
JedisDao.java
public interface JedisDao {
//查
public String getValue(String key);
//删
public Long delValue(String key);
//改
public boolean update(String key, String value);
//增
public String setValue(String key, String value);
//设置时间
public Long expire(String key, int seconds);
}
JedisDaoImpl.java
@Component
public class JedisDaoImpl implements JedisDao {
//连接池
@Autowired
private JedisPool jedisPool;
@Override
public String getValue(String key) {
Jedis jedis = jedisPool.getResource();
String value = jedis.get(key);
jedis.close();
return value;
}
@Override
public Long delValue(String key) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.del(key);
jedis.close();
return result;
}
@Override
public boolean update(String key, String value) {
Jedis jedis = jedisPool.getResource();
if (jedis.exists(key)) {
jedis.set(key, value);
if (value.equals(jedis.get(key))) {
System.out.println("修改数据成功");
return true;
} else {
System.out.println("修改数据失败");
return false;
}
} else {
System.out.println(key + "不存在");
System.out.println("若要新增数据请使用set()方法");
return false;
}
}
@Override
public String setValue(String key, String value) {
Jedis jedis = jedisPool.getResource();
String str = jedis.set(key, value);
jedis.close();
return str;
}
@Override
public Long expire(String key, int seconds) {
Jedis jedis = jedisPool.getResource();
Long time = jedis.expire(key, seconds);
jedis.close();
return time;
}
}
5.Service层,这里我只贴出了主要的代码:
@Override
//一般在项目开发中,是通过接受一个用户名得到对象来比对密码实现登录的,这里是在以前一个随便的demo改装的,所以没设计
public User doLogin(HttpServletResponse response, HttpServletRequest request, String username, String password) {
//先进行用户查找,比对密码,此处最好是加密比对
User user = userMapper.doLogin(username, password);
int userId = user.getId();
String token = redisKey + userId + UUID.randomUUID().toString();
String oldToken = userInfo.get(userId);
if (!ObjectUtils.isEmpty(oldToken)) {
jedisDao.delValue(oldToken);
}
userInfo.remove(userId);
//存放到临时map里
userInfo.put(userId, token);
//将信息存入redis中
jedisDao.setValue(token, JsonUtils.objectToJson(user));
//设置信息过期时间
//将token存入cookie中
//CookieUtils.deleteCookie(request, response, "USER_TOKEN");
CookieUtils.setCookie(request, response, "USER_TOKEN", token, 5 * 60, true);
//返回用户信息
return user;
}
@Override
public User getUserByToken(HttpServletResponse response, HttpServletRequest request) throws Exception {
User user = null;
//从cookie中取出用户token
String token = CookieUtils.getCookieValue(request, "USER_TOKEN");
//从redis中根据取出的token来寻找相对应的对象
String userJson = jedisDao.getValue(token);
if (!StringUtils.isEmpty(userJson)) {
//将获取到的用户信息,转换为实体类
user = JsonUtils.jsonToPojo(userJson, User.class);
}
//返回查到的信息
return user;
}
6.Controller层
@RequestMapping ("/doLogin")
public String doLogin(HttpServletResponse response, HttpServletRequest request, String username, String password, Model model) {
try {
userService.doLogin(response, request, username, password);
return "index";
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("error", "用户名或密码不匹配");
return "Login";
}
}
@GetMapping ("/info")
@ResponseBody
public boolean getUserInfo(HttpServletResponse response, HttpServletRequest request) throws Exception {
if (userService.getUserByToken(response, request) != null) {
return true;
} else {
return false;
}
}
7.前端登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action="/doLogin" method="get">
<input type="text" name="username"><br>
<input type="text" name="password"><br>
<input type="submit" value="提交">
</form>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录成功</title>
</head>
<body>
成功!
</body>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js">
</script>
<script>
window.onblur = function (e) {
$.get("/info", function (result) {
if (result == false) {
alert("已经在别处登录");
location.href = "/doLogin";
}
})
};
</script>
</html>
三.实现效果
这里需要用两个不同浏览器来模仿不同的两个登录平台
我这里是先打开的谷歌,再打开的火狐
最后返回谷歌失去焦点就会弹出,已在别处登录
四.小结
在登录方法中创建一个用户的token,把user作为key,token作为value存到临时map,又将token作为key,user对象作为value存到redis里面,在每次登录的时候都先根据对象的id来查map里面有没有token的存在,如果有新的会删除之前的旧的token来保证一个user只有一个token,然后getUserByToken方法通过获取当前请求的cookie来获取token,来返回user对象来判断,从而达到只允许唯一平台登录。