基于Token的身份验证用来替代传统的cookie+session身份验证方法中的session。
基于Token的身份验证流程如下。那在SpringBoot中怎么去实现呢?
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
首先前三步是一起的,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<String,String > register(String userName,String password){ Map<String,String> map = new HashMap<String, String >(); 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<String,Object> login(String username, String password){ Map<String,Object> map = new HashMap<String,Object>(); 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<String, String> 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<String,Object> 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的身份验证