SpringMVC中使用Interceptor+Cookie實如今必定天數以內自動登陸

一 簡介

本篇文章主要介紹:在SpringMVC中如何使用Interceptor+Cookie實如今必定天數以內自動登陸的功能。同時還介紹「若是校驗失敗則跳轉到登陸頁面,在輸入用戶名、密碼等完成登陸以後又自動跳轉到原頁面」的功能實現html

本次測試環境是SSM框架,在正式介紹本篇文章以前,建議須要熟悉如下前置知識點:java

  • Mybatis中使用mybatis-generator結合Ant腳本快速自動生成Model、Mapper等文件(PS:這是爲了快速生成一些基本文件)    https://www.zifangsky.cn/431.htmlmysql

  • SpringMVC經過配置mvc:view-controller直接解析到視圖頁面(PS:這是爲了簡化controller中的代碼)    https://www.zifangsky.cn/648.htmlweb

  • 基於SpringMVC的Cookie經常使用操做詳解(PS:這是介紹cookie的經常使用操做)    https://www.zifangsky.cn/665.htmlspring

  • SpringMVC中使用forward和redirect進行轉發和重定向以及重定向時如何傳參詳解(PS:這是介紹重定向時如何傳參的問題)    https://www.zifangsky.cn/661.htmlsql

  • 在SpringMVC中使用攔截器(interceptor)攔截CSRF***(PS:這是介紹攔截器的一些基礎用法)    https://www.zifangsky.cn/671.html數據庫

二 代碼實現

(1)數據庫表設計:apache

我這裏採用的是MySQL,同時設計了兩張表,分別是:userpersistent_logins瀏覽器

i)user表:spring-mvc

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:上面圖片中的水印是我我的博客的域名,所以還請管理員手下留情不要給我標爲「轉載文章」,謝謝!!!

相關文章
相關標籤/搜索