您好,登錄后才能下訂單哦!
本篇文章主要介紹:在SpringMVC中如何使用Interceptor+Cookie實(shí)現(xiàn)在一定天數(shù)之內(nèi)自動(dòng)登錄的功能。同時(shí)還介紹“如果校驗(yàn)失敗則跳轉(zhuǎn)到登錄頁(yè)面,在輸入用戶(hù)名、密碼等完成登錄之后又自動(dòng)跳轉(zhuǎn)到原頁(yè)面”的功能實(shí)現(xiàn)
本次測(cè)試環(huán)境是SSM框架,在正式介紹本篇文章之前,建議需要熟悉以下前置知識(shí)點(diǎn):
Mybatis中使用mybatis-generator結(jié)合Ant腳本快速自動(dòng)生成Model、Mapper等文件(PS:這是為了快速生成一些基本文件) https://www.zifangsky.cn/431.html
SpringMVC通過(guò)配置mvc:view-controller直接解析到視圖頁(yè)面(PS:這是為了簡(jiǎn)化controller中的代碼) https://www.zifangsky.cn/648.html
基于SpringMVC的Cookie常用操作詳解(PS:這是介紹cookie的常用操作) https://www.zifangsky.cn/665.html
SpringMVC中使用forward和redirect進(jìn)行轉(zhuǎn)發(fā)和重定向以及重定向時(shí)如何傳參詳解(PS:這是介紹重定向時(shí)如何傳參的問(wèn)題) https://www.zifangsky.cn/661.html
在SpringMVC中使用攔截器(interceptor)攔截CSRF***(PS:這是介紹攔截器的一些基礎(chǔ)用法) https://www.zifangsky.cn/671.html
(1)數(shù)據(jù)庫(kù)表設(shè)計(jì):
我這里采用的是MySQL,同時(shí)設(shè)計(jì)了兩張表,分別是:user和persistent_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');
這張表很簡(jiǎn)單,就是一張普通的用戶(hù)表
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;
這張表是用戶(hù)校驗(yàn)用戶(hù)自動(dòng)登錄的表。設(shè)計(jì)這張表的原因是我看過(guò)一些網(wǎng)上的文章介紹使用cookie自動(dòng)登錄,但是他們基本上都是將用戶(hù)名、密碼、salt等字符串拼接之后md5加密然后保存在cookie中。雖然使用了md5這類(lèi)非對(duì)稱(chēng)加密方式,但是將密碼這類(lèi)關(guān)鍵信息保存在用戶(hù)端,我覺(jué)得是不太靠譜的。因此設(shè)計(jì)了這張表,將用戶(hù)名、密碼等關(guān)鍵信息加密之后的數(shù)據(jù)保存到這張表中,在用戶(hù)的cookie里只保存了沒(méi)有特殊含義的UUID值以及用戶(hù)名
這張表中的幾個(gè)字段的含義分別是:
id 主鍵
username 用戶(hù)名
series 用戶(hù)使用密碼登錄成功之后獲取的一個(gè)UUID值,同時(shí)用戶(hù)端保存的cookie記錄就是:EncryptionUtil.base64Encode(用戶(hù)名:此UUID值)
token 在攔截器中校驗(yàn)是否能夠登錄的密文,其加密方式是:EncryptionUtil.sha256Hex(用戶(hù)名 + “_” + 密碼 + “_” + 自動(dòng)登錄失效的時(shí)間點(diǎn)的字符串 + “_” + 自定義的salt)
validTime 自動(dòng)登錄失效的時(shí)間,即:這個(gè)時(shí)間點(diǎn)之后只能重新用用戶(hù)名、密碼登錄,如果在重新登錄時(shí)勾選了“30天內(nèi)自動(dòng)登錄”則更新該用戶(hù)在persistent_logins這個(gè)表中的自動(dòng)登錄記錄
(2)幾個(gè)基本的配置文件:
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> <!-- 這兩個(gè)listener不加會(huì)出現(xiàn)無(wú)法依賴(lài)注入問(wèn)題 --> <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"> <!-- 配置數(shù)據(jù)源 --> <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> <!--連接池中保留的最小連接數(shù)。 --> <property name="minPoolSize"> <value>5</value> </property> <!--連接池中保留的最大連接數(shù)。Default: 15 --> <property name="maxPoolSize"> <value>30</value> </property> <!--初始化時(shí)獲取的連接數(shù),取值應(yīng)在minPoolSize與maxPoolSize之間。Default: 3 --> <property name="initialPoolSize"> <value>10</value> </property> <!--最大空閑時(shí)間,60秒內(nèi)未使用則連接被丟棄。若為0則永不丟棄。Default: 0 --> <property name="maxIdleTime"> <value>60</value> </property> <!--當(dāng)連接池中的連接耗盡的時(shí)候c3p0一次同時(shí)獲取的連接數(shù)。Default: 3 --> <property name="acquireIncrement"> <value>5</value> </property> <!--JDBC的標(biāo)準(zhǔn)參數(shù),用以控制數(shù)據(jù)源內(nèi)加載的PreparedStatements數(shù)量。但由于預(yù)緩存的statements 屬于單個(gè) connection而不是整個(gè)連接池。所以設(shè)置這個(gè)參數(shù)需要考慮到多方面的因素。 如果maxStatements與maxStatementsPerConnection均為0,則緩存被關(guān)閉。Default: 0 --> <property name="maxStatements"> <value>0</value> </property> <!--每60秒檢查所有連接池中的空閑連接。Default: 0 --> <property name="idleConnectionTestPeriod"> <value>60</value> </property> <!--定義在從數(shù)據(jù)庫(kù)獲取新連接失敗后重復(fù)嘗試的次數(shù)。Default: 30 --> <property name="acquireRetryAttempts"> <value>30</value> </property> <!--獲取連接失敗將會(huì)引起所有等待連接池來(lái)獲取連接的線(xiàn)程拋出異常。但是數(shù)據(jù)源仍有效 保留,并在下次調(diào)用 getConnection()的時(shí)候繼續(xù)嘗試獲取連接。如果設(shè)為true,那么在嘗試 獲取連接失敗后該數(shù)據(jù)源將申明已斷開(kāi)并永久關(guān)閉。Default: false --> <property name="breakAfterAcquireFailure"> <value>true</value> </property> <!--因性能消耗大請(qǐng)只在需要的時(shí)候使用它。如果設(shè)為true那么在每個(gè)connection提交的 時(shí)候都將校驗(yàn)其有效性。建議 使用idleConnectionTestPeriod或automaticTestTable 等方法來(lái)提升連接測(cè)試的性能。Default: false --> <property name="testConnectionOnCheckout"> <value>false</value> </property> </bean> <!-- MyBatis相關(guān)配置 --> <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> <!-- 事務(wù)相關(guān)配置 --> <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"/> <!-- 配置直接轉(zhuǎn)發(fā)的頁(yè)面 --> <mvc:view-controller path="/login.html" view-name="login" /> <mvc:view-controller path="/user/callback.html" view-name="user/callback" /> <!-- 攔截器 --> <mvc:interceptors> <mvc:interceptor> <!-- 對(duì)登錄操作進(jìn)行攔截 --> <mvc:mapping path="/check.html"/> <bean class="cn.zifangsky.interceptor.LoginInterceptor" /> </mvc:interceptor> <mvc:interceptor> <!-- 對(duì)/user/**的請(qǐng)求進(jìn)行攔截 --> <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行的攔截器的配置,這個(gè)攔截器就是用于使用cookie自動(dòng)登錄的攔截器,上面那個(gè)用于攔截登錄時(shí)的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); /** * 根據(jù)用戶(hù)信息查用戶(hù)詳情(登錄) * */ User selectByUser(User user); /** * 根據(jù)用戶(hù)名查用戶(hù)詳情 * */ User selectByName(String name); }
這里除了使用插件自動(dòng)生成的幾個(gè)方法之外,還添加了兩個(gè)其他的方法,它們對(duì)應(yīng)的SQL語(yǔ)句是:
<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); /** * 通過(guò)用戶(hù)名和UUID值查詢(xún)自動(dòng)登錄記錄 * * @param username * 用戶(hù)名 * @param series * UUID值 */ PersistentLogins selectByUsernameAndSeries(@Param("username") String username, @Param("series") String series); /** * 通過(guò)用戶(hù)名查詢(xún)自動(dòng)登錄記錄 * * @param username * 用戶(hù)名 */ PersistentLogins selectByUsername(@Param("username") String username); }
同樣,這里也添加了兩個(gè)其他的方法,它們對(duì)應(yīng)的SQL語(yǔ)句是:
<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層(即:業(yè)務(wù)邏輯層):
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); /** * 根據(jù)用戶(hù)名查用戶(hù)詳情 * */ User selectByName(String name); /** * 登錄 * * @param user * 登錄的用戶(hù)信息 * @param rememberme * 是否記住登錄 * @param response * HttpServletResponse * @return 根據(jù)傳遞的用戶(hù)信息在數(shù)據(jù)庫(kù)中查詢(xún)到的用戶(hù)詳情 */ 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); /** * 通過(guò)用戶(hù)名和UUID值查詢(xún)自動(dòng)登錄記錄 * * @param username * 用戶(hù)名 * @param series * UUID值 */ PersistentLogins selectByUsernameAndSeries(String username,String series); /** * 通過(guò)用戶(hù)名查詢(xún)自動(dòng)登錄記錄 * * @param username * 用戶(hù)名 */ PersistentLogins selectByUsername(String username); }
iii)PersistentLoginsManagerImpl實(shí)現(xiàn)類(lèi):
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實(shí)現(xiàn)類(lèi):
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(); // 如果用戶(hù)名和密碼不為空,執(zhí)行登錄 if (StringUtils.isNotBlank(user.getName()) && StringUtils.isNotBlank(user.getPassword())) { result = userMapper.selectByUser(user); // 如果rememberme為true,則保存cookie值,下次自動(dòng)登錄 if (result != null && rememberme == true) { // 有效期 Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.MONTH, 1); // 一個(gè)月 Date validTime = calendar.getTime(); // 精確到分的時(shí)間字符串 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加密用戶(hù)信息 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); // 在數(shù)據(jù)庫(kù)中保存自動(dòng)登錄記錄(如果已有該用戶(hù)的記錄則更新記錄) 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中獲取用戶(hù)詳情 User user = (User) request.getSession().getAttribute("user"); //刪除數(shù)據(jù)庫(kù)中的自動(dòng)登錄記錄 PersistentLogins pLogins = persistentLoginsManagerImpl.selectByUsername(user.getName()); if(pLogins != null) persistentLoginsManagerImpl.deleteByPrimaryKey(pLogins.getId()); //清除session和用于自動(dòng)登錄的cookie request.getSession().removeAttribute("user"); CookieUtils.delCookie(request, response, CookieConstantTable.RememberMe); } }
注:CookieConstantTable類(lèi):
package cn.zifangsky.utils; public class CookieConstantTable { // cookie的有效期默認(rèn)為30天 public final static int COOKIE_MAX_AGE = 60 * 60 * 24 * 30; //cookie加密時(shí)的額外的salt public final static String salt = "www.zifangsky.cn"; //自動(dòng)登錄的Cookie名 public final static String RememberMe = "remember-me"; }
CookieUtils類(lèi):
package cn.zifangsky.utils; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; public class CookieUtils { /** * 添加一個(gè)新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); } /** * 添加一個(gè)新Cookie * * @author zifangsky * @param response * HttpServletResponse * @param cookieName * cookie名稱(chēng) * @param cookieValue * cookie值 * @param domain * cookie所屬的子域 * @param httpOnly * 是否將cookie設(shè)置成HttpOnly * @param maxAge * 設(shè)置cookie的最大生存期 * @param path * 設(shè)置cookie路徑 * @param secure * 是否只允許HTTPS訪(fǎng)問(wèn) * * @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); } } /** * 添加一個(gè)新Cookie * * @author zifangsky * @param response * HttpServletResponse * @param cookieName * cookie名稱(chēng) * @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); } /** * 根據(jù)Cookie名獲取對(duì)應(yīng)的Cookie * * @author zifangsky * @param request * HttpServletRequest * @param cookieName * cookie名稱(chēng) * * @return 對(duì)應(yīng)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; } /** * 根據(jù)Cookie名獲取對(duì)應(yīng)的Cookie值 * * @author zifangsky * @param request * HttpServletRequest * @param cookieName * cookie名稱(chēng) * * @return 對(duì)應(yīng)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); } } /** * 根據(jù)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); } } /** * 根據(jù)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類(lèi):
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); } }
這個(gè)方法類(lèi)本質(zhì)上調(diào)用的是 commons-codec-1.10.jar 這個(gè)jar包中的方法
在這個(gè)類(lèi)中,關(guān)于退出登錄就不用多做解釋了,有詳細(xì)注釋自己參考下就行
關(guān)于這個(gè)登錄方法,實(shí)際上我這里的執(zhí)行流程是這樣的:
根據(jù)用戶(hù)名、密碼執(zhí)行登錄驗(yàn)證
如果前臺(tái)登錄的form表單中勾選了“30天內(nèi)自動(dòng)登錄”的選項(xiàng),那么就執(zhí)行下面的保存登錄記錄到persistent_logins這個(gè)表以及cookie中;如果沒(méi)勾選,那么就直接將驗(yàn)證結(jié)果返回到controller中
執(zhí)行保存記錄的這個(gè)操作,實(shí)際上分為以下兩步操作:a:向表persistent_logins保存記錄,username是當(dāng)前用戶(hù);series是獲取的當(dāng)前的UUID值;token是用戶(hù)名、密碼、cookie到期時(shí)間、以及自定義的salt經(jīng)過(guò)sha256非對(duì)稱(chēng)加密之后的字符串;validTime是到期時(shí)間。b:向“remember-me”這個(gè)cookie保存的記錄值是用戶(hù)名和UUID值經(jīng)過(guò)base64編碼之后的字符串
保存記錄,并返回到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; /** * 用戶(hù)主頁(yè) */ @RequestMapping("/user/index.html") public ModelAndView userIndex() { return new ModelAndView("user/index"); } /** * 登錄校驗(yàn) */ @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; } }
在這里,對(duì)“callback”的操作主要是在攔截器中判斷是否能夠自動(dòng)登錄時(shí),如果能夠登錄那么不用多說(shuō)直接轉(zhuǎn)到目標(biāo)頁(yè)面;如果不能通過(guò)驗(yàn)證,那么需要跳轉(zhuǎn)到登錄頁(yè)面進(jìn)行用戶(hù)名、密碼登錄,這里的callback參數(shù)的目的就是在攔截器中驗(yàn)證失敗跳轉(zhuǎn)到登錄頁(yè)面之前,將本來(lái)想要訪(fǎng)問(wèn)的頁(yè)面路徑存儲(chǔ)在session中,然后在controller中登錄成功之后從session中取出,最后再重定向到那個(gè)目標(biāo)頁(yè)面
如果對(duì)這里的重定向等代碼不太理解的話(huà),建議可以參考下我在本篇文章開(kāi)始時(shí)列舉的那幾篇文章
(6)幾個(gè)測(cè)試使用的前臺(tái)頁(yè)面:
首先給出這幾個(gè)頁(yè)面之間的層次關(guān)系:
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"> <h3>SpringMVC Cookie Demo</h3> <form action="check.html" method="post"> <table> <tr> <td>用戶(hù)名:</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天內(nèi)自動(dòng)登錄</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"> <h3>SpringMVC Cookie Demo</h3> <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"> <h3>SpringMVC Cookie Demo</h3> 測(cè)試 callback 頁(yè)面跳轉(zhuǎn) </div> </body> </html>
這個(gè)頁(yè)面主要是為了測(cè)試登錄之后是否能夠跳轉(zhuǎn)到原來(lái)想要訪(fǎng)問(wèn)的頁(yè)面
(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; /** * 用于處理自動(dòng)登錄 */ 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]; // 獲取用戶(hù)名 String uuidByCookie = cValues[1]; // 獲取UUID值 // 到數(shù)據(jù)庫(kù)中查詢(xún)自動(dòng)登錄記錄 PersistentLogins pLogins = persistentLoginsManagerImpl.selectByUsernameAndSeries(usernameByCookie, uuidByCookie); if (pLogins != null) { String savedToken = pLogins.getToken(); // 數(shù)據(jù)庫(kù)中保存的密文 // 獲取有效時(shí)間 Date savedValidtime = pLogins.getValidtime(); Date currentTime = new Date(); // 如果還在cookie有效期之內(nèi),繼續(xù)判斷是否可以自動(dòng)登錄 if (currentTime.before(savedValidtime)) { User u = userManager.selectByName(usernameByCookie); if (u != null) { Calendar calendar = Calendar.getInstance(); calendar.setTime(pLogins.getValidtime()); // 精確到分的時(shí)間字符串 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); // 為了校驗(yàn)而生成的密文 String newToken = EncryptionUtil.sha256Hex(u.getName() + "_" + u.getPassword() + "_" + timeString + "_" + CookieConstantTable.salt); // 校驗(yàn)sha256加密的值,如果不一樣則表示用戶(hù)部分信息已被修改,需要重新登錄 if (savedToken.equals(newToken)) { /** * 為了提高安全性,每次登錄之后都更新自動(dòng)登錄的cookie值 */ // 更新cookie值 String uuidNewString = UUID.randomUUID().toString(); String newCookieValue = EncryptionUtil .base64Encode(u.getName() + ":" + uuidNewString); CookieUtils.editCookie(request, response, CookieConstantTable.RememberMe, newCookieValue, null); // 更新數(shù)據(jù)庫(kù) pLogins.setSeries(uuidNewString); persistentLoginsManagerImpl.updateByPrimaryKeySelective(pLogins); /** * 將用戶(hù)加到session中,不退出瀏覽器時(shí)就只需判斷session即可 */ session.setAttribute("user", u); return true; //校驗(yàn)成功,此次攔截操作完成 } else { // 用戶(hù)部分信息被修改,刪除cookie并清空數(shù)據(jù)庫(kù)中的記錄 CookieUtils.delCookie(response, rememberme); persistentLoginsManagerImpl.deleteByPrimaryKey(pLogins.getId()); } } } else { // 超過(guò)保存的有效期,刪除cookie并清空數(shù)據(jù)庫(kù)中的記錄 CookieUtils.delCookie(response, rememberme); persistentLoginsManagerImpl.deleteByPrimaryKey(pLogins.getId()); } } } } //將來(lái)源地址存放在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); } }
在這里,驗(yàn)證流程如下:
如果在session中存在“user”對(duì)象,那么驗(yàn)證通過(guò),允許訪(fǎng)問(wèn)
如果session中沒(méi)有,則需要取出cookie中名字為“remember-me”對(duì)應(yīng)的值,用于下一步驗(yàn)證
根據(jù)base64解碼之后的用戶(hù)名和UUID值從表“persistent_logins”查詢(xún)記錄
取出數(shù)據(jù)庫(kù)中保存的token值和到期時(shí)間,根據(jù)同樣的加密方法加密待校驗(yàn)的密文,然后和數(shù)據(jù)庫(kù)中的token相比較
如果一樣,則表示可以自動(dòng)登錄。同時(shí),為了提高安全性,在校驗(yàn)成功之后更新用戶(hù)端的用于自動(dòng)登錄的cookie記錄
將“user”對(duì)象添加到session中,本次攔截器校驗(yàn)通過(guò)
當(dāng)然,我這里只是簡(jiǎn)單敘述了下流程,更具體的流程可以自行參考代碼中的注釋
(1)測(cè)試使用cookie實(shí)現(xiàn)自動(dòng)登錄:
啟動(dòng)項(xiàng)目后,訪(fǎng)問(wèn):http://localhost:9180/CookieDemo/login.html
輸入用戶(hù)名、密碼并勾上“30天內(nèi)自動(dòng)登錄”:
點(diǎn)擊登錄之后,可以發(fā)現(xiàn)頁(yè)面跳轉(zhuǎn)到了:http://localhost:9180/CookieDemo/user/index.html
同時(shí)生成了一條名為“remember-me”的cookie記錄值,其值是:YWRtaW46YzhjYTU3NjktNDhjZi00NWQ4LTk4YzQtM2QzMDMwNWVlMWY5
如果使用在線(xiàn)base64解碼工具解碼之后可以發(fā)現(xiàn),這個(gè)cookie值的原文是:
恰好與數(shù)據(jù)庫(kù)中persistent_logins表中的記錄相對(duì)應(yīng):
接著,退出瀏覽器之后再次打開(kāi)該瀏覽器訪(fǎng)問(wèn):http://localhost:9180/CookieDemo/user/index.html
可以發(fā)現(xiàn):可以直接訪(fǎng)問(wèn)該頁(yè)面,同時(shí)已經(jīng)是登錄狀態(tài)了。到此,我們的目的已經(jīng)達(dá)成了
(2)測(cè)試登錄之后跳回到原來(lái)想要訪(fǎng)問(wèn)的頁(yè)面:
刪除瀏覽器中的“remember-me”這個(gè)cookie,或者刪掉數(shù)據(jù)庫(kù)中persistent_logins表中的記錄,然后在退出登錄之后訪(fǎng)問(wèn):http://localhost:9180/CookieDemo/user/callback.html
可以發(fā)現(xiàn),頁(yè)面已經(jīng)被自動(dòng)重定向到登錄頁(yè)面了
接著,輸入用戶(hù)名、密碼登錄,可以發(fā)現(xiàn):在登錄成功之后能夠正常跳轉(zhuǎn)到我們?cè)瓉?lái)請(qǐng)求的頁(yè)面:
附:本次測(cè)試項(xiàng)目的完整源代碼:
鏈接:http://pan.baidu.com/s/1nvo72a9 密碼:dkxa
PS:上面圖片中的水印是我個(gè)人博客的域名,因此還請(qǐng)管理員手下留情不要給我標(biāo)為“轉(zhuǎn)載文章”,謝謝?。?!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。