Token的作用及原理分析
講到Token的作用和原理,網(wǎng)上有很多相關(guān)的技術(shù)文章,通過(guò)搜集整理并加入自己的理解體會(huì),做一個(gè)總結(jié)整理,希望可以幫助到更多有需要的人。
1、token作用及原理
Token,即令牌,是服務(wù)器產(chǎn)生的,具有隨機(jī)性和不可預(yù)測(cè)性,它主要有兩個(gè)作用:
(1)防止表單重復(fù)提交;
使用Token防表單重復(fù)提交步驟:
- ①在服務(wù)器端生成一個(gè)唯一的隨機(jī)標(biāo)識(shí)號(hào),專(zhuān)業(yè)術(shù)語(yǔ)稱(chēng)為T(mén)oken(令牌),同時(shí)在當(dāng)前用戶(hù)的Session域中保存這個(gè)Token;
- ②將Token發(fā)送到客戶(hù)端的Form表單中,在Form表單中使用隱藏域來(lái)存儲(chǔ)這個(gè)Token,表單提交的時(shí)候連同這個(gè)Token一起提交到服務(wù)器端;
- ③在服務(wù)器端判斷客戶(hù)端提交上來(lái)的Token與服務(wù)器端生成的Token是否一致,如果不一致,那就是重復(fù)提交了,此時(shí)服務(wù)器端就可以不處理重復(fù)提交的表單。如果相同則處理表單提交,處理完后清除當(dāng)前用戶(hù)的Session域中存儲(chǔ)的標(biāo)識(shí)號(hào)。
FromServlet.java
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class FormServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String token = TokenUtil.getInstance().makeToken();//創(chuàng)建令牌
System.out.println("在FormServlet中生成的token:"+token);
request.getSession().setAttribute("token", token); //在服務(wù)器使用session保存token(令牌)
request.getRequestDispatcher("/Form.jsp").forward(request, response);//跳轉(zhuǎn)到form.jsp頁(yè)面
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doGet(request, response);
}
}
TokenUtil.java
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import sun.misc.BASE64Encoder;
public class TokenUtil {
/*
*單例設(shè)計(jì)模式(保證類(lèi)的對(duì)象在內(nèi)存中只有一個(gè))
*1、把類(lèi)的構(gòu)造函數(shù)私有
*2、自己創(chuàng)建一個(gè)類(lèi)的對(duì)象
*3、對(duì)外提供一個(gè)公共的方法,返回類(lèi)的對(duì)象
*/
private TokenUtil(){
}
private static final TokenUtil instance = new TokenUtil();
/**
* 返回類(lèi)的對(duì)象
* @return
*/
public static TokenUtil getInstance(){
return instance;
}
/**
* 生成Token
* Token:Nv6RRuGEVvmGjB+jimI/gw==
* @return
*/
public String makeToken(){
String token = (System.currentTimeMillis() + new Random().nextInt(999999999)) + "";
//數(shù)據(jù)指紋 128位長(zhǎng) 16個(gè)字節(jié) md5
try {
MessageDigest md = MessageDigest.getInstance("md5");
//對(duì)于給定數(shù)量的更新數(shù)據(jù),digest 方法只能被調(diào)用一次。digest 方法被調(diào)用后,MessageDigest對(duì)象被重新設(shè)置成其初始狀態(tài)。
byte md5[] = md.digest(token.getBytes());
//base64編碼--任意二進(jìn)制編碼明文字符 adfsdfsdfsf
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(md5);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
Form.jsp
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>" rel="external nofollow" rel="external nofollow" >
<title>Form.jsp</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">
<!--
<link rel="stylesheet" type="text/css" href="styles.css" rel="external nofollow" rel="external nofollow" >
-->
</head>
<body>
<form action="DoFormServlet" method="post">
<%--使用隱藏域存儲(chǔ)生成的token--%>
<%--
<input type="hidden" name="token" value="<%=session.getAttribute("token") %>">
--%>
<%--使用EL表達(dá)式取出存儲(chǔ)在session中的token--%>
<input type="hidden" name="token" value="${token}" /> 用戶(hù)名:<input
type="text" name="username"> <input type="submit" value="提交">
</form>
</body>
</html>
DoFormServlet.java
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class DoFormServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
boolean isRepeat = isRepeatSubmit(request);//判斷用戶(hù)是否是重復(fù)提交
if(isRepeat){
request.setAttribute("MSG", "請(qǐng)不要重復(fù)提交");
System.out.println("請(qǐng)不要重復(fù)提交");
request.getRequestDispatcher("/Msg.jsp").forward(request, response);
return;
}
request.getSession().removeAttribute("token");//移除session中的token
request.setAttribute("MSG","處理用戶(hù)提交請(qǐng)求!!");
request.getRequestDispatcher("/Msg.jsp").forward(request, response);
System.out.println("處理用戶(hù)提交請(qǐng)求?。?);
}
/**
* 判斷客戶(hù)端提交上來(lái)的令牌和服務(wù)器端生成的令牌是否一致
* @param request
* @return
* true 用戶(hù)重復(fù)提交了表單
* false 用戶(hù)沒(méi)有重復(fù)提交表單
*/
private boolean isRepeatSubmit(HttpServletRequest request) {
String client_token = request.getParameter("token");
//1、如果用戶(hù)提交的表單數(shù)據(jù)中沒(méi)有token,則用戶(hù)是重復(fù)提交了表單
if(client_token==null){
return true;
}
//取出存儲(chǔ)在Session中的token
String server_token = (String) request.getSession().getAttribute("token");
//2、如果當(dāng)前用戶(hù)的Session中不存在Token(令牌),則用戶(hù)是重復(fù)提交了表單
if(server_token==null){
return true;
}
//3、存儲(chǔ)在Session中的Token(令牌)與表單提交的Token(令牌)不同,則用戶(hù)是重復(fù)提交了表單
if(!client_token.equals(server_token)){
return true;
}
return false;
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doGet(request, response);
}
}
Msg.jsp
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>" rel="external nofollow" rel="external nofollow" >
<title>result show</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">
<!--
<link rel="stylesheet" type="text/css" href="styles.css" rel="external nofollow" rel="external nofollow" >
-->
</head>
<body>${MSG}</body>
</html>
(2)用來(lái)作身份驗(yàn)證
使用基于 Token 的身份驗(yàn)證流程如下:
- ①用戶(hù)首次登錄,將輸入的賬號(hào)和密碼提交給服務(wù)器;
- ②服務(wù)器對(duì)輸入內(nèi)容進(jìn)行校驗(yàn),若賬號(hào)和密碼匹配則驗(yàn)證通過(guò),登錄成功,并生成一個(gè)token值,將其保存到數(shù)據(jù)庫(kù),并返回給客戶(hù)端;
- ③客戶(hù)端拿到返回的token值將其保存在本地(如cookie/localStorage),作為公共參數(shù),以后每次請(qǐng)求服務(wù)器時(shí)都攜帶該token(放在響應(yīng)頭里),提交給服務(wù)器進(jìn)行校驗(yàn);
- ④服務(wù)器接收到請(qǐng)求后,首先驗(yàn)證是否攜帶token,若攜帶則取出請(qǐng)求頭里的token值與數(shù)據(jù)庫(kù)存儲(chǔ)的token進(jìn)行匹配校驗(yàn),若token值相同則登錄成功,且當(dāng)前正處于登錄狀態(tài),此時(shí)正常返回?cái)?shù)據(jù),讓app顯示數(shù)據(jù);若不存在或兩個(gè)值不一致,則說(shuō)明原來(lái)的登錄已經(jīng)失效,此時(shí)返回錯(cuò)誤狀態(tài)碼,提示用戶(hù)跳轉(zhuǎn)至登錄界面重新登錄;
- ⑤注意:用戶(hù)每進(jìn)行一次登錄,登錄成功后服務(wù)器都會(huì)更新一個(gè)token新值返回給客戶(hù)端;
實(shí)現(xiàn)原理圖:

引入java-jwt依賴(lài)
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>簽名工具JwtUtil.java
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
/**
* 過(guò)期時(shí)間一天,
* TODO 正式運(yùn)行時(shí)修改為15分鐘
*/
private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000;
/**
* token私鑰
*/
private static final String TOKEN_SECRET = "f26e587c28064d0e855e72c0a6a0e618";
/**
* 校驗(yàn)token是否正確
*
* @param token 密鑰
* @return 是否正確
*/
public static boolean verify(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 生成簽名,15min后過(guò)期
*
* @param username 用戶(hù)名
* @return 加密的token
*/
public static String sign(String username,String userId) {
try {
// 過(guò)期時(shí)間
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
// 私鑰及加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
// 設(shè)置頭部信息
Map<String, Object> header = new HashMap<>(2);
header.put("typ", "JWT");
header.put("alg", "HS256");
// 附帶username,userId信息,生成簽名
return JWT.create()
.withHeader(header)
.withClaim("loginName", username)
.withClaim("userId",userId)
.withExpiresAt(date)
.sign(algorithm);
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
身份認(rèn)證LoginController.java
import com.joe.entity.ApiResponse;
import com.joe.entity.User;
import com.joe.enums.ApiResponseEnum;
import com.joe.service.IUserService;
import com.joe.util.ApiResponseUtil;
import com.joe.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Map;
@Controller
@RequestMapping("/")
public class LoginController {
@Autowired
private IUserService userService;
/**
* 登陸接口
*
* @return token
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public ApiResponse login(@RequestBody Map<String, String> map) {
String loginName = map.get("loginName");
String password = map.get("password");
//身份驗(yàn)證是否成功
boolean isSuccess = userService.checkUser(loginName, password);
if (isSuccess) {
User user = userService.getUserByLoginName(loginName);
if (user != null) {
//返回token
String token = JwtUtil.sign(user.getName(), user.getId());
if (token != null) {
return ApiResponseUtil.getApiResponse(token);
}
}
}
//返回登陸失敗消息
return ApiResponseUtil.getApiResponse(ApiResponseEnum.LOGIN_FAIL);
}
}
配置攔截器TokenInterceptor.java
import com.alibaba.fastjson.JSONObject;
import com.joe.entity.ApiResponse;
import com.joe.enums.ApiResponseEnum;
import com.joe.util.ApiResponseUtil;
import com.joe.util.JwtUtil;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setCharacterEncoding("utf-8");
String token = request.getHeader("access_token");
//token不存在
if (null != token) {
//驗(yàn)證token是否正確
boolean result = JwtUtil.verify(token);
if (result) {
return true;
}
}
ApiResponse apiResponse = ApiResponseUtil.getApiResponse(ApiResponseEnum.AUTH_ERROR);
responseMessage(response,response.getWriter(),apiResponse);
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
/**
* 返回信息給客戶(hù)端
*
* @param response
* @param out
* @param apiResponse
*/
private void responseMessage(HttpServletResponse response, PrintWriter out, ApiResponse apiResponse) {
response.setContentType("application/json; charset=utf-8");
out.print(JSONObject.toJSONString(apiResponse));
out.flush();
out.close();
}
}
spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--此文件負(fù)責(zé)整個(gè)mvc中的配置-->
<!--啟用spring的一些annotation -->
<context:annotation-config/>
<!-- 配置注解驅(qū)動(dòng) 可以將request參數(shù)與綁定到controller參數(shù)上 -->
<mvc:annotation-driven/>
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/login/"/>
<bean class="com.joe.interceptor.TokenInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
<!-- 自動(dòng)掃描裝配 -->
<context:component-scan base-package="com.joe"/>
</beans>2、token優(yōu)點(diǎn)
①支持跨域訪(fǎng)問(wèn),將token置于請(qǐng)求頭中,而cookie是不支持跨域訪(fǎng)問(wèn)的;
②無(wú)狀態(tài)化,服務(wù)端無(wú)需存儲(chǔ)token,只需要驗(yàn)證token信息是否正確即可,而session需要在服務(wù)端存儲(chǔ),一般是通過(guò)cookie中的sessionID在服務(wù)端查找對(duì)應(yīng)的session;
③無(wú)需綁定到一個(gè)特殊的身份驗(yàn)證方案(傳統(tǒng)的用戶(hù)名密碼登陸),只需要生成的token是符合我們預(yù)期設(shè)定的即可;
④適用于移動(dòng)端(Android,iOS,小程序等等),若原生平臺(tái)不支持cookie(比如說(shuō)微信小程序),每一次請(qǐng)求都是一次會(huì)話(huà),當(dāng)然我們可以手動(dòng)為他添加cookie;
⑤可避免CSRF跨站偽造攻擊;
⑥適用于RESTful API,這樣可以與各種后端(java,.net,python......)相結(jié)合。
3、token存儲(chǔ)
(1)本地通過(guò)DB數(shù)據(jù)庫(kù)或txt文本保存
①通過(guò)數(shù)據(jù)庫(kù)保存
- a.獲取access_token時(shí),把當(dāng)前系統(tǒng)時(shí)間和access_token保存到數(shù)據(jù)表(t_access_token)中;
- b.再次獲取時(shí),把上次獲取時(shí)間(getTime)與當(dāng)前系統(tǒng)時(shí)間作比較,看時(shí)間是否超過(guò)2小時(shí);
- c.超過(guò)2h時(shí)間限制,再獲取一個(gè)access_token,然后更新數(shù)據(jù)表的accessToken和getTime;
②通過(guò)txt文本保存
- a.創(chuàng)建 access_token.text文件;
- b.讀取get_time;
- c.讀取txt判斷時(shí)間是否超過(guò)2個(gè)小時(shí);
- d.超過(guò)則重寫(xiě)access_token.text文件;
(2)Client通過(guò)Cookie或Form表單的隱藏域存儲(chǔ),Server可存儲(chǔ)在Session(單機(jī)系統(tǒng))或者其他緩存系統(tǒng)(分布式系統(tǒng))中。
(3)本地通過(guò)cookie或 localStorage存儲(chǔ),作為公共參數(shù),以后每次請(qǐng)求服務(wù)器時(shí)都攜帶該token(放在響應(yīng)頭里),提交給服務(wù)器進(jìn)行校驗(yàn)。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- 深度解析Java視角下Cookie、Session、Token實(shí)戰(zhàn)教程
- java實(shí)現(xiàn)認(rèn)證與授權(quán)的jwt與token+redis,哪種方案更好用?
- JAVA實(shí)現(xiàn)Token自動(dòng)續(xù)期機(jī)制的示例代碼
- java-jwt通過(guò)公鑰字符串驗(yàn)證解析token方式
- Java字符串分解技術(shù)substring、tokenizing和trimming方法的使用
- Java中的Scanner、BufferedReader?和?StreamTokenizer使用詳解
- Java后端接口中提取請(qǐng)求頭中的Cookie和Token的方法
相關(guān)文章
基于Comparator對(duì)象集合實(shí)現(xiàn)多個(gè)條件按照優(yōu)先級(jí)的比較
這篇文章主要介紹了基于Comparator對(duì)象集合實(shí)現(xiàn)多個(gè)條件按照優(yōu)先級(jí)的比較,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
springboot應(yīng)用服務(wù)啟動(dòng)事件的監(jiān)聽(tīng)實(shí)現(xiàn)
本文主要介紹了springboot應(yīng)用服務(wù)啟動(dòng)事件的監(jiān)聽(tīng)實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04
Spring的@Scheduled 如何動(dòng)態(tài)更新cron表達(dá)式
這篇文章主要介紹了Spring的@Scheduled 如何動(dòng)態(tài)更新cron表達(dá)式的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
SpringBoot實(shí)現(xiàn)動(dòng)態(tài)數(shù)據(jù)源切換的項(xiàng)目實(shí)踐
在實(shí)際開(kāi)發(fā)過(guò)程中,我們經(jīng)常遇到需要同時(shí)操作多個(gè)數(shù)據(jù)源的情況,本文主要介紹了SpringBoot實(shí)現(xiàn)動(dòng)態(tài)數(shù)據(jù)源切換的項(xiàng)目實(shí)踐,具有一定的參考價(jià)值,感興趣的可以了解一下2024-04-04
Java動(dòng)態(tài)字節(jié)碼注入技術(shù)的實(shí)現(xiàn)
Java動(dòng)態(tài)字節(jié)碼注入技術(shù)是一種在運(yùn)行時(shí)修改Java字節(jié)碼的技術(shù),本文主要介紹了Java動(dòng)態(tài)字節(jié)碼注入技術(shù)的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08
Mybatis -如何處理clob類(lèi)型數(shù)據(jù)
這篇文章主要介紹了Mybatis 如何處理clob類(lèi)型數(shù)據(jù)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
java正則表達(dá)式優(yōu)化超詳細(xì)舉例講解
正則表達(dá)式是一種強(qiáng)大的文本處理工具,在數(shù)據(jù)驗(yàn)證、字符串搜索和替換等方面有廣泛應(yīng)用,這篇文章主要介紹了java正則表達(dá)式優(yōu)化的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-07-07

