基于Token的身份验证用来替代传统的cookie+session身份验证方法中的session。
请求中发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)。即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。
token是有时效的,一段时间之后用户需要重新验证。我们也不一定需要等到token自动失效,token有撤回的操作,通过token revocataion可以使一个特定的token或是一组有相同认证的token无效。
基于Token的身份验证流程如下。
那在SpringBoot中怎么去实现呢?
首先前三步是一起的,DAO层就不写了,就是设计一个相关的表用于存储,那在service层和Controller层对应的实现如下,主体就是标记红色的那一块:
package com.springboot.springboot.service;
import com.springboot.springboot.dao.loginTicketsDAO;
import com.springboot.springboot.dao.userDAO;
import com.springboot.springboot.model.User;
import com.springboot.springboot.model.loginTickets;
import com.springboot.springboot.utils.WendaUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.*;
@Service
public class userService {
Random random = new Random();
@Autowired
userDAO uDAO;
@Autowired
loginTicketsDAO lTicketsDAO;
//注册
public Map register(String userName,String password){
Map map = new HashMap();
if(StringUtils.isEmpty(userName)){
map.put("msg", "用户名不能为空");
return map;
}
if(StringUtils.isEmpty(password)){
map.put("msg","密码不能为空");
return map;
}
User user = uDAO.selectByName(userName);
if(user != null){
map.put("msg","用户名已被注册");
return map;
}
user = new User();
user.setName(userName);
user.setSalt(UUID.randomUUID().toString().substring(0,5));
user.setHead_url(String.format("http://images.nowcoder.com/head/%dt.png", random.nextInt(1000)));
user.setPassword(WendaUtil.MD5(password + user.getSalt()));
uDAO.addUser(user);
//注册完成下发ticket之后自动登录
String ticket = addLoginTicket(user.getId());
map.put("ticket",ticket);
return map;
}
//登陆
public Map login(String username, String password){
Map map = new HashMap();
if(StringUtils.isEmpty(username)){
map.put("msg","用户名不能为空");
return map;
}
if(StringUtils.isEmpty(password)){
map.put("msg","密码不能为空");
return map;
}
User user = uDAO.selectByName(username);
if (user == null){
map.put("msg","用户名不存在");
return map;
}
if (!WendaUtil.MD5(password+user.getSalt()).equals(user.getPassword())) {
map.put("msg", "密码错误");
return map;
}
String ticket = addLoginTicket(user.getId());
map.put("ticket",ticket);
return map;
}
public String addLoginTicket(int user_id){
loginTickets ticket = new loginTickets();
ticket.setUserId(user_id);
Date nowDate = new Date();
nowDate.setTime(3600*24*100 + nowDate.getTime());
ticket.setExpired(nowDate);
ticket.setStatus(0);
ticket.setTicket(UUID.randomUUID().toString().replaceAll("_",""));
lTicketsDAO.addTicket(ticket);
return ticket.getTicket();
}
public User getUser(int id){
return uDAO.selectById(id);
}
}
package com.springboot.springboot.controller;
import com.springboot.springboot.service.userService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
//首页的登录功能
@Controller
public class registerController {
private static final Logger logger = LoggerFactory.getLogger(registerController.class);
@Autowired
userService uService;
//注册
@RequestMapping(path = {"/reg/"}, method = {RequestMethod.POST})
public String reg(Model model, @RequestParam("username") String username, @RequestParam("password") String password, @RequestParam(value = "rememberme",defaultValue = "false") boolean rememberme, HttpServletResponse response) { try { Map map = uService.register(username, password); if (map.containsKey("ticket")) { Cookie cookie = new Cookie("ticket",map.get("ticket")); cookie.setPath("/"); response.addCookie(cookie); return "redirect:/"; }else{ model.addAttribute("msg", map.get("msg")); return "login"; } } catch (Exception e) { logger.error("注册异常" + e.getMessage()); return "login"; }
}
@RequestMapping(path = {"/reglogin"}, method = {RequestMethod.GET})
public String register(Model model) {
return "login";
}
//登陆
@RequestMapping(path={"/login/"},method = {RequestMethod.POST})
public String login(Model model,@RequestParam("username") String username, @RequestParam("password") String password,
@RequestParam(value = "rememberme",defaultValue = "false") boolean rememberme,
HttpServletResponse response){
try{ Map map = uService.login(username,password); if(map.containsKey("ticket")){ Cookie cookie = new Cookie("ticket",map.get("ticket").toString()); cookie.setPath("/"); //可在同一应用服务器内共享cookie response.addCookie(cookie); return "redirect:/"; } else{ model.addAttribute("msg",map.get("msg")); return "login"; } }catch (Exception e){ logger.error("登陆异常" + e.getMessage()); return "login"; }
}
}
从上面能够清楚的看出来,用户先去请求注册或者是登陆,然后服务器去验证他的用户名和密码
验证成功后会下发一个Token,我这里是ticket,客户端收到ticket之后呢会把ticket存在Cookie中,如下图,我登录成功之后会有一个与当前用户对应的ticket
每次访问服务器资源的时候需要带着这个ticket,然后怎么判断是否有呢?就要用拦截器来实现过滤,用拦截器去判断这个ticket当前的状态是什么样的?有没有过期?身份状态是不是有效的?然后根据这个来判断应该赋予什么样的权限?当验证成功之后就把ticket对应的用户的通过下面一段发送给freemaker的上下文,实现页面的正常的渲染
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//就是为了能够在渲染之前所有的freemaker模板能够访问这个对象user,就是在所有的controller渲染之前将这个user加进去
if(modelAndView != null){
//这个其实就和model.addAttribute一样的功能,就是把这个变量与前端视图进行交互 //就是与header.html页面的user对应
modelAndView.addObject("user",hostHolder.getUser());
}
}
完整的如下:
package com.springboot.springboot.interceptor;
import com.springboot.springboot.dao.loginTicketsDAO;
import com.springboot.springboot.dao.userDAO;
import com.springboot.springboot.model.HostHolder;
import com.springboot.springboot.model.User;
import com.springboot.springboot.model.loginTickets;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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 java.util.Date;
/**
* 拦截器
* @ 用来判断用户的
*1. 当preHandle方法返回false时,从当前拦截器往回执行所有拦截器的afterCompletion方法,再退出拦截器链。也就是说,请求不继续往下传了,直接沿着来的链往回跑。
2.当preHandle方法全为true时,执行下一个拦截器,直到所有拦截器执行完。再运行被拦截的Controller。然后进入拦截器链,运
行所有拦截器的postHandle方法,完后从最后一个拦截器往回执行所有拦截器的afterCompletion方法.
*/
//@component (把普通pojo实例化到spring容器中,相当于配置文件中的
@Component
public class PassportInterceptor implements HandlerInterceptor{
@Autowired
loginTicketsDAO lTicketsDAO;
@Autowired
userDAO uDAO;
@Autowired
HostHolder hostHolder;
//判断然后进行用户拦截
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String tickets = null;
if(request.getCookies() != null){
for(Cookie cookie : request.getCookies()){
if(cookie.getName().equals("ticket")){
tickets = cookie.getValue();
break;
}
}
}
if(tickets != null ){
loginTickets loginTickets = lTicketsDAO.selectByTicket(tickets);
if(loginTickets == null || loginTickets.getExpired().before(new Date()) || loginTickets.getStatus() != 0){
return true;
}
User user = uDAO.selectById(loginTickets.getUserId());
hostHolder.setUser(user);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//就是为了能够在渲染之前所有的freemaker模板能够访问这个对象user,就是在所有的controller渲染之前将这个user加进去
if(modelAndView != null){
//这个其实就和model.addAttribute一样的功能,就是把这个变量与前端视图进行交互 //就是与header.html页面的user对应
modelAndView.addObject("user",hostHolder.getUser());
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear(); //当执行完成之后呢需要将变量清空
}
}
当用户登出的时候就把ticket的身份状态置位为无效状态即可
public void logout(String ticket){
lTicketsDAO.updateStatus(ticket,1);
}
这样就完成了在SpringBoot实现基于Token的身份验证