在chapter6中用redis+cookie+jackson+filter实现单点登录,并且在chapter7中将redis改为了分布式。
但是这种方式对业务有侵入,可以看到我们需要将session.getAttribute()改为RedisPoolUtil.get()。
使用SpringSession可以实现零侵入。使用者仍然从session中拿到属性即可,也不需要处理cookie。但是这里的session已经是SpringSession包装过的了。它内部的实现中其实做了和chapter6类似的事情。
SpringSession(1.2.1 RELEASE版本)有一个缺点:查看JedisConnectionFactory的源码可知,它在初始化JedisPool时,虽然使用的是ShardedJedis,但是并不是使用List<JedisShardInfo>,而是只放了一个JedisShardInfo。所以它不支持多个redis。
7.1 代码准备
7.1.1 UserSpringSessionController
为了不影响原本代码,复制一份UserController,重命名为UserSpringSessionController。只保留login、logout和getUserInfo,用来测试SpringSession的功能。
package com.mmall.controller.portal;
import com.mmall.common.Const;
import com.mmall.common.ResponseCode;
import com.mmall.common.ServerResponse;
import com.mmall.pojo.User;
import com.mmall.service.IUserService;
import com.mmall.util.CookieUtil;
import com.mmall.util.JsonUtil;
import com.mmall.util.RedisShardedPoolUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* Created by geely
*/
@Controller
@RequestMapping("/user/springsession/")
public class UserSpringSessionController {
@Autowired
private IUserService iUserService;
/**
* 用户登录
* @param username
* @param password
* @param session
* @return
*/
@RequestMapping(value = "login.do",method = RequestMethod.GET)
@ResponseBody
public ServerResponse<User> login(String username, String password, HttpSession session, HttpServletResponse httpServletResponse){
//测试全局异常
// int i = 0;
// int j = 666/i;
ServerResponse<User> response = iUserService.login(username,password);
if(response.isSuccess()){
session.setAttribute(Const.CURRENT_USER,response.getData());
// CookieUtil.writeLoginToken(httpServletResponse,session.getId());
// RedisShardedPoolUtil.setEx(session.getId(), JsonUtil.obj2String(response.getData()),Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
}
return response;
}
@RequestMapping(value = "logout.do",method = RequestMethod.GET)
@ResponseBody
public ServerResponse<String> logout(HttpSession session,HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse){
// String loginToken = CookieUtil.readLoginToken(httpServletRequest);
// CookieUtil.delLoginToken(httpServletRequest,httpServletResponse);
// RedisShardedPoolUtil.del(loginToken);
session.removeAttribute(Const.CURRENT_USER);
return ServerResponse.createBySuccess();
}
@RequestMapping(value = "get_user_info.do",method = RequestMethod.GET)
@ResponseBody
public ServerResponse<User> getUserInfo(HttpSession session,HttpServletRequest httpServletRequest){
// String loginToken = CookieUtil.readLoginToken(httpServletRequest);
// if(StringUtils.isEmpty(loginToken)){
// return ServerResponse.createByErrorMessage("用户未登录,无法获取当前用户的信息");
// }
// String userJsonStr = RedisShardedPoolUtil.get(loginToken);
// User user = JsonUtil.string2Obj(userJsonStr,User.class);
User user = (User)session.getAttribute(Const.CURRENT_USER);
if(user != null){
return ServerResponse.createBySuccess(user);
}
return ServerResponse.createByErrorMessage("用户未登录,无法获取当前用户的信息");
}
7.1.2 User需要可序列化
User类需要加上implement Serializer,否则使用SpringSession时会报错如下:
public class User implements Serializable {
//略
}
7.2 SpringSession的依赖与配置
7.2.1 pom.xml
<!-- spring session 单点登录 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>
这里还需要注意一个问题,SpringSession1.2.0 RELEASE和Spring4.0.0集成时有bug,所以将依赖中的版本改为4.0.3。
<org.springframework.version>4.0.3.RELEASE</org.springframework.version>
7.2.2 applicationContext.xml
引入SpringSession的配置文件applicationContext-spring-session.xml。
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
<context:component-scan base-package="com.mmall" annotation-config="true">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>
<aop:aspectj-autoproxy/>
<import resource="applicationContext-spring-session.xml"/>
<import resource="applicationContext-datasource.xml"/>
</beans>
7.2.3 SpringContext-spring-session.xml
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<property name="maxInactiveIntervalInSeconds" value="1800" />
</bean>
<!--可选,如果不配置,将使用默认参数,可以参看源码的默认参数值-->
<bean id="defaultCookieSerializer" class="org.springframework.session.web.http.DefaultCookieSerializer">
<property name="domainName" value=".happymmall.com" />
<property name="useHttpOnlyCookie" value="true" />
<property name="cookiePath" value="/" />
<property name="cookieMaxAge" value="31536000" />
</bean>
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="20"/>
</bean>
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="127.0.0.1" />
<property name="port" value="6379" />
<property name="poolConfig" ref="jedisPoolConfig" />
</bean>
</beans>
7.3 SpringSession的校验
7.3.1 集群环境测试
开启tomcat+nginx+redis的集群环境,可以验证访问www.happymmall.com/user/springsession时已经实现了session共享。
还可以查看request中带上的cookie值。
这里的cookie中,key为SESSION,这是类DefaultCookieSerializer中定义的。可以在配置文件中修改。回看自己实现的session共享,可以看到是类似的,只不过这里我们是自己实现了CookieUtil将key命名为mmall_login_token,并且存入了sessionId。
7.3.2 SpringSession在redis中存有的key
SpringSession会在redis中存3个key。
这是SpringSession的一个保守做法,它不是一到失效期就立马删除session。
当session到了我们设置的失效期,SpringSession会删除其他两个key,只剩下spring:session:sessions。它的ttl是SpringSession代码中设置的,不是我们设置的有效期。等它自己的ttl到期才会被删除,即一般来说会延迟删除。
我们设置的有效期影响的是spring:session:sessions:expires,它一旦失效就表示session失效。所以虽然spring:session:sessions保留的session信息还在,但是因为其他两个key已经被删除,这个session是不能被读取的。就是说,前台此时访问接口会被提示未登录。
7.4 SpringSession的源码解析
涉及的重要类与接口:
- AbstractHttpSessionApplicationInitializer
- HttpSessionStrategy
- CookieHttpSessionStrategy
- SessionRepository
- RedisOperationSessionRepository
- DefaultCookieSerializer
- SessionRepositoryFilter
- JedisConnectionFactory
- RedisHttpSessionConfiguration
- DelegatingFilterProxy
- SessionRepositoryRequestWrapper
- SessionRepositoryResponseWrapper