1. 程式人生 > >SpringBoot(7)在SpringBoot實現基於Token的使用者身份驗證

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<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的身份驗證