一 简介

本篇文章主要介绍:在SpringMVC中如何使用Interceptor+Cookie实现在一定天数之内自动登录的功能。同时还介绍“如果校验失败则跳转到登录页面,在输入用户名、密码等完成登录之后又自动跳转到原页面”的功能实现

本次测试环境是SSM框架,在正式介绍本篇文章之前,建议需要熟悉以下前置知识点:

二 代码实现

(1)数据库表设计:

我这里采用的是MySQL,同时设计了两张表,分别是:userpersistent_logins

i)user表:

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  `password` varchar(300) DEFAULT NULL,
  `email` varchar(64) DEFAULT NULL,
  `birthday` date DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'admin', 'admin', 'admin@zifangsky.cn', '2016-06-30');
INSERT INTO `user` VALUES ('2', 'test', '123456', 'test@zifangsky.cn', '2015-12-12');
INSERT INTO `user` VALUES ('3', 'zifangsky', 'zifangsky', 'zifangsky@zifangsky.cn', '2010-02-10');

这张表很简单,就是一张普通的用户表

ii)persistent_logins表:

DROP TABLE IF EXISTS `persistent_logins`;
CREATE TABLE `persistent_logins` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL,
  `series` varchar(300) DEFAULT NULL,
  `token` varchar(500) DEFAULT NULL,
  `validTime` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这张表是用户校验用户自动登录的表。设计这张表的原因是我看过一些网上的文章介绍使用cookie自动登录,但是他们基本上都是将用户名、密码、salt等字符串拼接之后md5加密然后保存在cookie中。虽然使用了md5这类非对称加密方式,但是将密码这类关键信息保存在用户端,我觉得是不太靠谱的。因此设计了这张表,将用户名、密码等关键信息加密之后的数据保存到这张表中,在用户的cookie里只保存了没有特殊含义的UUID值以及用户名

这张表中的几个字段的含义分别是:

  • id    主键

  • username    用户名

  • series    用户使用密码登录成功之后获取的一个UUID值,同时用户端保存的cookie记录就是:EncryptionUtil.base64Encode(用户名:此UUID值)

  • token    在拦截器中校验是否能够登录的密文,其加密方式是:EncryptionUtil.sha256Hex(用户名 + “_” + 密码 + “_” + 自动登录失效的时间点的字符串 + “_” +  自定义的salt)

  • validTime    自动登录失效的时间,即:这个时间点之后只能重新用用户名、密码登录,如果在重新登录时勾选了“30天内自动登录”则更新该用户在persistent_logins这个表中的自动登录记录

(2)几个基本的配置文件:

i)web.xml:

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
	  http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>
			classpath:context/context.xml
		</param-value>
	</context-param>
	<!-- 这两个listener不加会出现无法依赖注入问题 -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	<listener>
		<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
	</listener>	
	<servlet>
		<servlet-name>springmvc</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>classpath:context/springmvc-servlet.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>springmvc</servlet-name>
		<url-pattern>*.html</url-pattern>
	</servlet-mapping>
	
	<filter>
		<filter-name>characterEncodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>characterEncodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

</web-app>

ii)context.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:cache="http://www.springframework.org/schema/cache" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:jee="http://www.springframework.org/schema/jee"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
			http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
            http://www.springframework.org/schema/jee 
            http://www.springframework.org/schema/jee/spring-jee-4.0.xsd
            http://www.springframework.org/schema/aop 
            http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
            http://www.springframework.org/schema/context 
            http://www.springframework.org/schema/context/spring-context-4.0.xsd
            http://www.springframework.org/schema/cache  
       		http://www.springframework.org/schema/cache/spring-cache-4.0.xsd  
            http://www.springframework.org/schema/tx 
            http://www.springframework.org/schema/tx/spring-tx-4.0.xsd"
	xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:aop="http://www.springframework.org/schema/aop">

	<!-- 配置数据源 -->
	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
		destroy-method="close">

		<property name="driverClass">
			<value>com.mysql.jdbc.Driver</value>
		</property>
		<property name="jdbcUrl">
			<value>jdbc:mysql://127.0.0.1:3306/cookie_db</value>
		</property>
		<property name="user">
			<value>root</value>
		</property>
		<property name="password">
			<value>root</value>
		</property>
		<!--连接池中保留的最小连接数。 -->
		<property name="minPoolSize">
			<value>5</value>
		</property>
		<!--连接池中保留的最大连接数。Default: 15 -->
		<property name="maxPoolSize">
			<value>30</value>
		</property>
		<!--初始化时获取的连接数,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
		<property name="initialPoolSize">
			<value>10</value>
		</property>
		<!--最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 -->
		<property name="maxIdleTime">
			<value>60</value>
		</property>
		<!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
		<property name="acquireIncrement">
			<value>5</value>
		</property>
		<!--JDBC的标准参数,用以控制数据源内加载的PreparedStatements数量。但由于预缓存的statements 属于单个 connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素。 
			如果maxStatements与maxStatementsPerConnection均为0,则缓存被关闭。Default: 0 -->
		<property name="maxStatements">
			<value>0</value>
		</property>
		<!--每60秒检查所有连接池中的空闲连接。Default: 0 -->
		<property name="idleConnectionTestPeriod">
			<value>60</value>
		</property>
		<!--定义在从数据库获取新连接失败后重复尝试的次数。Default: 30 -->
		<property name="acquireRetryAttempts">
			<value>30</value>
		</property>
		<!--获取连接失败将会引起所有等待连接池来获取连接的线程抛出异常。但是数据源仍有效 保留,并在下次调用 getConnection()的时候继续尝试获取连接。如果设为true,那么在尝试 
			获取连接失败后该数据源将申明已断开并永久关闭。Default: false -->
		<property name="breakAfterAcquireFailure">
			<value>true</value>
		</property>
		<!--因性能消耗大请只在需要的时候使用它。如果设为true那么在每个connection提交的 时候都将校验其有效性。建议 使用idleConnectionTestPeriod或automaticTestTable 
			等方法来提升连接测试的性能。Default: false -->
		<property name="testConnectionOnCheckout">
			<value>false</value>
		</property>
	</bean>
	<!-- MyBatis相关配置 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="configLocation" value="classpath:context/sql-map-config.xml" />
		<property name="dataSource" ref="dataSource" />
	</bean>
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="cn.zifangsky.mapper" />
		<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
	</bean>

	<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg index="0" ref="sqlSessionFactory" />
	</bean>

	<!-- 事务相关配置 -->
	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>

	<tx:annotation-driven transaction-manager="transactionManager" />
</beans>

iii)SpringMVC的配置文件springmvc-servlet.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:context="http://www.springframework.org/schema/context"
	xmlns:cache="http://www.springframework.org/schema/cache"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
	   http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.0.xsd
       http://www.springframework.org/schema/cache  
       http://www.springframework.org/schema/cache/spring-cache-4.0.xsd  
       http://www.springframework.org/schema/mvc 
       http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"
       default-lazy-init="true">
	
	<mvc:annotation-driven />
	<!-- 组件扫描 -->
	<context:component-scan base-package="cn.zifangsky.controller" />
	<context:component-scan base-package="cn.zifangsky.manager.impl"/>		
	<!-- 配置直接转发的页面 -->
	<mvc:view-controller path="/login.html" view-name="login" />
	<mvc:view-controller path="/user/callback.html" view-name="user/callback" />
	
	<!-- 拦截器 -->
	<mvc:interceptors>
		<mvc:interceptor>
			<!-- 对登录操作进行拦截 -->
			<mvc:mapping path="/check.html"/>
			<bean class="cn.zifangsky.interceptor.LoginInterceptor" />
		</mvc:interceptor>
		<mvc:interceptor>
			<!-- 对/user/**的请求进行拦截 -->
			<mvc:mapping path="/user/**"/>
			<bean class="cn.zifangsky.interceptor.UserInterceptor" />
		</mvc:interceptor>
	</mvc:interceptors>
	
	<!-- 视图解析 -->
	<bean
		class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<property name="prefix" value="/WEB-INF/pages/" />
		<property name="suffix" value=".jsp" />
	</bean>
</beans>

这里需要注意的是第31-35行的拦截器的配置,这个拦截器就是用于使用cookie自动登录的拦截器,上面那个用于拦截登录时的CSRF***的拦截器可以先不用管,直接注释掉或者参考下我的这篇文章:https://www.zifangsky.cn/671.html

(3)Mapper层的代码:

i)UserMapper:

package cn.zifangsky.mapper;

import cn.zifangsky.model.User;

public interface UserMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(User record);

    int insertSelective(User record);

    User selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(User record);

    int updateByPrimaryKey(User record);
    
    /**
     * 根据用户信息查用户详情(登录)
     * */
    User selectByUser(User user);
    /**
     * 根据用户名查用户详情
     * */
    User selectByName(String name);
}

这里除了使用插件自动生成的几个方法之外,还添加了两个其他的方法,它们对应的SQL语句是:

  <select id="selectByName" resultMap="BaseResultMap" parameterType="java.lang.String" >
    select 
    <include refid="Base_Column_List" />
    from user
    where name = #{name,jdbcType=VARCHAR}
  </select>
    
  <select id="selectByUser" resultMap="BaseResultMap" parameterType="cn.zifangsky.model.User" >
    select 
    <include refid="Base_Column_List" />
    from user
    where name = #{name,jdbcType=VARCHAR} and password = #{password,jdbcType=VARCHAR}
      <if test="email != null" >
        and email = #{email,jdbcType=VARCHAR}
      </if>
      <if test="birthday != null" >
        and birthday = #{birthday,jdbcType=DATE}
      </if>
  </select>

ii)PersistentLoginsMapper:

package cn.zifangsky.mapper;

import org.apache.ibatis.annotations.Param;

import cn.zifangsky.model.PersistentLogins;

public interface PersistentLoginsMapper {
	int deleteByPrimaryKey(Integer id);

	int insert(PersistentLogins record);

	int insertSelective(PersistentLogins record);

	PersistentLogins selectByPrimaryKey(Integer id);

	int updateByPrimaryKeySelective(PersistentLogins record);

	int updateByPrimaryKey(PersistentLogins record);

	/**
	 * 通过用户名和UUID值查询自动登录记录
	 * 
	 * @param username
	 *            用户名
	 * @param series
	 *            UUID值
	 */
	PersistentLogins selectByUsernameAndSeries(@Param("username") String username, @Param("series") String series);

	/**
	 * 通过用户名查询自动登录记录
	 * 
	 * @param username
	 *            用户名
	 */
	PersistentLogins selectByUsername(@Param("username") String username);
}

同样,这里也添加了两个其他的方法,它们对应的SQL语句是:

  <select id="selectByUsername" resultMap="BaseResultMap" parameterType="java.lang.String" >
    select 
    <include refid="Base_Column_List" />
    from persistent_logins where username = #{username,jdbcType=VARCHAR}
  </select>
    
  <select id="selectByUsernameAndSeries" resultMap="BaseResultMap" parameterType="java.util.Map" >
    select 
    <include refid="Base_Column_List" />
    from persistent_logins
    where username = #{username,jdbcType=VARCHAR} and series = #{series,jdbcType=VARCHAR}
  </select>

(4)Manager层(即:业务逻辑层):

i)UserManager接口:

package cn.zifangsky.manager;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import cn.zifangsky.model.User;

public interface UserManager {
	int deleteByPrimaryKey(Integer id);

	int insert(User user);

	int insertSelective(User user);

	User selectByPrimaryKey(Integer id);

	int updateByPrimaryKeySelective(User user);

	int updateByPrimaryKey(User user);
	
	/**
     * 根据用户名查用户详情
     * */
    User selectByName(String name);
    
	/**
	 * 登录
	 * 
	 * @param user
	 *            登录的用户信息
	 * @param rememberme
	 *            是否记住登录
	 * @param response
	 *            HttpServletResponse
	 * @return 根据传递的用户信息在数据库中查询到的用户详情
	 */
	User login(User user, boolean rememberme, HttpServletResponse response);
	
	/**
	 * 退出登录
	 * */
	void logout(HttpServletRequest request,HttpServletResponse response);
}

ii)PersistentLoginsManager接口:

package cn.zifangsky.manager;

import cn.zifangsky.model.PersistentLogins;

public interface PersistentLoginsManager {
    int deleteByPrimaryKey(Integer id);

    int insert(PersistentLogins pLogins);

    int insertSelective(PersistentLogins pLogins);

    PersistentLogins selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(PersistentLogins pLogins);

    int updateByPrimaryKey(PersistentLogins pLogins);
    /**
	 * 通过用户名和UUID值查询自动登录记录
	 * 
	 * @param username
	 *            用户名
	 * @param series
	 *            UUID值
	 */
	PersistentLogins selectByUsernameAndSeries(String username,String series);
	/**
	 * 通过用户名查询自动登录记录
	 * 
	 * @param username
	 *            用户名
	 */
	PersistentLogins selectByUsername(String username);	
}

iii)PersistentLoginsManagerImpl实现类:

package cn.zifangsky.manager.impl;

import javax.annotation.Resource;

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import cn.zifangsky.manager.PersistentLoginsManager;
import cn.zifangsky.mapper.PersistentLoginsMapper;
import cn.zifangsky.model.PersistentLogins;

@Service("persistentLoginsManagerImpl")
public class PersistentLoginsManagerImpl implements PersistentLoginsManager {
	@Resource(name="persistentLoginsMapper")
	private PersistentLoginsMapper persistentLoginsMapper;

	public int deleteByPrimaryKey(Integer id) {
		return persistentLoginsMapper.deleteByPrimaryKey(id);
	}

	@Override
	public int insert(PersistentLogins pLogins) {
		return persistentLoginsMapper.insert(pLogins);
	}

	@Override
	public int insertSelective(PersistentLogins pLogins) {
		return persistentLoginsMapper.insertSelective(pLogins);
	}

	@Override
	public PersistentLogins selectByPrimaryKey(Integer id) {
		return persistentLoginsMapper.selectByPrimaryKey(id);
	}

	@Override
	public int updateByPrimaryKeySelective(PersistentLogins pLogins) {
		return persistentLoginsMapper.updateByPrimaryKeySelective(pLogins);
	}

	@Override
	public int updateByPrimaryKey(PersistentLogins pLogins) {
		return persistentLoginsMapper.updateByPrimaryKey(pLogins);
	}

	public PersistentLogins selectByUsernameAndSeries(String username, String series) {
		if(StringUtils.isNotBlank(username) && StringUtils.isNotBlank(series))
			return persistentLoginsMapper.selectByUsernameAndSeries(username, series);
		else
			return null;
	}

	@Override
	public PersistentLogins selectByUsername(String username) {
		return persistentLoginsMapper.selectByUsername(username);
	}
}

iv)UserManagerImpl实现类:

package cn.zifangsky.manager.impl;

import java.util.Calendar;
import java.util.Date;
import java.util.UUID;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import cn.zifangsky.manager.UserManager;
import cn.zifangsky.mapper.UserMapper;
import cn.zifangsky.model.PersistentLogins;
import cn.zifangsky.model.User;
import cn.zifangsky.utils.CookieConstantTable;
import cn.zifangsky.utils.CookieUtils;
import cn.zifangsky.utils.EncryptionUtil;

@Service("userManagerImpl")
public class UserManagerImpl implements UserManager {
	

	@Resource(name = "userMapper")
	private UserMapper userMapper;
	@Resource(name = "persistentLoginsManagerImpl")
	private PersistentLoginsManagerImpl persistentLoginsManagerImpl;

	public int deleteByPrimaryKey(Integer id) {
		return userMapper.deleteByPrimaryKey(id);
	}

	@Override
	public int insert(User user) {
		return userMapper.insert(user);
	}

	@Override
	public int insertSelective(User user) {
		return userMapper.insertSelective(user);
	}

	@Override
	public User selectByPrimaryKey(Integer id) {
		return userMapper.selectByPrimaryKey(id);
	}

	@Override
	public int updateByPrimaryKeySelective(User user) {
		return userMapper.updateByPrimaryKeySelective(user);
	}

	@Override
	public int updateByPrimaryKey(User user) {
		return userMapper.updateByPrimaryKey(user);
	}

	@Override
	public User selectByName(String name) {
		return userMapper.selectByName(name);
	}

	@Override
	public User login(User user, boolean rememberme, HttpServletResponse response) {
		User result = new User();
		// 如果用户名和密码不为空,执行登录
		if (StringUtils.isNotBlank(user.getName()) && StringUtils.isNotBlank(user.getPassword())) {
			result = userMapper.selectByUser(user);
			// 如果rememberme为true,则保存cookie值,下次自动登录
			if (result != null && rememberme == true) {
				// 有效期
				Calendar calendar = Calendar.getInstance();
				calendar.add(Calendar.MONTH, 1); // 一个月
				Date validTime = calendar.getTime();
				// 精确到分的时间字符串
				String timeString = calendar.get(Calendar.YEAR) + "-" + calendar.get(Calendar.MONTH) + "-"
						+ calendar.get(Calendar.DAY_OF_MONTH) + "-" + calendar.get(Calendar.HOUR_OF_DAY) + "-"
						+ calendar.get(Calendar.MINUTE);

				// sha256加密用户信息
				String userInfoBySha256 = EncryptionUtil
						.sha256Hex(result.getName() + "_" + result.getPassword() + "_" + timeString + "_" + CookieConstantTable.salt);
				// UUID值
				String uuidString = UUID.randomUUID().toString();
				// Cookie值
				String cookieValue = EncryptionUtil.base64Encode(result.getName() + ":" + uuidString);

				// 在数据库中保存自动登录记录(如果已有该用户的记录则更新记录)
				PersistentLogins pLogin = persistentLoginsManagerImpl.selectByUsername(result.getName());
				if (pLogin == null) {
					pLogin = new PersistentLogins();
					pLogin.setUsername(result.getName());
					pLogin.setSeries(uuidString);
					pLogin.setToken(userInfoBySha256);
					pLogin.setValidtime(validTime);
					persistentLoginsManagerImpl.insertSelective(pLogin);
				}else{
					pLogin.setSeries(uuidString);
					pLogin.setToken(userInfoBySha256);
					pLogin.setValidtime(validTime);
					persistentLoginsManagerImpl.updateByPrimaryKeySelective(pLogin);
				}

				// 保存cookie
				CookieUtils.addCookie(response, CookieConstantTable.RememberMe, cookieValue, null);
			}

		}
		return result;
	}

	@Override
	public void logout(HttpServletRequest request, HttpServletResponse response) {
		//从session中获取用户详情
		User user = (User) request.getSession().getAttribute("user");
		//删除数据库中的自动登录记录
		PersistentLogins pLogins = persistentLoginsManagerImpl.selectByUsername(user.getName());
		if(pLogins != null)
			persistentLoginsManagerImpl.deleteByPrimaryKey(pLogins.getId());
		//清除session和用于自动登录的cookie
		request.getSession().removeAttribute("user");
		CookieUtils.delCookie(request, response, CookieConstantTable.RememberMe);
	}
}

注:CookieConstantTable类:

package cn.zifangsky.utils;

public class CookieConstantTable {
	// cookie的有效期默认为30天
	public final static int COOKIE_MAX_AGE = 60 * 60 * 24 * 30; 
	//cookie加密时的额外的salt
	public final static String salt = "www.zifangsky.cn";
	//自动登录的Cookie名
	public final static String RememberMe = "remember-me";
}

CookieUtils类:

package cn.zifangsky.utils;

import javax.servlet.http.HttpServletResponse;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

public class CookieUtils {

	

	/**
	 * 添加一个新Cookie
	 * 
	 * @author zifangsky
	 * @param response
	 *            HttpServletResponse
	 * @param cookie
	 *            新cookie
	 * 
	 * @return null
	 */
	public static void addCookie(HttpServletResponse response, Cookie cookie) {
		if (cookie != null)
			response.addCookie(cookie);
	}

	/**
	 * 添加一个新Cookie
	 * 
	 * @author zifangsky
	 * @param response
	 *            HttpServletResponse
	 * @param cookieName
	 *            cookie名称
	 * @param cookieValue
	 *            cookie值
	 * @param domain
	 *            cookie所属的子域
	 * @param httpOnly
	 *            是否将cookie设置成HttpOnly
	 * @param maxAge
	 *            设置cookie的最大生存期
	 * @param path
	 *            设置cookie路径
	 * @param secure
	 *            是否只允许HTTPS访问
	 * 
	 * @return null
	 */
	public static void addCookie(HttpServletResponse response, String cookieName, String cookieValue, String domain,
			boolean httpOnly, int maxAge, String path, boolean secure) {
		if (cookieName != null && !cookieName.equals("")) {
			if (cookieValue == null)
				cookieValue = "";

			Cookie newCookie = new Cookie(cookieName, cookieValue);
			if (domain != null)
				newCookie.setDomain(domain);

			newCookie.setHttpOnly(httpOnly);

			if (maxAge > 0)
				newCookie.setMaxAge(maxAge);

			if (path == null)
				newCookie.setPath("/");
			else
				newCookie.setPath(path);

			newCookie.setSecure(secure);

			addCookie(response, newCookie);
		}
	}

	/**
	 * 添加一个新Cookie
	 * 
	 * @author zifangsky
	 * @param response
	 *            HttpServletResponse
	 * @param cookieName
	 *            cookie名称
	 * @param cookieValue
	 *            cookie值
	 * @param domain
	 *            cookie所属的子域
	 * 
	 * @return null
	 */
	public static void addCookie(HttpServletResponse response, String cookieName, String cookieValue, String domain) {
		addCookie(response, cookieName, cookieValue, domain, true, CookieConstantTable.COOKIE_MAX_AGE, "/", false);
	}

	/**
	 * 根据Cookie名获取对应的Cookie
	 * 
	 * @author zifangsky
	 * @param request
	 *            HttpServletRequest
	 * @param cookieName
	 *            cookie名称
	 * 
	 * @return 对应cookie,如果不存在则返回null
	 */
	public static Cookie getCookie(HttpServletRequest request, String cookieName) {
		Cookie[] cookies = request.getCookies();

		if (cookies == null || cookieName == null || cookieName.equals(""))
			return null;

		for (Cookie c : cookies) {
			if (c.getName().equals(cookieName))
				return (Cookie) c;
		}
		return null;
	}

	/**
	 * 根据Cookie名获取对应的Cookie值
	 * 
	 * @author zifangsky
	 * @param request
	 *            HttpServletRequest
	 * @param cookieName
	 *            cookie名称
	 * 
	 * @return 对应cookie值,如果不存在则返回null
	 */
	public static String getCookieValue(HttpServletRequest request, String cookieName) {
		Cookie cookie = getCookie(request, cookieName);
		if (cookie == null)
			return null;
		else
			return cookie.getValue();
	}

	/**
	 * 删除指定Cookie
	 * 
	 * @author zifangsky
	 * @param response
	 *            HttpServletResponse
	 * @param cookie
	 *            待删除cookie
	 */
	public static void delCookie(HttpServletResponse response, Cookie cookie) {
		if (cookie != null) {
			cookie.setPath("/"); 
			cookie.setMaxAge(0);
			cookie.setValue(null);

			response.addCookie(cookie);
		}
	}

	/**
	 * 根据cookie名删除指定的cookie
	 * 
	 * @author zifangsky
	 * @param request
	 *            HttpServletRequest
	 * @param response
	 *            HttpServletResponse
	 * @param cookieName
	 *            待删除cookie名
	 */
	public static void delCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) {
		Cookie c = getCookie(request, cookieName);
		if (c != null && c.getName().equals(cookieName)) {
			delCookie(response, c);
		}
	}

	/**
	 * 根据cookie名修改指定的cookie
	 * 
	 * @author zifangsky
	 * @param request
	 *            HttpServletRequest
	 * @param response
	 *            HttpServletResponse
	 * @param cookieName
	 *            cookie名
	 * @param cookieValue
	 *            修改之后的cookie值
	 * @param domain
	 *            修改之后的domain值
	 */
	public static void editCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
			String cookieValue,String domain) {
		Cookie c = getCookie(request, cookieName);
		if (c != null && cookieName != null && !cookieName.equals("") && c.getName().equals(cookieName)) {
			addCookie(response, cookieName, cookieValue, domain);
		}
	}
}

EncryptionUtil类:

package cn.zifangsky.utils;

import java.io.UnsupportedEncodingException;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;

public class EncryptionUtil {
    /**
     * Base64 encode
     * */
    public static String base64Encode(String data){
        return Base64.encodeBase64String(data.getBytes());
    }
     
    /**
     * Base64 decode
     * @throws UnsupportedEncodingException 
     * */
    public static String base64Decode(String data) throws UnsupportedEncodingException{
        return new String(Base64.decodeBase64(data.getBytes()),"utf-8");
    }
     
    /**
     * md5
     * */
    public static String md5Hex(String data){
        return DigestUtils.md5Hex(data);
    }
     
    /**
     * sha1
     * */
    public static String sha1Hex(String data){
        return DigestUtils.sha1Hex(data);
    }
     
    /**
     * sha256
     * */
    public static String sha256Hex(String data){
        return DigestUtils.sha256Hex(data);
    }
     
}

这个方法类本质上调用的是 commons-codec-1.10.jar 这个jar包中的方法

在这个类中,关于退出登录就不用多做解释了,有详细注释自己参考下就行

关于这个登录方法,实际上我这里的执行流程是这样的:

  1. 根据用户名、密码执行登录验证

  2. 如果前台登录的form表单中勾选了“30天内自动登录”的选项,那么就执行下面的保存登录记录到persistent_logins这个表以及cookie中;如果没勾选,那么就直接将验证结果返回到controller中

  3. 执行保存记录的这个操作,实际上分为以下两步操作:a:向表persistent_logins保存记录,username是当前用户;series是获取的当前的UUID值;token是用户名、密码、cookie到期时间、以及自定义的salt经过sha256非对称加密之后的字符串;validTime是到期时间。b:向“remember-me”这个cookie保存的记录值是用户名和UUID值经过base64编码之后的字符串

  4. 保存记录,并返回到controller中操作

(5)Controller层:

package cn.zifangsky.controller;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import cn.zifangsky.manager.UserManager;
import cn.zifangsky.model.User;

@Controller
public class UserController {
	@Resource(name = "userManagerImpl")
	private UserManager userManager;

	/**
	 * 用户主页
	 */
	@RequestMapping("/user/index.html")
	public ModelAndView userIndex() {
		return new ModelAndView("user/index");
	}

	/**
	 * 登录校验
	 */
	@RequestMapping("/check.html")
	public ModelAndView login(@RequestParam("username") String username, @RequestParam("password") String password,
			@RequestParam(name = "remember-me", required = false) boolean rememberme, HttpServletRequest request,
			HttpServletResponse response, RedirectAttributes redirectAttributes) {
		HttpSession session = request.getSession();
		User user = new User();
		user.setName(username);
		user.setPassword(password);

		User result = userManager.login(user, rememberme, response);
		if (result != null) {
			ModelAndView mAndView = null;
			//登录之前地址
			String callback = (String) session.getAttribute("callback");
			session.removeAttribute("callback"); // 获取之后移除
			// 基本路径
			String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
					+ request.getContextPath();
			if (StringUtils.isNotBlank(callback)) {
				String[] urls = callback.split(basePath);
				if (urls.length == 2 && StringUtils.isNotBlank(urls[1])) {
					mAndView = new ModelAndView("redirect:" + urls[1]);
				}else{
					mAndView = new ModelAndView("redirect:/user/index.html");
				}
			}else{
				mAndView = new ModelAndView("redirect:/user/index.html");
			}
			
			session.setAttribute("user", result); // 登录成功之后加入session中
			redirectAttributes.addFlashAttribute("user", result); 

			return mAndView;
		} else {
			return new ModelAndView("redirect:/login.html");
		}
	}

	/**
	 * 退出登录
	 */
	@RequestMapping("/logout.html")
	public ModelAndView logout(HttpServletRequest request, HttpServletResponse response) {
		ModelAndView mAndView = new ModelAndView("redirect:/login.html");

		userManager.logout(request, response);

		return mAndView;
	}
}

在这里,对“callback”的操作主要是在拦截器中判断是否能够自动登录时,如果能够登录那么不用多说直接转到目标页面;如果不能通过验证,那么需要跳转到登录页面进行用户名、密码登录,这里的callback参数的目的就是在拦截器中验证失败跳转到登录页面之前,将本来想要访问的页面路径存储在session中,然后在controller中登录成功之后从session中取出,最后再重定向到那个目标页面

如果对这里的重定向等代码不太理解的话,建议可以参考下我在本篇文章开始时列举的那几篇文章

(6)几个测试使用的前台页面:

首先给出这几个页面之间的层次关系:

wKioL1hHgtnjVUbVAABCn4Rtvug106.png

i)login.jsp:

<%@page import="java.security.SecureRandom"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>    
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<base href="<%=basePath%>">
<title>SpringMVC Cookie Demo</title>
<%
	SecureRandom random = new SecureRandom();
	random.setSeed(8738);
	double _csrf = random.nextDouble();
	session.setAttribute("_csrf", _csrf);
%>
</head>
<body>
	<div align="center">
		<h2>SpringMVC Cookie Demo</h2>
		<form action="check.html" method="post">
			<table>
				<tr>
					<td>用户名:</td>
					<td><input type="text" name="username" /></td>
				</tr>
				<tr>
					<td>密码:</td>
					<td><input type="password" name="password" /></td>
				</tr>
				<tr>
					<td><input name="remember-me" type="checkbox">30天内自动登录</input></td>
				</tr>
				<tr>
					<td colspan="2" align="center"><input type="submit" value="登录" />
						<input type="reset" value="重置" /></td>
				</tr>			
			</table>
			<input type="hidden" name="_csrf" value="<%=_csrf %>" />
		</form>
		
	</div>
</body>
</html>

登录用的form表单

ii)user目录下的index.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>    
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<base href="<%=basePath%>">
<title>SpringMVC Cookie Demo</title>
</head>
<body>
	<div align="center">
		<h2>SpringMVC Cookie Demo</h2>
		<div align="right">
			<a href="logout.html">退出登录</a>
		</div>
		Hello <b>${user.name}</b>,welcome to user home page!
	</div>
</body>
</html>

iii)callback.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<base href="<%=basePath%>">
	<title>SpringMVC Cookie Demo</title>
</head>
<body>
	<div align="center">
		<h2>SpringMVC Cookie Demo</h2>
		测试 callback 页面跳转
	</div>
</body>
</html>

这个页面主要是为了测试登录之后是否能够跳转到原来想要访问的页面

(7)拦截器UserInterceptor:

package cn.zifangsky.interceptor;

import java.util.Calendar;
import java.util.Date;
import java.util.UUID;

import javax.annotation.Resource;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import cn.zifangsky.manager.UserManager;
import cn.zifangsky.manager.impl.PersistentLoginsManagerImpl;
import cn.zifangsky.model.PersistentLogins;
import cn.zifangsky.model.User;
import cn.zifangsky.utils.CookieConstantTable;
import cn.zifangsky.utils.CookieUtils;
import cn.zifangsky.utils.EncryptionUtil;

public class UserInterceptor extends HandlerInterceptorAdapter {
	@Resource(name = "persistentLoginsManagerImpl")
	private PersistentLoginsManagerImpl persistentLoginsManagerImpl;
	@Resource(name = "userManagerImpl")
	private UserManager userManager;

	/**
	 * 用于处理自动登录
	 */
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		HttpSession session = request.getSession();
		User user = (User) session.getAttribute("user");

		// 已登录
		if (user != null) {
			return true;
		} else {
			// 从cookie中取值
			Cookie rememberme = CookieUtils.getCookie(request, CookieConstantTable.RememberMe);
			if (rememberme != null) {
				String cookieValue = EncryptionUtil.base64Decode(rememberme.getValue());

				String[] cValues = cookieValue.split(":");
				if (cValues.length == 2) {
					String usernameByCookie = cValues[0]; // 获取用户名
					String uuidByCookie = cValues[1]; // 获取UUID值
					// 到数据库中查询自动登录记录
					PersistentLogins pLogins = persistentLoginsManagerImpl.selectByUsernameAndSeries(usernameByCookie,
							uuidByCookie);
					if (pLogins != null) {
						String savedToken = pLogins.getToken(); // 数据库中保存的密文

						// 获取有效时间
						Date savedValidtime = pLogins.getValidtime();
						Date currentTime = new Date();

						// 如果还在cookie有效期之内,继续判断是否可以自动登录
						if (currentTime.before(savedValidtime)) {
							User u = userManager.selectByName(usernameByCookie);
							if (u != null) {
								Calendar calendar = Calendar.getInstance();
								calendar.setTime(pLogins.getValidtime());
								// 精确到分的时间字符串
								String timeString = calendar.get(Calendar.YEAR) + "-" + calendar.get(Calendar.MONTH)
										+ "-" + calendar.get(Calendar.DAY_OF_MONTH) + "-"
										+ calendar.get(Calendar.HOUR_OF_DAY) + "-" + calendar.get(Calendar.MINUTE);
								// 为了校验而生成的密文
								String newToken = EncryptionUtil.sha256Hex(u.getName() + "_" + u.getPassword() + "_"
										+ timeString + "_" + CookieConstantTable.salt);

								// 校验sha256加密的值,如果不一样则表示用户部分信息已被修改,需要重新登录
								if (savedToken.equals(newToken)) {
									/**
									 * 为了提高安全性,每次登录之后都更新自动登录的cookie值
									 */
									// 更新cookie值
									String uuidNewString = UUID.randomUUID().toString();
									String newCookieValue = EncryptionUtil
											.base64Encode(u.getName() + ":" + uuidNewString);
									CookieUtils.editCookie(request, response, CookieConstantTable.RememberMe,
											newCookieValue, null);

									// 更新数据库
									pLogins.setSeries(uuidNewString);
									persistentLoginsManagerImpl.updateByPrimaryKeySelective(pLogins);

									/**
									 * 将用户加到session中,不退出浏览器时就只需判断session即可
									 */
									session.setAttribute("user", u);

									return true;  //校验成功,此次拦截操作完成
								} else { // 用户部分信息被修改,删除cookie并清空数据库中的记录
									CookieUtils.delCookie(response, rememberme);
									persistentLoginsManagerImpl.deleteByPrimaryKey(pLogins.getId());
								}
							}
						} else { // 超过保存的有效期,删除cookie并清空数据库中的记录
							CookieUtils.delCookie(response, rememberme);
							persistentLoginsManagerImpl.deleteByPrimaryKey(pLogins.getId());
						}
					}
				}
			}
			//将来源地址存放在session中,登录成功之后跳回原地址
			String callback = request.getRequestURL().toString();
			session.setAttribute("callback", callback);
			response.sendRedirect(
					request.getContextPath() + "/login.html?callback=" + callback);
			return false;
		}
	}

	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {

		super.afterCompletion(request, response, handler, ex);
	}

}

在这里,验证流程如下:

  1. 如果在session中存在“user”对象,那么验证通过,允许访问

  2. 如果session中没有,则需要取出cookie中名字为“remember-me”对应的值,用于下一步验证

  3. 根据base64解码之后的用户名和UUID值从表“persistent_logins”查询记录

  4. 取出数据库中保存的token值和到期时间,根据同样的加密方法加密待校验的密文,然后和数据库中的token相比较

  5. 如果一样,则表示可以自动登录。同时,为了提高安全性,在校验成功之后更新用户端的用于自动登录的cookie记录

  6. 将“user”对象添加到session中,本次拦截器校验通过

当然,我这里只是简单叙述了下流程,更具体的流程可以自行参考代码中的注释

三 测试

(1)测试使用cookie实现自动登录:

启动项目后,访问:http://localhost:9180/CookieDemo/login.html

输入用户名、密码并勾上“30天内自动登录”:

wKiom1hHg4eTznS3AAA2J-f07mo184.png

点击登录之后,可以发现页面跳转到了:http://localhost:9180/CookieDemo/user/index.html

同时生成了一条名为“remember-me”的cookie记录值,其值是:YWRtaW46YzhjYTU3NjktNDhjZi00NWQ4LTk4YzQtM2QzMDMwNWVlMWY5

wKiom1hHg6Hx0cYPAAD3iu6yDsQ301.png如果使用在线base64解码工具解码之后可以发现,这个cookie值的原文是:

wKiom1hHg7uBvGUVAAAlXwXaPZc566.png

恰好与数据库中persistent_logins表中的记录相对应:

wKioL1hHg9WxaIwKAAArV8aItfE858.png接着,退出浏览器之后再次打开该浏览器访问:http://localhost:9180/CookieDemo/user/index.html

可以发现:可以直接访问该页面,同时已经是登录状态了。到此,我们的目的已经达成了

(2)测试登录之后跳回到原来想要访问的页面:

删除浏览器中的“remember-me”这个cookie,或者删掉数据库中persistent_logins表中的记录,然后在退出登录之后访问:http://localhost:9180/CookieDemo/user/callback.html

可以发现,页面已经被自动重定向到登录页面了

wKioL1hHg_TBlXhZAAB2Yukk0hQ316.png

接着,输入用户名、密码登录,可以发现:在登录成功之后能够正常跳转到我们原来请求的页面:

wKiom1hHhBPBn54iAABt6x0h8zQ393.png附:本次测试项目的完整源代码:

PS:上面图片中的水印是我个人博客的域名,因此还请管理员手下留情不要给我标为“转载文章”,谢谢!!!