传统session交互流程,如下图:
当浏览器向做事器发送登录要求时,验证通过之后,会将用户信息存入seesion中,然后做事器会天生一个sessionId放入cookie中,随后返回给浏览器。
当浏览器再次发送要求时,会在要求头部的cookie中放入sessionId,将要求数据一并发送给做事器。
做事器就可以再次从seesion获取用户信息,全体流程完毕!
常日在做事端会设置seesion的时长,例如 30 分钟没有活动,会将已经存放的用户信息从seesion中移除。
session.setMaxInactiveInterval(30 60);//30分钟没活动,自动移除
同时,在做事端也可以通过seesion来判断当前用户是否已经登录,如果为空表示没有登录,直接跳转到登录页面;如果不为空,可以从session中获取用户信息即可进行后续操作。
在单体运用中,这样的交互办法,是没啥问题的。
但是,如果运用做事器的要求量变得很大,而单台做事器能支撑的要求量是有限的,这个时候就随意马虎涌现要求变慢或者OOM。
办理的办法,要么给单台做事器增加配置,要么增加新的做事器,通过负载均衡来知足业务的需求。
如果是给单台做事器增加配置,要求量连续变大,依然无法支撑业务处理。
显而易见,增加新的做事器,可以实现无限的水平扩展。
但是增加新的做事器之后,不同的做事器之间的sessionId是不一样的,可能在A做事器上已经登录成功了,能从做事器的session中获取用户信息,但是在B做事器上却查不到session信息,此时肯定无比的尴尬,只好退出来连续登录,结果A做事器中的session由于超时失落效,登录之后又被逼迫退出来哀求重新登录,想想都挺尴尬~~
面对这种情形,几位大佬于是合起来切磋,想出了一个token方案。
将各个运用程序与内存数据库redis相连,对登录成功的用户信息进行一定的算法加密,天生的ID被称为token,将token还有用户的信息存入redis;等用户再次发起要求的时候,将token还有要求数据一并发送给做事器,做事端验证token是否存在redis中,如果存在,表示验证通过,如果不存在,见告浏览器跳转到登录页面,流程结束。
token方案担保了做事的无状态,所有的信息都是存在分布式缓存中。基于分布式存储,这样可以水平扩展来支持高并发。
当然,现在springboot还供应了session共享方案,类似token方案将session存入到redis中,在集议论况下实现一次登录之后,每个做事器都可以获取到用户信息。
二、JWT是什么上文中,我们谈到的session还有token的方案,在集议论况下,他们都是靠第三方缓存数据库redis来实现数据的共享。
那有没有一种方案,不用缓存数据库redis来实现用户信息的共享,以达到一次登录,处处可见的效果呢?
答案肯定是有的,便是我们本日要先容的JWT!
JWT全称JSON Web Token,实现过程大略的说便是用户登录成功之后,将用户的信息进行加密,然后天生一个token返回给客户端,与传统的session交互没太大差异。
交互流程如下:
唯一的不同点便是:token存放了用户的基本信息,更直不雅观一点便是将原来放入redis中的用户数据,放入到token中去了!
这样一来,客户端、做事端都可以从token中获取用户的基本信息,既然客户端可以获取,肯定是不能存放敏感信息的,由于浏览器可以直接从token获取用户信息。
JWT详细长什么样呢?JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了JWT字符串。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分:我们称它为头部(header),用于存放token类型和加密协议,一样平常都是固定的;第二部分:我们称其为载荷(payload),用户数据就存放在里面;第三部分:是签证(signature),紧张用于做事真个验证;1、header
JWT的头部承载两部分信息:
声明类型,这里是JWT;声明加密的算法,常日直策应用 HMAC SHA256;完全的头部就像下面这样的JSON:
{ 'typ': 'JWT', 'alg': 'HS256'}利用base64加密,构成了第一部分。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
2、playload
载荷便是存放有效信息的地方,这些有效信息包含三个部分:
标准中注册的声明;公共的声明;私有的声明;个中,标准中注册的声明 (建议但不逼迫利用)包括如下几个部分 :
iss: jwt签发者;sub: jwt所面向的用户;aud: 吸收jwt的一方;exp: jwt的过期韶光,这个过期韶光必须要大于签发韶光;nbf: 定义在什么韶光之前,该jwt都是不可用的;iat: jwt的签发韶光;jwt的唯一身份标识,紧张用来作为一次性token,从而回避重放攻击;公共的声明部分:公共的声明可以添加任何的信息,一样平常添加用户的干系信息或其他业务须要的必要信息,但不建议添加敏感信息,由于该部分在客户端可解密。
私有的声明部分:私有声明是供应者和消费者所共同定义的声明,一样平常不建议存放敏感信息,由于base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{ "sub": "1234567890", "name": "John Doe", "admin": true}然后将其进行base64加密,得到Jwt的第二部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
3、signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的);payload (base64后的);secret (密钥);这个部分须要base64加密后的header和base64加密后的payload利用.连接组成的字符串,然后通过header中声明的加密办法进行加盐secret组合加密,然后就构成了jwt的第三部分。
//javascriptvar encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);var signature = HMACSHA256(encodedString, '密钥');
加密之后,得到signature署名信息。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.连接成一个完全的字符串,就构成了终极的jwt:
//jwt终极格式eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
这个只是通过javascript实现的一个演示,JWT的签发和密钥的保存都是在做事端来完成。
secret用来进行jwt的签发和jwt的验证,以是,在任何场景都不应该流露出去。
三、实战先容了这么多,怎么实现呢?废话不多说,下面我们直接开撸!
<!-- jwt支持 --><dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version></dependency>
然后,创建一个用户信息类,将会通过加密存放在token中
@Data@EqualsAndHashCode(callSuper = false)@Accessors(chain = true)public class UserToken implements Serializable { private static final long serialVersionUID = 1L; / 用户ID / private String userId; / 用户登录账户 / private String userNo; / 用户中文名 / private String userName;}
接着,创建一个JwtTokenUtil工具类,用于创建token、验证token
public class JwtTokenUtil {//定义token返转头部 public static final String AUTH_HEADER_KEY = "Authorization";//token前缀 public static final String TOKEN_PREFIX = "Bearer ";//署名密钥 public static final String KEY = "q3t6w9z$C&F)J@NcQfTjWnZr4u7x";//有效期默认为 2hour public static final Long EXPIRATION_TIME = 1000L60602; / 创建TOKEN @param content @return / public static String createToken(String content){ return TOKEN_PREFIX + JWT.create() .withSubject(content) .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .sign(Algorithm.HMAC512(KEY)); } / 验证token @param token / public static String verifyToken(String token) throws Exception { try { return JWT.require(Algorithm.HMAC512(KEY)) .build() .verify(token.replace(TOKEN_PREFIX, "")) .getSubject(); } catch (TokenExpiredException e){ throw new Exception("token已失落效,请重新登录",e); } catch (JWTVerificationException e) { throw new Exception("token验证失落败!
编写配置类,许可跨域,并且创建一个权限拦截器
",e); } }}
@Slf4j@Configurationpublic class GlobalWebMvcConfig implements WebMvcConfigurer { / 重写父类供应的跨域要求处理的接口 @param registry / @Override public void addCorsMappings(CorsRegistry registry) { // 添加映射路径 registry.addMapping("/") // 放行哪些原始域 .allowedOrigins("") // 是否发送Cookie信息 .allowCredentials(true) // 放行哪些原始域(要求办法) .allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS", "HEAD") // 放行哪些原始域(头部信息) .allowedHeaders("") // 暴露哪些头部信息(由于跨域访问默认不能获取全部头部信息) .exposedHeaders("Server","Content-Length", "Authorization", "Access-Token", "Access-Control-Allow-Origin","Access-Control-Allow-Credentials"); } / 添加拦截器 @param registry / @Override public void addInterceptors(InterceptorRegistry registry) { //添加权限拦截器 registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/").excludePathPatterns("/static/"); }}
利用AuthenticationInterceptor拦截器对接口参数进行验证
@Slf4jpublic class AuthenticationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 从http要求头中取出token final String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY); //如果不是映射到方法,直接通过 if(!(handler instanceof HandlerMethod)){ return true; } //如果是方法探测,直接通过 if (HttpMethod.OPTIONS.equals(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); return true; } //如果方法有JwtIgnore表明,直接通过 HandlerMethod handlerMethod = (HandlerMethod) handler; Method method=handlerMethod.getMethod(); if (method.isAnnotationPresent(JwtIgnore.class)) { JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class); if(jwtIgnore.value()){ return true; } } LocalAssert.isStringEmpty(token, "token为空,鉴权失落败!
末了,在controller层用户登录之后,创建一个token,存放在头部即可
"); //验证,并获取token内部信息 String userToken = JwtTokenUtil.verifyToken(token);//将token放入本地缓存 WebContextUtil.setUserToken(userToken); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //方法结束后,移除缓存的token WebContextUtil.removeUserToken(); }}
/ 登录 @param userDto @return /@JwtIgnore@RequestMapping(value = "/login", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})public UserVo login(@RequestBody UserDto userDto, HttpServletResponse response){ //...参数合法性验证 //从数据库获取用户信息 User dbUser = userService.selectByUserNo(userDto.getUserNo); //....用户、密码验证 //创建token,并将token放在相应头 UserToken userToken = new UserToken(); BeanUtils.copyProperties(dbUser,userToken); String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken)); response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, token); //定义返回结果 UserVo result = new UserVo(); BeanUtils.copyProperties(dbUser,result); return result;}
到这里基本就完成了!
个中AuthenticationInterceptor中用到的JwtIgnore是一个表明,用于不须要验证token的方法上,例如验证码的获取等等。
@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface JwtIgnore { boolean value() default true;}
而WebContextUtil是一个线程缓存工具类,其他接口通过这个方法即可从token中获取用户信息
public class WebContextUtil { //本地线程缓存token private static ThreadLocal<String> local = new ThreadLocal<>(); / 设置token信息 @param content / public static void setUserToken(String content){ removeUserToken(); local.set(content); } / 获取token信息 @return / public static UserToken getUserToken(){ if(local.get() != null){ UserToken userToken = JSONObject.parseObject(local.get() , UserToken.class); return userToken; } return null; } / 移除token信息 @return / public static void removeUserToken(){ if(local.get() != null){ local.remove(); } }}
末了,启动项目,我们来用postman测试一下,看看头部返回结果。
我们把返回的信息提取处理,利用浏览器的base64对前两个部分进行解密。
第一部分,也便是header,结果如下:第二部分,也便是playload,结果如下:可以很清晰的看到,头部、载荷的信息都可以通过base64解密出来。
以是,一定别在token中存放敏感信息!
当我们须要要求其它做事接口时,只须要在要求头部headers中加入Authorization参数即可。
当权限拦截器验证通过之后,在接口方法中只须要通过WebContextUtil工具类就可以获取用户信息。
//获取用户token信息UserToken userToken = WebContextUtil.getUserToken();
四、总结
JWT比较session方案,由于json的通用性,以是JWT是可以进行跨措辞支持的,像JAVA、JavaScript、PHP等很多措辞都可以利用,而session方案只针对JAVA。
由于有了payload部分,以是JWT可以存储一些其他业务逻辑所必要的非敏感信息。
同时,保护好做事端secret私钥非常主要,由于私钥可以对数据进行验证、解密!
如果可以,请利用https协议!