webapp用户身份认证方案 JSON WEB TOKEN 实现
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