webapp用戶身份認證方案 JSON WEB TOKEN 實現Deme示例,Java版
本項目依賴於下面jar包:
- nimbus-jose-jwt-4.13.1.jar (一款開源的成熟的JSON WEB TOKEN 解決方法,本倉庫的代碼是對其的進一步封裝)
- json-smart-2.0-RC2.jar和asm-1.0-RC1.jar (依賴jar包,主要用於JSONObject序列化)
- cors-filter-2.2.1.jar和java-property-utils-1.9.1.jar(用於處理跨域ajax請求)
- junit.jar(單元測試相關jar包)
核心類Jwt.java結構:
2個靜態方法createToken和validToken,分別用於生成TOKEN和校驗TOKEN; 定義了枚舉TokenState,用於表示驗證token時的結果,用戶可根據結果進行不同處理:
- EXPIRED token過期
- INVALID token無效(包括token不合法,token格式不對,校驗時異常)
- VALID token有效
使用示例
獲取token
Map<String , Object> payload=new HashMap<String, Object>(); Date date=new Date(); payload.put("uid", "291969452");//用戶id payload.put("iat", date.getTime());//生成時間 payload.put("ext",date.getTime()+1000*60*60);//過期時間1小時 String token=Jwt.createToken(payload); System.out.println("token:"+token);
校驗token
String token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIyOTE5Njk0NTIiLCJpYXQiOjE0NjA0MzE4ODk2OTgsImV4dCI6MTQ2MDQzNTQ4OTY5OH0.RAa71BnklRMPyPhYBbxsfJdtXBnXeWevxcXLlwC2PrY";
Map<String, Object> result=Jwt.validToken(token);
String state=(String)result.get("state");
switch (TokenState.getTokenState(state)) {
case VALID:
//To do somethings
System.out.println("有效token");
break;
case EXPIRED:
System.out.println("過期token");
break;
case INVALID:
System.out.println("無效的token");
break;
}
System.out.println("返回結果數據是:" +result.toString());
項目應用中代碼:
JAT 工具類
public class Jwt { /** * 秘鑰 */ private static final byte[] SECRET="3d990d2276917dfac04467df11fff26d".getBytes(); /** * 初始化head部分的數據為 * { * "alg":"HS256", * "type":"JWT" * } */ private static final JWSHeader header=new JWSHeader(JWSAlgorithm.HS256, JOSEObjectType.JWT, null, null, null, null, null, null, null, null, null, null, null); /** * 生成token,該方法只在用戶登錄成功后調用 * * @param Map集合,可以存儲用戶id,token生成時間,token過期時間等自定義字段 * @return token字符串,若失敗則返回null */ public static String createToken(Map<String, Object> payload) { String tokenString=null; // 創建一個 JWS object JWSObject jwsObject = new JWSObject(header, new Payload(new JSONObject(payload))); try { // 將jwsObject 進行HMAC簽名 jwsObject.sign(new MACSigner(SECRET)); tokenString=jwsObject.serialize(); } catch (JOSEException e) { System.err.println("簽名失敗:" + e.getMessage()); e.printStackTrace(); } return tokenString; } /** * 校驗token是否合法,返回Map集合,集合中主要包含 state狀態碼 data鑒權成功后從token中提取的數據 * 該方法在過濾器中調用,每次請求API時都校驗 * @param token * @return Map<String, Object> */ public static Map<String, Object> validToken(String token) { Map<String, Object> resultMap = new HashMap<String, Object>(); try { JWSObject jwsObject = JWSObject.parse(token); Payload payload = jwsObject.getPayload(); JWSVerifier verifier = new MACVerifier(SECRET); if (jwsObject.verify(verifier)) { JSONObject jsonOBj = payload.toJSONObject(); // token校驗成功(此時沒有校驗是否過期) resultMap.put("state", TokenState.VALID.toString()); // 若payload包含ext字段,則校驗是否過期 if (jsonOBj.containsKey("ext")) { long extTime = Long.valueOf(jsonOBj.get("ext").toString()); long curTime = new Date().getTime(); // 過期了 if (curTime > extTime) { resultMap.clear(); resultMap.put("state", TokenState.EXPIRED.toString()); } } resultMap.put("data", jsonOBj); } else { // 校驗失敗 resultMap.put("state", TokenState.INVALID.toString()); } } catch (Exception e) { //e.printStackTrace(); // token格式不合法導致的異常 resultMap.clear(); resultMap.put("state", TokenState.INVALID.toString()); } return resultMap; } }
TokenState
package com.jwt; /** * 枚舉,定義token的三種狀態 * @author running@vip.163.com * */ public enum TokenState { /** * 過期 */ EXPIRED("EXPIRED"), /** * 無效(token不合法) */ INVALID("INVALID"), /** * 有效的 */ VALID("VALID"); private String state; private TokenState(String state) { this.state = state; } /** * 根據狀態字符串獲取token狀態枚舉對象 * @param tokenState * @return */ public static TokenState getTokenState(String tokenState){ TokenState[] states=TokenState.values(); TokenState ts=null; for (TokenState state : states) { if(state.toString().equals(tokenState)){ ts=state; break; } } return ts; } public String toString() { return this.state; } public String getState() { return state; } public void setState(String state) { this.state = state; } }
junit 測試結果
package com.jwt; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.junit.Test; /** * 單元測試(請自行引入junit4 Jar包) */ public class JwtTestCase { @Test @SuppressWarnings("unchecked") public void test1() { // 正常生成token---------------------------------------------------------------------------------------------------- String token = null; Map<String, Object> payload = new HashMap<String, Object>(); Date date = new Date(); payload.put("uid", "291969452");// 用戶id payload.put("iat", date.getTime());// 生成時間:當前 payload.put("ext", date.getTime() + 2000 * 60 * 60);// 過期時間2小時 token = Jwt.createToken(payload); System.out.println("新生成的token是:" + token+"\n馬上將該token進行校驗"); Map<String, Object> resultMap = Jwt.validToken(token); System.out.println("校驗結果是:" + getResult((String)resultMap.get("state")) ); HashMap<String,String> dataobj = (HashMap<String,String>) resultMap.get("data"); System.out.println("從token中取出的payload數據是:" +dataobj.toString()); } public void test2() { // 校驗過期---------------------------------------------------------------------------------------------------- String token = null; Map<String, Object> payload = new HashMap<String, Object>(); Date date = new Date(); payload.put("uid", "291969452");// 用戶id payload.put("iat", date.getTime());// 生成時間 payload.put("ext", date.getTime());// 過期時間就是當前 token = Jwt.createToken(payload); System.out.println("新生成的token是:" + token+"\n馬上將該token進行校驗"); Map<String, Object> resultMap = Jwt.validToken(token); System.out.println("校驗結果是:" + getResult((String)resultMap.get("state")) ); } @SuppressWarnings("unchecked") public void test2_1() { // 不校驗過期(當payload中無過期ext字段時)---------------------------------------------------------------------------------------------------- String token = null; Map<String, Object> payload = new HashMap<String, Object>(); Date date = new Date(); payload.put("uid", "291969452");// 用戶id payload.put("iat", date.getTime());// 生成時間 token = Jwt.createToken(payload); System.out.println("新生成的token是:" + token+"\n馬上將該token進行校驗"); Map<String, Object> resultMap = Jwt.validToken(token); System.out.println("校驗結果是:" + getResult((String)resultMap.get("state")) ); HashMap<String,String> dataobj = (HashMap<String,String>) resultMap.get("data"); System.out.println("從token中取出的payload數據是:" +dataobj.toString()); } public void test3() { // 校驗非法token的情況---------------------------------------------------------------------------------------------------- String token = null; Map<String, Object> payload = new HashMap<String, Object>(); Date date = new Date(); payload.put("uid", "291969452");// 用戶id payload.put("iat", date.getTime());// 生成時間 payload.put("ext", date.getTime());// 過期時間就是當前 token = Jwt.createToken(payload); System.out.println("新生成的token是:" + token); System.out.println("將新生成的token加點調料再來進行校驗"); token = token + "YouAreSB"; Map<String, Object> resultMap = Jwt.validToken(token); System.out.println("校驗結果是:" + getResult((String)resultMap.get("state")) ); System.out.println("原因是(非法token,payload參數可能經過中間人篡改,或者別人偽造的token)" ); } public void test4() { // 校驗異常的情況---------------------------------------------------------------------------------------------------- String token = "123"; System.out.println("我胡亂傳一個token:" + token); Map<String, Object> resultMap = Jwt.validToken(token); System.out.println("校驗結果是:" + getResult((String)resultMap.get("state")) ); System.out.println("原因是(token格式不合法導致的程序異常)"); } public String getResult(String state) { switch (TokenState.getTokenState(state)) { case VALID: //To do somethings state = "有效token"; break; case EXPIRED: state = "過期token"; break; case INVALID: state = "無效的token"; break; } return state; } }
loginServlet 使用,具體使用springmvc還是struts 可以參考servlet寫法
package com.servlet; import java.io.IOException; import java.io.PrintWriter; import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.minidev.json.JSONObject; import com.jwt.Jwt; @WebServlet(urlPatterns="/servlet/login",loadOnStartup=1) public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 5285600116871825644L; /** * 校驗用戶名密碼 */ public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String userName=request.getParameter("userName"); String password =request.getParameter("password"); JSONObject resultJSON=new JSONObject(); //用戶名密碼校驗成功后,生成token返回客戶端 if("admin".equals(userName)&&"123".equals(password)){ //生成token Map<String , Object> payload=new HashMap<String, Object>(); Date date=new Date(); payload.put("uid", "admin");//用戶ID payload.put("iat", date.getTime());//生成時間 payload.put("ext",date.getTime()+1000*60*60);//過期時間1小時 String token=Jwt.createToken(payload); resultJSON.put("success", true); resultJSON.put("msg", "登陸成功"); resultJSON.put("token", token); }else{ resultJSON.put("success", false); resultJSON.put("msg", "用戶名密碼不對"); } //輸出結果 output(resultJSON.toJSONString(), response); } public void output(String jsonStr,HttpServletResponse response) throws IOException{ response.setContentType("text/html;charset=UTF-8;"); PrintWriter out = response.getWriter(); out.println(jsonStr); out.flush(); out.close(); } }
每次請求都需要驗證token 是否有效
package com.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.minidev.json.JSONObject;
import com.jwt.Jwt;
@WebServlet(urlPatterns="/author/token",loadOnStartup=1,description="生成token的方法")
public class AuthorServlet extends HttpServlet {
private static final long serialVersionUID = -8463692428988705309L;
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String token=request.getHeader("token");
System.out.println(token);
Map<String, Object> result=Jwt.validToken(token);
//轉JSON並輸出
PrintWriter out = response.getWriter();
out.println(new JSONObject(result).toJSONString());
out.flush();
out.close();
}
public void doPut(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Map<String , Object> payload=new HashMap<String, Object>();
Date date=new Date();
payload.put("uid", "291969452");//用戶id
payload.put("iat", date.getTime());//生成時間
payload.put("ext",date.getTime()+1000*60*60);//過期時間1小時
String token=null;
token=Jwt.createToken(payload);
response.setContentType("text/html;charset=UTF-8;");
Cookie cookie=new Cookie("token", token);
cookie.setMaxAge(3600);
response.addCookie(cookie);
PrintWriter out = response.getWriter();
out.println(token);
out.flush();
out.close();
}
}
調用獲取信息的接口
mainServlet
package com.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.minidev.json.JSONObject;
@WebServlet(urlPatterns="/servlet/getInfo",loadOnStartup=1)
public class mainServlet extends HttpServlet {
private static final long serialVersionUID = -1643121334640537359L;
@SuppressWarnings("unchecked")
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("正在調用獲取信息的接口");
//將過濾器中存入的payload數據取出來
HashMap<String, String> data=(HashMap<String, String>) request.getAttribute("data");
//payload中的數據可以用來做查詢,比如我們在登陸成功時將用戶ID存到了payload中,我們可以將它取出來,去數據庫查詢這個用戶的所有信息;
//而不是用request.getParameter("uid")方法來獲取前端傳給我們的uid,因為前端的參數時可篡改的不完全可信的,而我們從payload中取出來的數據是從token中
//解密取出來的,在秘鑰沒有被破解的情況下,它是絕對可信的;這樣可以避免別人用這個接口查詢非自己用戶ID的相關信息
JSONObject resp=new JSONObject();
resp.put("success", true);
resp.put("msg", "成功");
resp.put("data", data);
output(resp.toJSONString(), response);
}
public void output(String jsonStr,HttpServletResponse response) throws IOException{
response.setContentType("text/html;charset=UTF-8;");
PrintWriter out = response.getWriter();
out.println(jsonStr);
out.flush();
out.close();
}
}
跨域過濾器
package com.filter; import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import javax.servlet.annotation.WebInitParam; import com.thetransactioncompany.cors.CORSConfiguration; import com.thetransactioncompany.cors.CORSFilter; /** * 服務端跨域處理過濾器,該過濾器需要依賴cors-filter-2.2.1.jar和java-property-utils-1.9.1.jar * @author running@vip.163.com * */ @WebFilter(urlPatterns={"/*"},asyncSupported=true, initParams={ @WebInitParam(name="cors.allowOrigin",value="*"), @WebInitParam(name="cors.supportedMethods",value="CONNECT, DELETE, GET, HEAD, OPTIONS, POST, PUT, TRACE"), @WebInitParam(name="cors.supportedHeaders",value="token,Accept, Origin, X-Requested-With, Content-Type, Last-Modified"),//注意,如果token字段放在請求頭傳到后端,這里需要配置 @WebInitParam(name="cors.exposedHeaders",value="Set-Cookie"), @WebInitParam(name="cors.supportsCredentials",value="true") }) public class Filter0_CrossOriginResource extends CORSFilter implements javax.servlet.Filter{ public void init(FilterConfig config) throws ServletException { System.out.println("跨域資源處理過濾器初始化了"); super.init(config); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("跨域過濾器"); super.doFilter(request, response, chain); } public void setConfiguration(CORSConfiguration config) { super.setConfiguration(config); } }
驗證登陸過濾器
package com.filter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.minidev.json.JSONObject;
import com.jwt.Jwt;
import com.jwt.TokenState;
/**
* toekn校驗過濾器,所有的API接口請求都要經過該過濾器(除了登陸接口)
* @author running@vip.163.com
*
*/
@WebFilter(urlPatterns="/servlet/*")
public class Filter1_CheckToken implements Filter {
@Override
public void doFilter(ServletRequest argo, ServletResponse arg1,
FilterChain chain ) throws IOException, ServletException {
HttpServletRequest request=(HttpServletRequest) argo;
HttpServletResponse response=(HttpServletResponse) arg1;
// response.setHeader("Access-Control-Allow-Origin", "*");
if(request.getRequestURI().endsWith("/servlet/login")){
//登陸接口不校驗token,直接放行
chain.doFilter(request, response);
return;
}
//其他API接口一律校驗token
System.out.println("開始校驗token");
//從請求頭中獲取token
String token=request.getHeader("token");
Map<String, Object> resultMap=Jwt.validToken(token);
TokenState state=TokenState.getTokenState((String)resultMap.get("state"));
switch (state) {
case VALID:
//取出payload中數據,放入到request作用域中
request.setAttribute("data", resultMap.get("data"));
//放行
chain.doFilter(request, response);
break;
case EXPIRED:
case INVALID:
System.out.println("無效token");
//token過期或者無效,則輸出錯誤信息返回給ajax
JSONObject outputMSg=new JSONObject();
outputMSg.put("success", false);
outputMSg.put("msg", "您的token不合法或者過期了,請重新登陸");
output(outputMSg.toJSONString(), response);
break;
}
}
public void output(String jsonStr,HttpServletResponse response) throws IOException{
response.setContentType("text/html;charset=UTF-8;");
PrintWriter out = response.getWriter();
// out.println();
out.write(jsonStr);
out.flush();
out.close();
}
@Override
public void init(FilterConfig arg0) throws ServletException {
System.out.println("token過濾器初始化了");
}
@Override
public void destroy() {
}
}
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>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<title></title>
</head>
<body>
<button id="gettoken">點擊ajax獲取token</button>
<textarea id="token" rows="5" cols="25" style="width: 300px;" placeholder="token值"></textarea>
<br />
<br />
<button id="validtoken">點擊解析上面的token</button><br/>
<textarea id="result" readonly rows="5" cols="25" style="width: 300px;" placeholder="數據解析結果"></textarea>
<script src="jquery-2.1.0.js" type="text/javascript" charset="utf-8"></script>
<script>
$(function () {
$("#gettoken").on("click",function () {
$.ajax({
type:"put",
url:"http://localhost:8080/JWT/author/token",
async:true,
success:function(data){
$("#token").val(data);
}
});
});
$("#validtoken").on('click',function (e) {
var token=$.trim($("#token").val());
if(!token.length){
alert("請先獲取token");
return;
}
$.ajax({
type:"get",
dataType:"json",
url:"http://localhost:8080/JWT/author/token?r="+Math.random(),
async:true,
beforeSend: function(request) {
request.setRequestHeader("token", token);
},
success:function (data) {
$("#result").val(JSON.stringify(data));
}
});
});
})
</script>
</body>
</html>
具體代碼地址:https://github.com/bigmeow/JWT
