前言
JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。
1、概念
- 基于JSON的开发标准
- 用户信息加密到token里,服务器不保存任何用户信息
2、与Cookie/session的对比
- 在传统的用户登录认证中,因为http是无状态的,所以都是采用session方式。用户登录成功,服务端会保证一个session,当然会给客户端一个sessionId,客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId。
- JWT方式校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录,验证token更为简单。
3、JWT构成与交互流程
第一部分为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature)。【中间用 . 分隔】
一个标准的JWT生成的token格式如下:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI1IiwiaWF0IjoxNTY1NTk3MDUzLCJleHAiOjE1NjU2MDA2NTN9.qesdk6aeFEcNafw5WFm-TwZltGWb1Xs6oBEk5QdaLzlHxDM73IOyeKPF_iN1bLvDAlB7UnSu-Z-Zsgl_dIlPiw
3.1、Jwt的头部承载两部分信息
声明类型,这里是jwt
声明加密的算法,通常直接使用HMACSHA256,就是HS256了
{
“alg”: “HS256”,
“typ”: “JWT”
}
然后将头部进行base64编码构成了第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
然后我们看第二部分:载荷
载荷,即承载的意思。也就是说这里是承载消息具体内容的地方。
(你在令牌上附带的信息:比如用户的姓名,这样以后验证了令牌之后就可以直接从这里获取信息而不用再查数据库了)
内容又可以分为3种标准
标准中注册的声明
公共的声明
私有的声明
【标准声明】
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
【公共声明】
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密
【私有声明】
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息
对Payload进行Base64加密就得到了JWT第二部分的内容。
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
第三部分需要base64加密后的header和base64加密后的payload使用 . 连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT的第三部分。
(对前两部分的签名,防止数据篡改)
验证流程:
① 在头部信息中声明加密算法和常量, 然后把header使用json转化为字符串
② 在载荷中声明用户信息,同时还有一些其他的内容;再次使用json 把载荷部分进行转化,转化为字符串
③ 使用在header中声明的加密算法和每个项目随机生成的secret来进行加密, 把第一步分字符串和第二部分的字符串进行加密, 生成新的字符串。词字符串是独一无二的。
④ 解密的时候,只要客户端带着JWT来发起请求,服务端就直接使用secret进行解密。
4、缺点
- 安全性
- 性能
- 一次性
5、Spring boot与JWT整合
5.1、添加模块
authority-service
5.2、添加依赖
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2021.0.4.0</version> </dependency>
5.3、修改配置文件
server: port: 7777 spring: application: name: auth-service cloud: nacos: discovery: server-addr: localhost:8848 #Nacos server 的地址 config: jwt: # 加密密钥 secret: tigerkey # token有效时长 expire: 3600 # header 名称 header: token
5.4、添加类 config
package com.xxxx.springCloud.auth.config; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.Date; @Component @ConfigurationProperties(prefix = "config.jwt") @Data public class JwtConfig { /** * 密钥 */ private String secret; /** * 过期时间 */ private Long expire; /** * 头部 */ private String header; /** * 生成token * @param subject * @return */ public String createToken(String subject){ Date nowDate = new Date(); Date expireDate = new Date(nowDate.getTime() + expire * 1000); return Jwts.builder() .setHeaderParam("typ","JWT") .setSubject(subject) .setIssuedAt(nowDate) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS512,secret) .compact(); } /** * 获取token中的注册信息 * @param token * @return */ public Claims getTokenClaim(String token){ try{ return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); }catch (Exception e){ return null; } } /** * 验证token是否过期 * @param expirationTime * @return */ public boolean isTokenExpired(Date expirationTime){ if(null == expirationTime){ return true; }else{ return expirationTime.before(new Date()); } } /** * 获取token的失效时间 * @param token * @return */ public Date getExpirationDateFromToken(String token){ Claims tokenClaim = this.getTokenClaim(token); if(tokenClaim == null){ return null; }else{ return this.getTokenClaim(token).getExpiration(); } } /** * 获取token中的用户名 * @param token * @return */ public String getUserNameFromToken(String token){ return this.getTokenClaim(token).getSubject(); } /** * 获取token中发布时间 * @param token * @return */ public Date getIssuedDateFromToken(String token){ return this.getTokenClaim(token).getIssuedAt(); } }
5.5、添加controller
package com.xxxx.springCloud.auth.controller; import com.xxxx.springCloud.auth.config.JwtConfig; import com.xxxx.springCloud.common.entity.UserInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/auth") public class AuthController { @Autowired private JwtConfig jwtConfig; @PostMapping("/login") public Map<String,String> login(@RequestBody UserInfo userInfo){ String token = jwtConfig.createToken(userInfo.getUserAccount()); Map<String, String> map = new HashMap<String, String>(); map.put("token",token); return map; } /** * token是否过期 * @param token * @return */ @PostMapping("/isTokenExpiration") public Boolean isTokenExpiration(@RequestParam String token){ return this.jwtConfig.isTokenExpired(this.jwtConfig.getExpirationDateFromToken(token)); } }
5.6、gateway工程 改造
5.6.1、新增线程类UrlThread
package com.xxxx.springCloud.gateway.hread; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import java.util.concurrent.Callable; public class UrlThread implements Callable<String> { private final LoadBalancerClient loadBalancerClient; public UrlThread(LoadBalancerClient loadBalancerClient) { this.loadBalancerClient = loadBalancerClient; } @Override public String call() throws Exception { ServiceInstance serviceInstance = this.loadBalancerClient.choose("auth-service"); return "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/auth/isTokenExpiration"; } }
5.6.2、新增GlobalBeanConf类
@Configuration public class GlobalBeanConf { @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } }
5.6.3、全局过滤器改造
//负载均衡获取微服务实例 private final LoadBalancerClient loadBalancerClient; //远程调用 private final RestTemplate restTemplate; public DrfGlobalFilter(LoadBalancerClient loadBalancerClient, RestTemplate restTemplate) { this.loadBalancerClient = loadBalancerClient; this.restTemplate = restTemplate; }
//启动获取url的线程 FutureTask<String> stringFutureTask = new FutureTask<String>(new UrlThread(this.loadBalancerClient)); new Thread(stringFutureTask).start(); String url = stringFutureTask.get(); Boolean aBoolean = this.restTemplate.postForObject(url+"?token="+token, null,Boolean.class);
package com.xxxx.springCloud.gateway.filter; import com.xxxx.springCloud.common.dto.TokenDTO; import com.xxxx.springCloud.gateway.service.AuthService; import com.xxxx.springCloud.gateway.thread.ValidateTokenTask; import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; @Component public class DrfGlobalFilter implements GlobalFilter, Ordered { private final AuthService authService; private ExecutorService executorService; public DrfGlobalFilter(AuthService authService) { this.authService = authService; this.executorService = Executors.newFixedThreadPool(5); } @SneakyThrows @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); //如果登录请求,不用验证token String path = request.getURI().getPath(); if(!path.contains("login")){ HttpHeaders headers = request.getHeaders(); String token = headers.getFirst("token"); //token为空表示没有登录,否则已经登录 if(StringUtils.isBlank(token)){ ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); }else{ TokenDTO tokenDTO = new TokenDTO(); tokenDTO.setToken(token); //token验证不通过,返回给前端401 FutureTask<Boolean> booleanFutureTask = new FutureTask<>(new ValidateTokenTask(this.authService, token)); this.executorService.submit(booleanFutureTask); Boolean aBoolean = booleanFutureTask.get(); if(aBoolean){ ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } } } return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
package com.xxxx.springCloud.gateway.service; import com.xxxx.springCloud.common.dto.TokenDTO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @FeignClient(value = "auth-service") public interface AuthService { @PostMapping("/auth/validateToken") public Boolean validateToken(TokenDTO tokenDTO); } package com.xxxx.springCloud.gateway.config; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import java.util.stream.Collectors; @Configuration public class GlobalConf { @Bean @ConditionalOnMissingBean public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) { return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList())); } }
package com.xxxx.springCloud.gateway.thread; import com.xxxx.springCloud.common.dto.TokenDTO; import com.xxxx.springCloud.gateway.service.AuthService; import java.util.concurrent.Callable; public class ValidateTokenTask implements Callable<Boolean> { private final AuthService authService; private final String token; public ValidateTokenTask(AuthService authService, String token) { this.authService = authService; this.token = token; } @Override public Boolean call() throws Exception { TokenDTO tokenDTO = new TokenDTO(); tokenDTO.setToken(this.token); return this.authService.validateToken(tokenDTO); } }