由于HTTP协议是无状态的,这意味着如果我们想判定一个接口是否被认证后访问,就需要借助cookie或者session会话机制进行判定,但是由于现在的系统架构大部分都不止一台服务器,此时又要借助数据库或者全局缓存 做存储,这种方案显然受限太多。
那么我们可不可以让认证令牌的发布者自己去识别这个令牌是不是我曾经发布的令牌呢(JWT核心思想),这是JWT最大的优点也是最大的缺点,优点是简单快捷、不需要依赖任何第三方操作就能实现身份认证,缺点就是对于任何拥有用户发布令牌的请求都会认证通过。
在介绍JWT之前,我们先来回顾一下利用token进行用户身份验证的流程:
这种基于token的认证方式相比传统的session认证方式更节约服务器资源,并且对移动端和分布式更加友好。其优点如下:
支持跨域访问:cookie是无法跨域的,而token由于没有用到cookie(前提是将token放到请求头中),所以跨域后不会存在信息丢失问题
无状态:token机制在服务端不需要存储session信息,因为token自身包含了所有登录用户的信息,所以可以减轻服务端压力
适用CDN:可以通过内容分发网络请求服务端的所有资料
适用移动端:当客户端是非浏览器平台时,cookie是不被支持的,此时采用token认证方式会简单很多
无需考虑CSRF:由于不再依赖cookie,所以采用token认证方式不会发生CSRF,所以也就无需考虑CSRF的防御
而JWT就是上述流程当中token的一种具体实现方式,JSON Web Token(缩写 JWT)JWT官网
通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。JWT的认证流程如下:
正常的JWT数据结构应该如下
它是一个很长的字符串,中间用点(.)分隔成三个部分
JWT的三个部分依次: Header - 头部 、Payload - 负载 、Signature(签名)
即:Header.Payload.Signature
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);
typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段
{
"sub": "1234567890",
"name": "John Doe",
"age": "19"
}
注意:JWT默认是明文展示,任何人都可以读取到,所以此处不要放私密信息
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.5.0version>
dependency>
@Slf4j
public class JWTUtil {
private static final String SECRET = "!Q@W#E$R^Y&U";
//token签发者
private static final String ISSUSRE = "HZSTYGZPT";
//token过期时间
public static final Long EXPIRE_DATE = 1000*60L;
/**
* 生成token
* @param userVo
* @return
*/
public static String Token(UserVo userVo){
//当前时间
Date now = new Date();
//创建过期时间
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7); //7天过期
//1. header
Algorithm algorithm = Algorithm.HMAC256(SECRET);
String token = JWT.create()
.withIssuer(ISSUSRE)
.withIssuedAt(now)
.withExpiresAt(new Date(now.getTime() + EXPIRE_DATE))
.withClaim("username", userVo.getUsername())
.withClaim("userId", userVo.getUserId())
.sign(algorithm);
return token;
}
/**
* 生成token
* @param map
* @return
*/
public static String createToken(Map<String,String> map){
//创建过期时间
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7); //7天过期
//创建builder对象
JWTCreator.Builder builder = JWT.create();
//遍历map
map.forEach((k,v)->{
builder.withClaim(k,v);
});
String token = builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SECRET));
return token;
}
/**
* 验证token
* 验证过程中如果有异常,则抛出;
* 如果没有,则返回 DecodedJWT 对象来获取用户信息;
* @param token
*/
public static JsonResult verifyToken(String token, String username){
Algorithm algorithm = Algorithm.HMAC256(SECRET);
try {
JWTVerifier jwtVerifier = JWT.require(algorithm).withClaim("username", username).build();
jwtVerifier.verify(token);
return new JsonResult();
}catch (SignatureVerificationException e) {
//验证的签名不一致
throw new SignatureVerificationException(algorithm);
}catch (TokenExpiredException e){
throw new TokenExpiredException("token is alreadey expired");
}catch (AlgorithmMismatchException e){
throw new AlgorithmMismatchException("签名算法不匹配");
}catch (InvalidClaimException e){
throw new InvalidClaimException("校验的claims内容不匹配");
}catch (Exception e){
e.printStackTrace();
}
return new JsonResult().error("用户和jwt-token校验失败");
}
/**
* 验证token
* 验证过程中如果有异常,则抛出;
* 如果没有,则返回 DecodedJWT 对象来获取用户信息;
* @param token
*/
public static DecodedJWT verify(String token){
return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
}
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
/**
* 注册拦截器
* @return
*/
@Bean
public JwtInterceptor getJwtInterceptor(){
return new JwtInterceptor();
}
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getJwtInterceptor())
.addPathPatterns("/**") //其他接口保护 token验证
.excludePathPatterns("/user/**") //所有用户接口都放行
.excludePathPatterns("/favicon.ico");
}
/**
* 解决 No mapping for GET /favicon.ico 访问静态资源图标
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
//addResourceHandler("/qy/**") 所有/qy/开头的请求 都会去后面配置的路径下查找资源
registry.addResourceHandler("/favicon.ico").addResourceLocations("classpath:/static/");
//图片真实存放的路径
//registry.addResourceHandler("/upload/avatar/**").addResourceLocations("file:"+System.getProperty("user.dir")+"/upload/avatar/");
super.addResourceHandlers(registry);
}
}
或者
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
@Autowired
private TokenHandler tokenHandler;
@Override
public void addInterceptors(InterceptorRegistry registry) {
List<String> excludePath = new ArrayList<>();
String checkLogin = "/pushlogin/checkIsCanLogin";
String login = "/pushlogin/login";
String getVerifyCode = "/common/send";
String verfifyMethod = "/common/validationCode";
excludePath.add(checkLogin);
excludePath.add(login);
excludePath.add(getVerifyCode);
excludePath.add(verfifyMethod);
registry.addInterceptor(tokenHandler).excludePathPatterns(excludePath);
}
}
public class JwtInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(JwtInterceptor.class);
private static final String AUTH = "authorization";
private static final String AUTH_USER_NAME = "user-name";
@Value("${spring.profiles.active}")
private String profiles;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setContentType("application/json;charset=utf-8");
//如果是开发环境,则不需要token。直接通过
if(StrUtil.isNotEmpty(profiles) && profiles.equals("dev")){
return true;
}
//如果接口或者类上有@IgnoreToken注解,意思该接口不需要token就能访问,需要放行
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//先从类上获取该注解,判断类上是否加了IgnoreToken ,代表不需要token,直接放行
IgnoreToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreToken.class);
if(annotation == null){
//再从方法上获取该注解
if(method.isAnnotationPresent(IgnoreToken.class)){
//annotation = handlerMethod.getMethodAnnotation(IgnoreToken.class);
annotation = method.getAnnotation(IgnoreToken.class);
log.info("请求方法 {} 上有注解 {} ",method.getName(),annotation);
}
}
if(annotation != null){
return true;
}
String token = getParams(request, AUTH);
String username = getParams(request, AUTH_USER_NAME);
if (StrUtil.isEmpty(token)) {
throw new ValidationException("Authorization不允许为空,请重新登录");
}
if (StrUtil.isEmpty(username)) {
throw new ValidationException("用户名不允许为空,请重新登录");
}
JsonResult jsonResult = JWTUtil.verifyToken(token, username);
if (jsonResult.getCode() != 200) {
log.error("token valid error!");
return false;
}
return true;
}
或者
@Component
@Slf4j
public class TokenHandler implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authentication");
if (token != null){
boolean result = TokenUtil.verify(token);
if(result){
log.info("通过拦截器");
return true;
}
}
log.info("认证失败");
return false;
}
}
@PostMapping("/user/jwtLogin")
public JsonResult jwtLogin(String username, String password,HttpServletResponse response) {
//1. 判断用户名密码是否为空
if(StrUtil.isBlank(username) || StrUtil.isBlank(password)) {
throw new IllegalArgumentException("用户名或者密码为空");
}
MUser user = userService.getUserByUsername(username);
if(user ==null) {
throw new ValidationException("用户名或者密码错误");
}
//对密码进行加密
password = SecureUtil.md5(password);
if(!password.equalsIgnoreCase(user.getPassword())){
throw new ValidationException("用户名或者密码错误");
}
UserVo userVo = new UserVo();
userVo.setUserId(user.getId());
userVo.setUsername(user.getUsername());
//生成token
String token = JWTUtil.Token(userVo);
//写入token
userVo.setToken(token);
//刷新token的key
userVo.setRefreshToken(UUID.randomUUID().toString());
//存储用户信息到redis中
redisTemplate.opsForValue().set(userVo.getRefreshToken(),userVo,JWTUtil.EXPIRE_DATE, TimeUnit.SECONDS);
response.setHeader("authorization",token);
//response.setHeader("user-name",username);
return new JsonResult<>(userVo);
}
@GetMapping("/user/refresh")
public JsonResult refreshToken(String refreshToken){
//根据 key 为 refreshToken,获取当前登录的用户信息是否还在有效期内
UserVo userVo = (UserVo) redisTemplate.opsForValue().get(refreshToken);
if(userVo ==null){
//告诉用户 token已经失效,需要重新登录
return new JsonResult().error("用户信息已不存在,需重新登录");
}
//重新生成token
String jwt = JWTUtil.Token(userVo);
userVo.setToken(jwt);
userVo.setRefreshToken(UUID.randomUUID().toString());
//删除之前的用户信息
redisTemplate.delete(refreshToken);
//将刷新后的值存入redis
redisTemplate.opsForValue().set(userVo.getRefreshToken(),userVo,JWTUtil.EXPIRE_DATE, TimeUnit.SECONDS);
return new JsonResult<>(userVo);
}
String token = Jwts.builder().setHeaderParam("typ", "JWT")
.setSubject("11221")
.setClaims(map)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS256,SIGN_KEY)
.compact();
String token = Jwts.builder().setHeaderParam("typ", "JWT")
.setClaims(map)
.setSubject("11221")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS256,SIGN_KEY)
.compact();
测试结果:
(2)不使用 builder.setClaims(map) 封装 。
@Test
void createJWT(){
Map<String,Object> map = new HashMap<>();
map.put("userId","1111");
map.put("name","张某");
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime()+604800*1000);
System.out.println("=======第一种写法=======");
String token = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setClaims(map)
.setSubject("11221213131")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS256,SIGN_KEY)
.compact();
System.out.println(token);
System.out.println("=======第二种写法=======");
JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("typ", "JWT")
.setSubject("11221")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS256, SIGN_KEY);
//第一种
map.forEach((k,v)->{
jwtBuilder.claim(k,v);
});
//第二种
Set<String> keys = map.keySet();
for (String key : keys){
jwtBuilder.claim(key,map.get(key));
}
//第三种
Set<Map.Entry<String, Object>> entries = map.entrySet();
for (Map.Entry<String, Object> entry : entries){
jwtBuilder.claim(entry.getKey(),entry.getValue());
}
String token1 = jwtBuilder.compact();
System.out.println(token1);