一.简介
JSON Web Token, 通过JSON形式作为Web应用中的令牌,用于各方之间安全地将信息作为JSON传输对象,在传输过程中完成数据加密、签名等相关处理。(使用HMAC/RSA/ECDSA的公钥/私钥进行签名)
二.JWT能做什么?
- 授权
一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用的一项功能,因为它的开销很小并且可以在不同的域中使用。
- 信息交换
Jsw是在各方(如两台服务器)之间安全传输信息的解决方式——可以对JWT进行签名(例如,使用公钥/私钥对),可确保调用方是确定的对象(验签)。此外,由于签名是使用标头和有效负载计算的,所以还可以验证内容是否遭到篡改。
三.基于传统的Session认证
1.认证方式
http协议是一种无状态的协议(如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不知道是哪个用户发出的请求,所以为了让应用能识别出是哪个用户发出的请求,只能在服务器存储一份用户登录的信息,这份登录信息(cookie)会在响应时传递给浏览器,以便下次请求时将cookie发给服务器,这样我们的服务器就能识别请求来自哪个用户了==>传统的基于session认证。
2.认证流程
客户端发起请求,认证通过 往session中存储用户信息,sessionid以响应的形式(cookie中记录了sessionId)写到客户端
- 暴漏问题
①每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录(以便用户下次请求的鉴别,通常而言session都保存在内存中,而随着认证用户的增多,服务端的开销会明显增大)。
②用户认证之后,服务端做认证记录,如果认证的记录保存在内存中的话,这意味着用户下次请求仍要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
③因为基于cookie来进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击
④在前后端分离系统中会更加痛苦:
前后端分离在应用解耦后增加了部署的复杂性。通常用户一次请求就要转发多次。如果用session,每次携带sessionId到服务器,服务器还要查询用户信息。同时如果用户很多,这些信息存储到服务器内存中,给服务器增加负担;CSRF(跨站伪造请求攻击),session是基于cookie进行用户识别的,cookie若被截获,用户很容易受到跨站请求伪造的攻击;session就是一个特征值,表达的信息不够丰富,不容易扩展,如果后端应用是多点部署,那么就需要实现session共享机制,不方便集群应用。
(前端web服务可能部署在nginx上,后端代理层路由网关又可能是个服务,路由网关中对应着真实的后端服务节点,用户发起请求会携带sessionId经过前端web,代理层,把网关sessionId传给后端应用的某个节点上,若后端应用是集群部署,还要实现session的共享)
四.基于JWT认证
Jwt完全是基于令牌的验证方式,而且是基于客户端的令牌存储(session是存储在服务器端)
认证流程:
- 前端通过Web表单将自己的用户名和密码发送到后端的接口,这一过程一般是一个HTTP POST请求。建议使用SSL加密传输(https协议),从而避免敏感信息被嗅探。
- 后端核对用户名和密码成功后,将用户的ID等其他信息作为JWT Playload,将其与头部分别进行Base64编码拼接后签名,形成一个JWT(token)
- 后端将JWT字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时,前端删除保存的JWT即可。
- 前端在每次请求时将JWT放入HTTP Head中的Authorization中。
- 后端检查是否存在,如存在验证JWT的有效性。如:检查签名是否正确;检查Token是否过期;检查Token的接收方式是否是自己;
- 验证通过后,后端使用JWT中包含的用户信息进行其他逻辑操作。返回相应结果。
五.Jwt优势:
- 简洁:可以通过URL,POST参数或在HTTP header中发送,数据量小,传输速度也快。
- 自包含:负载中包含了所用用户 需要的信息,避免了多次查询数据库
- Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何WEB形式都支持。
- 不需要在服务器端保存会话信息,特别适用于分布式微服务。
六.JWT的结构:
1.Token组成:header.payload.signature(头.负载.签名)
2.Header由两部分:令牌类型(值为JWT)和所使用的签名算法(HMAC,SHA256,RSA)它会使用Base64编码组成JWT结构的第一部分
{
“alg”:”HS256”,
“typ”:”JWT”
}
注:Base64是一种编码,即它是可以被翻译回来的,它不是一种加密过程
3.Payload(有效负载)
其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用Base64编码组成JWT结构的第二部分
{
“sub”:”123456”,
“name”:”xxx”,
“role”:”admin”
}
- Signature
前面两部分都是使用Base64进行编码的,即前端可以解开知道其中的信息。Signature需要使用编码后的header和payload以及我们体哦那个的一个密钥,然后使用header中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过。
如:
HMACSHA256(base64UrlEncode(header))+”.”+base64UrlEncode(payload),secret);
签名的目的:
最后一步签名的过程,是对头部以及负载内容进行签名,如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务端会判断出新的头部和负载形成的签名,其与JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时使用的密钥的话,得出来的签名也是不一样的。
信息安全问题:
Base64是一种编码,是可逆的,那么信息不就被暴露了吗?
-是的,所以在JWT中,不应该在负载中加入任何敏感数据,如密码,否则怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。
- JWT与springboot整合利用到实战中
1.所需pom依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
2.封装JWT工具类
public class JWTUtils {
private static final String SING = "!Q@W3e4r";
//生成Token head.payload.sing
public static String getToken(Map<String,String> map){
Calendar instance = Calendar.getInstance();
//过期时间
instance.add(Calendar.SECOND,30);
//创建jwt builder
JWTCreator.Builder builder = JWT.create();
//payload
map.forEach((k,v) ->{
builder.withClaim(k,v);
});
// String token = JWT.create()
// .withClaim("userId","001")
// .withClaim("username","szh")//payload 负载
// .withExpiresAt(instance.getTime())//指定令牌过期时间
// .sign(Algorithm.HMAC256(SING));//签名
String token = builder.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256(SING));
return token;
}
//验证token合法性
public static void verify(String token){
JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
}
//验证后获取token中的信息
public static DecodedJWT getTokenInfo(String token){
DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
return verify;
}
}
2.编写controller
@RestController
@RequestMapping(path = "/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping(path = "/login")
public Map<String,Object> login(User user){
Map<String,Object> map = new HashMap<>();
try{
User userDB = userService.login(user);
Map<String,String> payload = new HashMap<>();
payload.put("id",userDB.getId());
payload.put("name",userDB.getName());
String token = JWTUtils.getToken(payload);
map.put("state",true);
map.put("msg","认证成功");
map.put("token",token);
}catch (Exception e){
map.put("state",false);
map.put("msg",e.getMessage());
}
return map;
}
@PostMapping("/test")
public Map<String,Object> test(HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
//处理自己的业务逻辑
String token = request.getHeader("token");
DecodedJWT verify = JWTUtils.getTokenInfo(token);
System.out.println("用户ID:"+verify.getClaim("id").asString());
System.out.println("用户name:"+verify.getClaim("name").asString());
map.put("state",true);
map.put("msg","请求成功");
return map;
}
}
3.实现类
@Service
public class UserServiceImpl implements UserService {
@Override
public User login(User user) {
//模拟数据库的用户 若数据库中存在与客户端传来的对象,则认证成功
User userDB = new User("001","szh","admin");
return userDB;
// else throw new RuntimeException("登录失败");
}
}
3.配置JWT拦截器(从httpservletquest中拿到token,用于token验证)
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception{
Map<String,Object> map = new HashMap<>();
//jwt藏在请求头中获取
String token = request.getHeader("token");
try{
DecodedJWT tokenInfo = JWTUtils.getTokenInfo(token);
return true;
}catch (SignatureVerificationException e){
e.printStackTrace();
map.put("msg","无效签名");
}catch (TokenExpiredException e){
e.printStackTrace();
map.put("msg","token过期");
}catch (AlgorithmMismatchException e){
e.printStackTrace();
map.put("msg","toekn算法不一致");
}catch (Exception e){
e.printStackTrace();
map.put("msg","token无效");
}
map.put("state",false);
//将map转为json
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
4.添加 Interceptor配置类(实现WebMvcConfigurer配置类中的addInterceptorRegistry方法,用于添加拦截器,拦截路径,可放行路径)
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/user/test")//拦截(进行token验证)
.excludePathPatterns("/user/login");//放行
}
}