SpringBoot(7)在SpringBoot實現基於Token的使用者身份驗證
阿新 • • 發佈:2019-01-30
基於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的身份驗證