1、什么是JWT
官方文档解释:JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,可以在各方之间作为JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
官网地址: https://jwt.io/introduction/
通俗来讲,JWT是一个含签名并携带用户相关信息的加密串,页面请求校验登录接口时,客户端请求头中携带JWT串到后端服务,后端通过签名加密串匹配校验,保证信息未被篡改。校验通过则认为是可靠的请求,将正常返回数据。
2、JWT解决了什么问题
- 授权:这是最常见的使用场景,解决单点登录问题。因为JWT使用起来轻便,开销小,服务端不用记录用户状态信息(无状态),所以使用比较广泛;
- 信息交换:JWT是在各个服务之间安全传输信息的好方法。因为JWT可以签名,例如,使用公钥/私钥是以对儿 - 可以确定请求方是合法的。此外,由于使用标头和有效负载计算签名,还可以验证内容是否未被篡改
3、早期的SSO认证
我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。
4、JWT认证
首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议)
后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同aaaa.bbb.cc的字符串。 token head.payload.singurater
后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题) HEADER
后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
5、JWT优势
JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:
- 简洁(Compact)
可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快
- 自包含(Self-contained)
负载中包含了所有用户所需要的信息,避免了多次查询数据库
- 自校验
对token可以自己校验是否过期
6、JWT结构
令牌组成
- 标头(Header)
- 有效载荷(Payload)
- 签名(Signature)
Header 标头
标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法。它会使用 Base64 对header做编码,组成而来JWT结构的第一部分。
Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。
{
"alg": "HS256", # 签名算法
"typ": "JWT" # 类型
}
Payload 负载
这部分就是我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。同样的,它也会使用 Base64 编码组成 JWT 结构的第二部分
{
"iss": "demo JWT",
"iat": 1342513302,
"exp": 1342513302,
"name": "admin",
"sub": "dev"
}
Signature 签名
前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。
三个部分通过
.
连接在一起就是我们的 JWT 了
签名的目的
最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
信息安全性
在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?
是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。
因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。
7、Hello-Word
添加依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
生成Token
@Test
void testCreateToken() {
// 1.设置超时时间
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND,30); // 超时时间是30s
// 2.创建JWTbuilder
JWTCreator.Builder builder = JWT.create();
// 3.设置头,负载,签名
String token = builder
// .withHeader(map) 设置头信息,可以不设置有默认值
.withClaim("name", "admin")
.withClaim("id", 10) // 设置用户自定义属性
.withExpiresAt(calendar.getTime()) // 设置令牌超时时间
.sign(Algorithm.HMAC256("dalaoshi"));// 设置用户签名
// 4.输出结果
System.out.println(token);
}
认证token
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYWRtaW4iLCJpZCI6MTAsImV4cCI6MTU5OTQwNTQ2NH0.7YFYieOC-ChS32He7DqyVtECCvM4nFWmb7hKLiPAIXY
";
// 1.根据用户签签名获取JTW校验器
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("dalaoshi")).build();
// 2.验证token
DecodedJWT verify = jwtVerifier.verify(token);
// 3.获取token的数据
System.out.println(verify.getClaim("name").asString()); // 字符串使用asString()
System.out.println(verify.getClaim("id").asInt()); // int使用asInt
System.out.println(verify.getExpiresAt()); // 获取过期时间
认证常见的异常
- SignatureVerificationException: 签名不一致异常
- TokenExpiredException: 令牌过期异常
- AlgorithmMismatchException: 算法不匹配异常
- InvalidClaimException: 失效的payload异常
- JWTDecodeException
8、工具类
public class JWTUtils {
private static String sign = "dalaoshi";
public static String createToken(Map<String, String> map) {
// 1.设置超时时间
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, 7); // 7天
// 2.创建JWTbuilder
JWTCreator.Builder builder = JWT.create();
// 设置负载数据
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entrie : entries) {
builder.withClaim(entrie.getKey(), entrie.getValue());
}
// 3.设置签名,过期时间
String token = builder
.withExpiresAt(calendar.getTime()) // 设置令牌超时时间
.sign(getSignature());// 设置用户签名
// 4.返回
return token;
}
// 获取起签名
public static Algorithm getSignature() {
return Algorithm.HMAC256(sign);
}
// 校验
public static DecodedJWT require(String token) {
return JWT.require(getSignature()).build().verify(token);
}
// 获取token中的数据
public static Claim getPayload(String token, String key) {
return require(token).getClaim(key);
}
}
9、JWT整合Web
@Autowired
private IUserService userService;
@RequestMapping("/login")
public ResultEntity login(String username,String password){
ResultEntity resultEntity = userService.login(username, password);
if(ResultEntity.SUCEESS.equals(resultEntity.getStatus())){
Map<String,String> map = new HashMap<>();
map.put("id","10");
map.put("username",username);
String token = JWTUtils.createToken(map);
return ResultEntity.success(token);
}else{
return ResultEntity.error("登录失败");
}
}
@RequestMapping("/require")
public ResultEntity require(String token){
try {
DecodedJWT require = JWTUtils.require(token);
return ResultEntity.response(require);
}catch (TokenExpiredException e){
return ResultEntity.error("token过期");
}catch (SignatureVerificationException e){
return ResultEntity.error("用户签名不一致");
} catch (InvalidClaimException e){
return ResultEntity.error("payload数据有误");
}catch (Exception e){
return ResultEntity.error("校验失败");
}
}
@RequestMapping(value = "/getPayLoad")
public ResultEntity getPayLoad(String token){
DecodedJWT decodedJWT = JWTUtils.require(token);
Map<String, Claim> claims = decodedJWT.getClaims();
Map<String,String> map = new HashMap<>();
Set<Map.Entry<String, Claim>> entries = claims.entrySet();
for (Map.Entry<String, Claim> entrie:entries) {
map.put(entrie.getKey(),entrie.getValue().asString());
}
return ResultEntity.success(map);
}
10、拦截器校验
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取token
String token = request.getHeader("token");
Map<String,Object> map = new HashMap<>();
try {
// 2.校验
JWTUtils.verify(token);
return true;
}catch (TokenExpiredException e){
return ResultEntity.error("token过期");
}catch (SignatureVerificationException e){
return ResultEntity.error("用户签名不一致");
} catch (InvalidClaimException e){
return ResultEntity.error("payload数据有误");
}catch (Exception e){
return ResultEntity.error("校验失败");
}
// 3.校验失败响应数据
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
11、网关路由校验
@Component
public class SSOFilter extends ZuulFilter{
@Autowired
private ISSOService ssoService;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER-1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
StringBuffer requestURL = request.getRequestURL();
System.out.println(requestURL);
// 1.该服务是否需要验证
if("http://localhost/shop-back/user/getUserPage".equals(requestURL.toString())){
String token = request.getHeader("token");
// 2.验证服务
ResultEntity resultEntity = ssoService.require(token);
System.out.println(resultEntity);
if(!ResultEntity.SUCEESS.equals(resultEntity.getStatus())){
requestContext.setSendZuulResponse(false); // 不能往下执行了
HttpServletResponse response = requestContext.getResponse();
response.setContentType("application/json;charset=utf-8"); // 设置响应数据类型
requestContext.setResponseBody(JSON.toJSONString(ResultEntity.error("校验未通过"))); // 设置响应数据
}
}
return null;
}
}
12、解决多用户登录的问题
如果一个用户登录在多个设备登录,就会出现一个用户多个token在多个设备上同时登录。如果要解决这个问题就要判断用户操作的token是否是最新的,只有是最新的token才能认证成功。
// 伪代码
// login
public String login(String name,String password){
// 1.查询数据库认证
// 2.生成token
String token = "";
// 3.把用户最新的token放入到reids中
redisTemp.set(username,token); // username作为key,多次登录key会被覆盖
}
// 路由校验
// 1.获取用户token
// 2.根据用户名查询用户最新的token
// 3.对比两个token是否一致,如果不一致就说明用户进行了第二次登陆,就不让认证通过。
13、客户端保存/携带token
// 登录获取token,保存到本地
function login(){
var username ="admin";
var password ="123";
var param = new Object();
param.username=username;
param.password=password;
$.post("http://localhost/shop-sso/sso/login",param,function (data) {
if(data.status ="success"){
// 获取token
var token = data.data;
// 保存toke到客户端
localStorage.setItem("login-token",token);
}
},"JSON");
}
// 发送请求是把token放到请求头中保存
function sendRequest(){
$.ajax({
url: "http://localhost/shop-sso/addXxxxx",
type: "post",
dataType: 'json',
beforeSend: function (XMLHttpRequest) {
// 获取本地储存的token,添加到请求头中
XMLHttpRequest.setRequestHeader("Authorization", localStorage.getItem("login-token"));
},
success: function (result) {
}
});
}
为什么要把token放在请求头中的Authorization中?
a)保存在请求头中方便和其他参数区分
b)保存在请求头中可以解决跨域的问题,比如cookie是存在跨域的问题
c)Authorization header就是为用户认证而生的。
d)解决XSS和XSRF问题
14、抽取ajax工具类
window.utils={
ajax:function(param){
$.ajax({
url: param,
type: "post",
dataType: 'json',
data:param.data,
beforeSend: function (XMLHttpRequest) {
XMLHttpRequest.setRequestHeader("Authorization", localStorage.getItem("login-token"));
},
success: function (result) {
param.success(result);
}
});
}
}
// 调用
utils.ajax({
url:"http://localhost/shop-sso/sso/login",
data:param,
success:function(data){
if(data.status ="success"){
// 获取token
var token = data.data;
// 保存toke到客户端
localStorage.setItem("login-token",token);
}
}
})
15、a标签跳转如何传递token
token只针对api设计,和原生标签的跳转没有直接的关系。如果请求跳转可以在url后面携带token。