问题描述:
现在分布式微服务的逐渐使用广泛,前后端分离已经成为互联网项目开发标准,它会为以后的大型分布式架构打下基础。为以后服务的横向扩展提供了方便
JSON Web Tokens(JWT)能提供基于JSON格式的安全认证。JWT可以跨不同语言,自带身份信息,并且非常容易传递。
JWT即JSON WEB TOKEN的缩写,轻量级的令牌认证(相比于oauth),可用于数据交换间的安全传输,同时可使用公钥/私钥的非对称算法对信息进行数字签名,如RS256算法。
它由3部分组成:
使用场景:
1、单点登录,此场景也是使用最广的一种,流程原理很简单:用户登录成功-》生成token返回前端并保存(cookie或localstorage)-》之后每次请求在header或body中带此token-》服务器验证此token以保证数据合法性
官方给的单点登录流程图
2、与第三方接口的数据传输,在处理对外接口时(特别是公司之外业务)可很方便的作为一个约定,可很好的减少由于数据安全性问题所要做的沟通工作。
优点:
JWT是基于JSON格式的数据安全认证,可以跨平台、跨语言、自带身份信息、非常容易传递
缺点:
jwt涉及base64及加密处理,所以会使传输的数据会比裸传数据大很多,这方面大家可以根据实际情况做平衡取舍。对于公司内部的项目及对传输数据量有极高要求的更要慎重考虑用jwt方式;
和Session方式存储id的差异
Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态。一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存储。
而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分组等。虽说JWT方式让服务器有一些计算压力(例如加密、编码和解码),但是这些压力相比磁盘存储而言可能就不算什么了。具体是否采用,需要在不同场景下用数据说话。
实战部分
创建实体类
@Component
public class JWT {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 加密秘钥
*/
@Value( "${secret}" )
private String secret;
/**
* 有效时间
*/
@Value( "${expire}" )
private long expire;
/**
* 用户凭证
*/
@Value( "${header}" )
private String header;
/**
* 获取:加密秘钥
*/
public String getSecret() {
return secret;
}
/**
* 设置:加密秘钥
*/
public void setSecret(String secret) {
this.secret = secret;
}
/**
* 获取:有效期(s)
* */
public long getExpire() {
return expire;
}
/**
* 设置:有效期(s)
* */
public void setExpire(long expire) {
this.expire = expire;
}
/**
* 获取:凭证
* */
public String getHeader() {
return header;
}
/**
* 设置:凭证
* */
public void setHeader(String header) {
this.header = header;
}
/**
* 生成Token签名
* @param userId 用户ID
* @return 签名字符串
*/
public String generateToken(long userId) {
logger.info("header= " + getHeader() + ", expire=" + getExpire() + ", secret=" + getSecret());
Date nowDate = new Date();
// 过期时间
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(String.valueOf(userId))
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, getSecret()).compact();
// 注意: JDK版本高于1.8, 缺少 javax.xml.bind.DatatypeConverter jar包,编译出错
}
/**
* 获取签名信息
* @param token
*/
public Claims getClaimByToken(String token) {
try {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
logger.debug("validate is token error ", e);
return null;
}
}
/**
* 判断Token是否过期
* @param expiration
* @return true 过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
}
@Component
public class JwtInterceptor extends HandlerInterceptorAdapter {
@Autowired
private JWT jwt;
public static final String USER_KEY = "userId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String servletPath = request.getServletPath();
System.out.println("ServletPath: " + servletPath);
boolean isCheck = isCheck(servletPath);
// 不需要验证,直接放行
if (!isCheck) {
return true;
}
// 需要验证
String token = getToken(request);
if (StringUtils.isBlank(token)) {
throw new RuntimeException(jwt.getHeader() + "失效,请重新登录");
}
// 获取签名信息
Claims claims = jwt.getClaimByToken(token);
System.out.println("TOKEN: " + claims);
// 判断签名是否存在或过期
boolean b = claims==null || claims.isEmpty() || jwt.isTokenExpired(claims.getExpiration());
if (b) {
throw new RuntimeException(jwt.getHeader() + "失效,请重新登录");
}
// 将签名中获取的用户信息放入request中;
request.setAttribute(USER_KEY, claims.getSubject());
return true;
}
/**
* 根据URL判断当前请求是否需要校验, true:需要校验
*/
private boolean isCheck(String servletPath) {
// for (String path : NOT_CHECK_URL) {
// if (servletPath.startsWith(path)) {
// return false;
// }
// }
return false;
}
/**
* 获取请求Token
*/
private String getToken(HttpServletRequest request) {
String token = request.getHeader(jwt.getHeader());
if (StringUtils.isBlank(token)) {
token = request.getParameter(jwt.getHeader());
}
return token;
}
/**
* 不用拦截的页面路径(也可存入数据库中)
*/
private static final String[] NOT_CHECK_URL = {};
}
讲拦截器注册到spring中
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
private JwtInterceptor jwtInterceptor;
/**
* 设置资源文件路径
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/statics/**").addResourceLocations("classpath:/statics/");
}
/**
*
* APP接口拦截器
* */
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor).addPathPatterns("/*");
}
}