传统验证方式
在前后端分离的场景中,包括APP端和WEB端,由于http协议是无状态的,那么用户登录认证之后,再向服务器发送的请求的时候,服务器是不知道这个这个用户是否已经登录认证过了。
- 传统的方式是通过cookie和session,用户登录之后将用户信息存储在session中,下次请求的时候cookie里面会携带sessionId,以此来判断用户是否登录过。
这样做的话,每次用户认证通过之后,服务器都需要保持一份用户信息存在session中,这样登录的用户变多的时候,服务器的开销会很大。
另一方面,在扩展性方面,由于session是在服务器端的,在搭建分布式服务的话,需要每一台服务器都认证一下,这样显示是不合理的。 - 将token存在redis中,这种方案也是大多数实行的方案,这种方案的弊端是,在每一次请求的时候,都需要拦截请求,获取token,然后再判断redis中是否有这个数据,这种方案弊端是对redis的依赖太大了,如果redis宕机了,那么整个项目都不能运行了。
JWT是什么
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
JWT流程大概是,用户登录之后,给用户生成jwt并返回给客户端,客户端每次请求的时候会携带这个jwt,然后再在拦截器中拦截请求,判断当前是否正确或者过期。
JWT机制优缺点
优点:
- 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
- 无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
- 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
- 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
- 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
- CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
- 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
- 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft)
缺点:
- JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
- JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令×××的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
使用
依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>2.2.0</version>
</dependency>
jwtUtil
@Component
@ConfigurationProperties("config.jwt")
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Slf4j
public class JwtUtil {
/**
* 过期时间为一天
*/
private long expire;
/**
* token私钥
*/
private String secret;
private String header;
/**
* 生成签名,15分钟后过期
* @param username
* @param userId
* @return
*/
public String sign(String username,String userId){
//过期时间
Date date = new Date(System.currentTimeMillis() + expire* 1000);
//私钥及加密算法
Algorithm algorithm = Algorithm.HMAC256(secret);
//设置头信息
HashMap<String, Object> header = new HashMap<>(2);
header.put("typ", "JWT");
header.put("alg", "HS256");
//附带username和userID生成签名
return JWT.create().withHeader(header).withClaim("username",username)
.withClaim("userId",userId).withExpiresAt(date).sign(algorithm);
}
public boolean verity(String token){
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
log.info("================认证成功===============");
return true;
} catch (IllegalArgumentException e) {
return false;
} catch (JWTVerificationException e) {
return false;
}
}
}
拦截器
@Component
@Slf4j
public class JwtInterceptor extends HandlerInterceptorAdapter {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 地址过滤
String uri = request.getRequestURI() ;
if (uri.contains("/login")){
return true ;
}
String token = request.getHeader(jwtUtil.getHeader());
if(StringUtils.isEmpty(token)){
token = request.getParameter(jwtUtil.getHeader());
}
log.info("token:{}",token);
if(StringUtils.isEmpty(token)){
throw new BizException(jwtUtil.getHeader()+ "不能为空");
}
if(!jwtUtil.verity(token)){
throw new BizException(jwtUtil.getHeader() + "失效,请重新登录");
}
return true;
}
}
配置拦截器和跨域
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
/**
* 设置跨域访问
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "TRACE")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加拦截器
registry.addInterceptor(jwtInterceptor);
}
}
问题:
因为JWT一旦签发,不可撤销,为了防止被盗用,所以设置过期时间比较短,那如果JWT过期了,怎么处理?
在生成JWT的时候,比如设置过期时间为15分钟,同时保存该token到redis中,设置过期时间比如为7天。
当用户在登录后,比如登录后20分钟后,再发送请求的时候,拦截器会判断该token已过期,这时再查一下redis里面有没有这个缓存,如果有的话,代表没有过期,则重新生成JWT返回前端,并且重新刷新redis的过期的时间。
这样就可以解决JWT过期的问题,同时对redis的依赖也没有那么大。