(一)案例背景介绍
首先申请,本案例主要针对有Cookie的客户端,没有Cookie的客户端可以参照执行。
在我们登录网站时,有些网站需要我们登录以后才能浏览。而在我们登陆以后,在需要登录之后才能浏览的页面之间进行跳转的时候是无需登录验证的,这里使用的主要是我们的Session技术。
而有些网页,只要我们登陆一次,一段时间内就可以不用再登录,这里的免登陆使用的就是Cookie技术。
(二)Session和Cookie介绍
1.Session
Session就是Web应用程序中经常使用来记录客户端状态的对象。Session是服务器端使用的一种记录客户端状态的机制,使用上比Cookie简单一些,相应的也增加了服务器的存储压力。
Session是在服务器端程序运行的过程中创建的,在Java中是通过调用HttpServletRequest的getSession方法(使用true作为参数)创建。在创建了Session的同时,服务器会为该Session生成唯一的Session id,而这个Session id在随后的请求中会被用来重新获得已经创建的Session;在Session被创建之后,就可以调用Session相关的方法往Session中增加内容了,而这些内容只会保存在服务器中,发到客户端的只有Session id;当客户端再次发送请求的时候,会将这个Session id带上,服务器接受到请求之后就会依据Session id找到相应的Session,从而再次使用之。正是这样一个过程,用户的状态也就得以保持了。
Session的生存周期:一旦当前浏览器被关闭了,Session就消失了(当前页面关闭的话,Session依然存在)。
2.Cookie
由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份。怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。
Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。
(二)实现细节
1.第一步:登录校验(主要用于从未登陆过或登录过期的用户)
package com.imooc.service.impl;
import com.imooc.bean.SysUser;
import com.imooc.constant.SessionKeyConst;
import com.imooc.constant.PageCodeEnum;
import com.imooc.dao.SysUserDao;
import com.imooc.dto.SysUserDto;
import com.imooc.service.LoginService;
import com.imooc.util.Md5Util;
import com.imooc.util.VerifyUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* @author **
* @date 2018/6/13 17:01
*/
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private SysUserDao sysUserDao;
@Override
public boolean validate(SysUserDto sysUserDto, RedirectAttributes redirectAttributes
, HttpServletRequest request, HttpServletResponse response) {
if (!VerifyUtil.validateUsername(sysUserDto.getUsername())){
redirectAttributes.addFlashAttribute(PageCodeEnum.KEY, PageCodeEnum.USERNAME_ERROR);
return false;
}
if (!VerifyUtil.validateUsername(sysUserDto.getPassword())){
redirectAttributes.addFlashAttribute(PageCodeEnum.KEY, PageCodeEnum.PASSWORD_ERROR);
return false;
}
SysUser sysUser = new SysUser();
BeanUtils.copyProperties(sysUserDto, sysUser);
List sysUserList = sysUserDao.selectUser(sysUser);
if (sysUserList.size() != 1){
redirectAttributes.addFlashAttribute(PageCodeEnum.KEY, PageCodeEnum.LOGIN_FAILURE);
return false;
}
redirectAttributes.addFlashAttribute(PageCodeEnum.KEY, PageCodeEnum.LOGIN_SUCCESS);
//将用户设置到Session中(留服务器内部判断是否登录使用!)
request.getSession().setAttribute(SessionKeyConst.SESSION_USER, sysUser);
//将Session设置到Cookie中(留服务器判断浏览器是否登录使用!)
if (sysUserDto.isRemember()){
SysUser user = sysUserList.get(0);
String loginToken = generateLoginToken(user.getUsername(), user.getPassword(), user.getId());
Cookie cookie = new Cookie(SessionKeyConst.SESSION_LOGIN_TOKEN, loginToken);
//若我们这里不设置path,则只要访问“/login”时才会带该cooke
cookie.setPath("/");
cookie.setMaxAge(SessionKeyConst.EXPIRE_TIME);
response.addCookie(cookie);
}
return true;
}
/**
* 根据用户名、密码和用户id生成登录令牌
* @param username 用户名
* @param password 密码
* @param userId 用户id
* @return 登录令牌
*/
public static String generateLoginToken(String username, String password, Integer userId) {
return Md5Util.getMD5WithSalt(username+password, SessionKeyConst.ENCRYPT_SALT) + ":" + userId;
}
}
这里需要注意的是:
(1)登陆成功以后需要将用户信息存到Session中,供需要登录以后才能访问的页面之间跳转使用
(2)登陆成功以后通过“remember”标识来判断是否将“md5(用户名,md5(密码)):用户ID”
写到Cookie中。关于Token的组成方式,我简单说一下原理。通过“:”
切割Token即可得到用户id,我们可以去数据库中查询到相应的用户,然后将数据库中的用户名,密码进行md5盐值加密,与传过来的Token进行比较,相等即通过验证。
(3)在设置Cookie的时候,我们根据需要设置Cookie的有效期,它决定我们可以多长时间免登陆
(4)在设置Cookie的时候,一定要视情况设置Cookie的path,因为它决定访问哪些路径的时候,客户端才会在访问的时候,将Cookie传送过来
2、第二步,免登陆验证与免登陆(使用Spring拦截器)
package com.imooc.interceptor;
import com.imooc.bean.SysUser;
import com.imooc.constant.SessionKeyConst;
import com.imooc.dao.SysUserDao;
import com.imooc.util.Md5Util;
import com.imooc.util.SpringContextHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* 拦截Session,判断是否登录(HandlerInterceptor是Spring提供的拦截器,不是原生JDK的拦截器)
* @author **
* @date 2018/6/13 11:37
*/
public class SessionInterceptor implements HandlerInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(SessionInterceptor.class);
/**
* 在进入Handler方法(就是Controller中映射路径对应的方法)执行之前执行本方法
* @return true:执行下一个拦截器,直到所有的拦截器都执行完毕,再执行拦截的Controller中的Handler方法
* false:从当前的拦截器往回执行所有拦截器的afterCompletion()方法,再退出拦截器链,不再执行Handler方法
*/
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
HttpSession session = httpServletRequest.getSession();
//若Session中有User,说明用户已经登录,可以继续执行后续代码
if (session.getAttribute(SessionKeyConst.SESSION_USER) != null){
return true;
}
//验证Cookie中是否有登陆标识
if (validateLoginWithCookie(httpServletRequest)){
return true;
}
//若Session中没有User,说明用户未登录或登录超时,跳转到登录界面
httpServletRequest.getRequestDispatcher("/login/sessionTimeout").forward(httpServletRequest, httpServletResponse);
return false;
}
private boolean validateLoginWithCookie(HttpServletRequest httpServletRequest) {
String loginToken = "";
Cookie[] cookies = httpServletRequest.getCookies();
if (cookies != null && cookies.length >0){
for (Cookie cookie : cookies){
if (SessionKeyConst.SESSION_LOGIN_TOKEN.equals(cookie.getName())){
loginToken = cookie.getValue();
}
}
}
if (!loginToken.trim().equals("")){
//切割出userId
String[] strs = loginToken.split(":");
String md5Str = strs[0];
String userId = strs[1];
//因为拦截器启动的时候,SysUserDao还未注入到Spring容器中,所以通过@AutoWired无法注入SysUserDao对象
SysUserDao sysUserDao = SpringContextHolder.getBean(SysUserDao.class);
SysUser sysUser = sysUserDao.selectUserWithId(Integer.parseInt(userId));
if (sysUser != null){
//因为这个时候,LoginServiceImpl不一定能注入到Spring容器中,所以它生成token的方法这里没法调用,老老实实自己写
String str = Md5Util.getMD5WithSalt(sysUser.getUsername()+sysUser.getPassword(), SessionKeyConst.ENCRYPT_SALT);
//经过相同加密手段得到的字符串相同,则说明通过验证
if (md5Str.equals(str)){
//构建Session,供服务器内做已登录跳转(这步必须做,否则内部跳转还是需要登录!!)
httpServletRequest.getSession().setAttribute(SessionKeyConst.SESSION_USER, sysUser);
return true;
}
}
}
return false;
}
/**
* 在进入Handler方法之后,返回ModelAndView之前执行(就是拿到了Handler方法的返回值之后)
*/
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
/**
* 在Handler方法执行完之后执行
*/
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
这里需要注意的是:
(1)必须先进行Session校验,再进行Cookie校验,两个顺序不能反,否则在需要登陆之后才能访问的页面之间跳转的时候,会反复进行Cookie校验,浪费资源。
(2)在利用userId去查询数据库的时候,需要调用Mybatis接口,但是在拦截器执行期间,Spring还未将“SysUserDao”注入到容器中去,所以这个时候无法通过“@Autowired”注入,需要通过“SpringContextHandler”来获取“SysUserDao”对象,关于“SpringContextHandler”定义,参见Spring 中获取Bean的不同方式
(3)"/login/sessionTimeout"
是我在Controller中专门用于集中处理校验不成功,需要重新登录的入口。
(4)当用户更改密码时,需要强制用户退出,重新登录。因为客户端中的Token依然是老的Token,需要将老的Token换成新的。