JWT(java web token) 使用整理合集

1: JSON Web Token是什么

JWT——Json web token
是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准,可实现无状态、分布式的Web应用授权。

2:JWT结构

JWT包含了使用.分隔的三部分: Header 头部 Payload 负载 Signature 签名

其结构看起来是这样的Header.Payload.Signature

Header

在header中通常包含了两部分:token类型和采用的加密算法。{ "alg": "HS256", "typ": "JWT"} 接下来对这部分内容使用 Base64Url 编码组成了JWT结构的第一部分。

Payload

Token的第二部分是负载,它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据,有三种类型的claim:reserved, public 和 private.Reserved claims: 这些claim是JWT预先定义的,在JWT中并不会强制使用它们,而是推荐使用,常用的有 iss(签发者),exp(过期时间戳), sub(面向的用户), aud(接收方), iat(签发时间)。 Public claims:根据需要定义自己的字段,注意应该避免冲突 Private claims:这些是自定义的字段,可以用来在双方之间交换信息 负载使用的例子:{ "sub": "1234567890", "name": "John Doe", "admin": true} 上述的负载需要经过Base64Url编码后作为JWT结构的第二部分。

Signature

创建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。例如如果希望使用HMAC SHA256算法,那么签名应该使用下列方式创建: HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) 签名用于验证消息的发送者以及消息是没有经过篡改的。 完整的JWT 完整的JWT格式的输出是以. 分隔的三段Base64编码,与SAML等基于XML的标准相比,JWT在HTTP和HTML环境中更容易传递。 下列的JWT展示了一个完整的JWT格式,它拼接了之前的Header, Payload以及秘钥签名。

3:JWT使用方式

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息里面。另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

4: JWT使用场景

下列场景中使用JSON Web Token是很有用的:

Authorization (授权) :

这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现

在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。

Information Exchange (信息交换) :

 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。

5:基于token认证和服务器认证的区别

 

5.1 基于服务器的认证

HTTP协议是无状态的,也就是说,如果我们已经认证了一个用户,那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证

传统的做法是将已经认证过的用户信息存储在服务器上,比如Session。用户下次请求的时候带着Session ID,然后服务器以此检查用户是否认证过。

这种基于服务器的身份认证方式存在一些问题:

Sessions : 每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大。

Scalability : 由于Session是在内存中的,这就带来一些扩展性的问题。

CORS : 当我们想要扩展我们的应用,让我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题。当使用AJAX调用从另一个域名下获取资源时,我们可能会遇到禁止请求的问题。

CSRF : 用户很容易受到CSRF攻击。

 

5.2 jwt与session的差异

相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。

Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。

而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。

Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。

 

 

6: 基于JWT认证流程

基于Token的身份认证是无状态的,服务器或者Session中不会存储任何用户信息。

没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。

虽然这一实现可能会有所不同,但其主要流程如下:

1: 用户携带用户名和密码请求访问。

2: 服务器校验用户凭据。

3: 应用提供一个token给客户端。

4: 客户端存储token,并且在随后的每一次请求中都带着它。

5: 服务器校验token并返回数据。

注意:

1: 每一次请求都需要token。

2: Token应该放在请求header中。

3:我们还需要将服务器设置为接受来自所有域的请求,用Access-Control-Allow-Origin: *。

7:JWT机制优缺点

优点:

1:支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.

2:无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.

3:更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.

4:去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.

5:更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。

6:CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。

7:性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.

8:基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft)。

缺点:

1:JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

2:JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令×××的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

3:为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

8:JWT的整合使用

1:引入依赖

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>2.2.0</version>
</dependency>

2:工具类

import com.auth0.jwt.JWTSigner;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.internal.com.fasterxml.jackson.databind.ObjectMapper;

import java.util.HashMap;
import java.util.Map;

/**
 * JWT工具类
 */
public class JWTUtils {

    private static final String SECRET = "XX#$%()(#*!()!KL<><MQLMNQNQJQK sdfkjsdrow32234545fdf>?N<:{LWPW";
    private static final String EXP = "exp";
    private static final String PAYLOAD = "payload";

    //加密,传入一个对象和有效期
    public static <T> String sign(T object, long maxAge) {
        try {
            final JWTSigner signer = new JWTSigner(SECRET);
            final Map<String, Object> claims = new HashMap<String, Object>();
            ObjectMapper mapper = new ObjectMapper();
            String jsonString = mapper.writeValueAsString(object);
            claims.put(PAYLOAD, jsonString);//传输相关信息
            claims.put(EXP, System.currentTimeMillis() + maxAge);//过期时长
            return signer.sign(claims);
        } catch (Exception e) {
            return null;
        }
    }

    //解密,传入一个加密后的token字符串和解密后的类型
    public static <T> T unsign(String jwt, Class<T> classT) {
        final JWTVerifier verifier = new JWTVerifier(SECRET);
        try {
            final Map<String, Object> claims = verifier.verify(jwt);
            if (claims.containsKey(EXP) && claims.containsKey(PAYLOAD)) {
                long exp = (Long) claims.get(EXP);
                long currentTimeMillis = System.currentTimeMillis();
                if (exp > currentTimeMillis) {
                    String json = (String) claims.get(PAYLOAD);
                    ObjectMapper objectMapper = new ObjectMapper();
                    return objectMapper.readValue(json, classT);
                }
            }
            return null;
        } catch (Exception e) {
            return null;
        }
    }

}

3:登录放在header中返回给前台。

public static Long jwtMaxAge = 30L * 60L * 1000L。半小时。

4:每次请求验证

import com.rongji.common.annotation.Log;
import com.rongji.common.config.Constant;
import com.rongji.common.entity.LogEntity;
import com.rongji.common.service.LogService;
import com.rongji.common.utils.*;
import com.rongji.system.entity.JWTentity;
import com.rongji.system.entity.SysUserTokenEntity;
import com.rongji.system.service.SysUserService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.*;


@Aspect
@Component
public class LogAspect {
    private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);

    @Autowired
    LogService logService;

    @Autowired
    SysUserService userService;

    @Pointcut("execution(* com.*.controller..*(..)))")
    public void logPointCut() {
    }

    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) {
        //判断token信息
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        String token = request.getHeader("token");
        //如果传的值不存在:1:登录 2:过期
        String uri = request.getRequestURI();
        String method = request.getMethod();
        boolean isLogin = StringUtils.equals("/login", uri) && StringUtils.equals("POST", method);
        JWTentity user = null;
        R r =R.error(Constant.jwtErrorCode, "session失效,请重新登录");
        if (StringUtils.isEmpty(token)) {
            //进入的是登录界面
            if (!isLogin) {
                //其他接口:session过期
                return r;
            }
        } else if (!isLogin) {
            //校验
            user = JWTUtils.unsign(token, JWTentity.class);
            if (user == null) {
                //返回错误信息
                return r;
            } else {
                Long userId = user.getUserId();
                //判断是否在黑名单里面
                List tokens = userService.getUserToken(userId);
                if (tokens.contains(token)) {
                    return r;
                }
                // 执行时长(毫秒)
                long time = Constant.jwtMaxAge - user.getLoginDate().getSeconds();
                if (0 < time && time <= Constant.jwtDelayMinAge) {
                    return r;
                }
                //延时
                delayTime(token, user, userId, time);
            }
        }
        long beginTime = System.currentTimeMillis();
        // 执行方法
        Object result = null;
        try {
            result = point.proceed();
        } catch (Throwable throwable) {
           logger.error(throwable.toString());
        }
        // 执行时长(毫秒)
        long time = System.currentTimeMillis() - beginTime;
        //异步保存日志
        saveLog(point, time, user);
        return result;
    }

    private void delayTime(String token, JWTentity user, Long userId, long time) {
        if (time > 0 && time < Constant.jwtDelayMaxAge) { //延时
            HttpServletResponse response = HttpContextUtils.getHttpServletResponse();
            user.setLoginDate(new Date());
            String jwtToken = JWTUtils.sign(user, Constant.jwtMaxAge);
            response.setHeader("token", jwtToken);
            //保存黑名单
            SysUserTokenEntity userToken = new SysUserTokenEntity();
            userToken.setUserId(userId);
            userToken.setTokenId(token);
            userToken.setOperDate(new Date());
            userService.saveUserTokenEntity(userToken);
        }
    }

    void saveLog(ProceedingJoinPoint joinPoint, long time, JWTentity user) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        LogEntity sysLog = new LogEntity();
        //Log syslog = method.getAnnotation(Log.class);
        //if (syslog != null) {
            // 注解上的描述
           // sysLog.setOperation(className + "." + methodName + "()");
       // }
        // 请求的方法名
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = signature.getName();

        sysLog.setMethod(className + "." + methodName + "()");
        // 请求的参数
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        Object[] args = joinPoint.getArgs();
//        String params1 = JSONUtils.beanToJson(args[0]);
  //      sysLog.setParams(params1);

        // 获取request
        // HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        StringBuffer url = request.getRequestURL();
        String uri = request.getRequestURI();
        // 设置IP地址
        String ip = IPUtils.getIpAddr(request);
        sysLog.setIp(ip);
        if (user != null) {
            Long userId = user.getUserId();
            if (userId != null) {
                sysLog.setUserId(userId);
            }
            String userName = user.getUserName();
            if (StringUtils.isNotEmpty(userName)) {
                sysLog.setUsername(userName);
            }
        }
        sysLog.setTime((int) time);
        // 系统当前时间
        Date date = new Date();
        sysLog.setGmtCreate(date);
        // 保存系统日志
        logService.save(sysLog);
    }

}

前台vue实现,接收token,并放在header中每次请求带着:

import axios from 'axios';
import Qs from 'qs';
import router from '../router';
import {MessageBox} from 'element-ui';

let host = "http://192.168.1.178:7171";
const instance = axios.create({
  baseURL: host,
  headers: {'content-type': 'application/x-www-form-urlencoded'},
  withCredentials: true
});
instance.interceptors.request.use(config => {
 // 判断是否存在token,如果存在的话,则每个http header都加上token
  let jsessionid = localStorage.getItem("token");
   if(jsessionid) {
    config.headers.token = jsessionid;
   }
   return config;
})

instance.interceptors.response.use(res =>{
   let ssession = res.headers.token;
   if(ssession){
     localStorage.setItem("token",ssession);
   }
  return handleResponse(res);
})

 

问题:由于前后端分离问题,需要在response中设置参数:

import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpStatus;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@ServletComponentScan
@WebFilter(urlPatterns = "/*",filterName = "CORSFilter")
public class CORSFilter implements Filter {

    private static final long serialVersionUID = 1L;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpServletRequest request=(HttpServletRequest)servletRequest;
        response.setContentType("text/plain;charset=UTF-8");
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT, OPTIONS");
        response.setHeader("Access-Control-Max-Age", "1728000");
        response.setHeader("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since, " +
                "Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,userId,token");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("XDomainRequestAllowed","1");
        //添加可以返回自定义header信息
        response.setHeader("Access-Control-Expose-Headers","token");
        if ("OPTIONS".equals(request.getMethod())){//这里通过判断请求的方法,判断此次是否是预检请求,如果是,立即返回一个204状态吗,标示,允许跨域;预检后,正式请求
            response.setStatus(HttpStatus.SC_NO_CONTENT); //HttpStatus.SC_NO_CONTENT = 204
            return;
        }
     
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {

    }

    /**
     * 重新封装request包装类
     */
    class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
        private String url;

        public MyHttpServletRequestWrapper(HttpServletRequest request,String url) {
            super(request);
            this.url=url;
        }
        @Override
        public String getServletPath() {
            if(super.getDispatcherType().name().equals("REQUEST")) {
                return url;
            } else {
                return super.getServletPath();
            }
        }
    }

}
  • 6
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值