token的登录流程
token是用来验证的一个标识,所以他会在前端登录功能点的时候生成,然后作为一个标识带到后续的功能接口中。所以他的生命周期如上图所示,在登录成功后生成,并且会携带到后续的功能接口中去。
1、前端发送请求登录,后端验证通过,登录成功,同时生成JWT token返回给前端,这样前后端就有了一个统一的信息标识,来表示一个用户的唯一性。
2、前端在接收到这个token后,一般都是存到cookie中,在后续所有接口的调用都会在标头带上token这个参数(下图所示)。后端在除了登录接口外,都会拦截下每一个请求,查看token是不是符合规定(一般是放在缓存中来验证这个token是不是过期/不正确),这样保证信息的准确性。
JWT结构
JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。
在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串
JWTString=Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
这里使用这个网站的解析器能够快速解析token jwt解析器
JWT种类
其实JWT(JSON Web Token)指的是一种规范,这种规范允许我们使用JWT在两个组织之间传递安全可靠的信息,JWT的具体实现可以分为以下几种:
- nonsecure JWT:未经过签名,不安全的JWT。其header部分没有指定签名算法
{
"alg": "none",
"typ": "JWT"
}
- JWS:经过签名的JWT
JWS ,也就是JWT Signature,其结构就是在之前nonsecure JWT的基础上,在头部声明签名算法,并在最后添加上签名。创建签名,是保证jwt不能被他人随意篡改。我们通常使用的JWT一般都是JWS
到目前为止,jwt的签名算法有三种:
- HMAC【哈希消息验证码(对称)】:HS256/HS384/HS512
- RSASSA【RSA签名算法(非对称)】(RS256/RS384/RS512)
- ECDSA【椭圆曲线数据签名算法(非对称)】(ES256/ES384/ES512)
- JWE:payload部分经过加密的JWT
Java中使用JWT
官网推荐了6个Java使用JWT的开源库,其中比较推荐使用的是java-jwt和jjwt-root
1、java-jwt
首先是依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
然后是你可以把生成token和解析token的方法封装生成一个工具类
public class JwtUtil {
//TODO 签名密钥
private static final String SECRET = "自己设置的秘钥&%¥#";
/**
* 生成token
* @param payload token携带的信息
* @return token字符串
*/
public static String getToken(Map<String,String> payload){
// 指定token过期时间为7天
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, 7);
JWTCreator.Builder builder = JWT.create();
// 构建payload
payload.forEach((k,v) -> builder.withClaim(k,v));
// 指定过期时间和签名算法
String token = builder.withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(SECRET));
return token;
}
/**
* 解析token
* @param token token字符串
* @return 解析后的token
*/
public static DecodedJWT decode(String token){
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return decodedJWT;
}
}
然后在一个测试方法里面可以调用
Map<String,String> map=new HashMap<>();
map.put("name","xxx");
map.put("password","123");
map.put("xxx","aaa");
String token = JwtUtil.getToken(map);
System.out.println(token);
DecodedJWT decodedJWT = JwtUtil.decode(token);
System.out.println(decodedJWT);
然后你的token的过期时间可以存在一个redis里面,这样就可以比对token有没有过期了
一个简易的登录流程
1、在登录验证通过后,给用户生成一个对应的随机token(注意这个token不是指jwt,可以用uuid等算法生成),然后将这个token作为key的一部分,用户信息作为value存入Redis,并设置过期时间,这个过期时间就是登录失效的时间
2、将第1步中生成的随机token作为JWT的payload生成JWT字符串返回给前端
3、前端之后每次请求都在请求头中的Authorization字段中携带JWT字符串
4、后端定义一个拦截器,每次收到前端请求时,都先从请求头中的Authorization字段中取出JWT字符串并进行验证,验证通过后解析出payload中的随机token,然后再用这个随机token得到key,从Redis中获取用户信息,如果能获取到就说明用户已经登录
拦截器,除了login这个登录接口外,别的接口全部拦截住
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 拦截器
*
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor())
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/user/login/**"); //排除登录接口
}
@Bean
public JwtInterceptor jwtInterceptor() {
return new JwtInterceptor();
}
}
import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class JwtInterceptor implements HandlerInterceptor {
private final static String signature = "user";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if (!(handler instanceof HandlerMethod)) {
return true;
}
// 验证payload部分
if (StrUtil.isEmpty(token)) {
throw new RuntimeException("无token,请重新登录");
}
Claim name;
name = Jwtutil.decode(token).getClaim("username");
//这可以去数据库查你的user是不是name对得到,我这里省略
if ( name == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
try {
Jwtutil.decode(token);
} catch (JWTVerificationException e) {
e.printStackTrace();
System.out.println("token错误或已过期,请重新登录");
}
return true;
}
}
然后是Controller来验证是不是拦截到了
首先第一个接口是login,登录,这样会生成一个token,接着调用get方法,在postman里面填入这个生成的token,看看是不是会正常执行、不填写token又会怎么样。
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserImpl user;
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String sendTempMsg(){
String token = user.getToken();
return token;
}
//测试拦截token
@RequestMapping(value = "/get", method = RequestMethod.POST)
public String getTest(HttpServletRequest request){
String token = request.getHeader("token");
return token;
}
}
首先是生成这个token
然后用这个token去调用get方法
填入token,正常通过