SpringBoot(7)在SpringBoot实现基于Token的用户身份验证

基于Token的身份验证用来替代传统的cookie+session身份验证方法中的session。

    请求中发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)。即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。 
    token是有时效的,一段时间之后用户需要重新验证。我们也不一定需要等到token自动失效,token有撤回的操作,通过token revocataion可以使一个特定的token或是一组有相同认证的token无效。

基于Token的身份验证流程如下。

  • 客户端使用用户名跟密码请求登录
  • 服务端收到请求,去验证用户名与密码
  • 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
  • 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
  • 客户端每次向服务端请求资源的时候需要带着服务端签发的 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的身份验证



你可能感兴趣的:(SpringBoot)