JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON
的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON
对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC
算法或者是RSA
的公私秘钥对进行签名。
JWT请求流程
1. 用户使用账号和面发出post请求;
2. 服务器使用私钥创建一个jwt;
3. 服务器返回这个jwt给浏览器;
4. 浏览器将该jwt串在请求头中像服务器发送请求;
5. 服务器验证该jwt;
6. 返回响应的资源给浏览器。
JWT组成
JWT格式的输出是以.分隔的三段Base64编码,与SAML等基于XML的标准相比,JWT在HTTP和HTML环境中更容易传递。(形式:xxxxx.yyy.zzz):
1、Header:头部
2、Payload:负载
3、Signature:签名
Header
在header中通常包含了两部分,Token类型以及采用加密的算法
Payload
Token的第二部分是负载,它包含了Claim,Claim是一些实体(一般都是用户)的状态和额外的数据组成。
Signature
创建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。
引入JWT
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
导入依赖Web、JPA、MySQL等配置好application.yml。
数据库配置
1.用户基本信息表
2.Token基本信息表
实体类
1.用户实体
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
/**
* @Author:hk
* @Date: 2019/11/11 16:32
* @Description:
*/
@Entity
@Data
@Table(name = "user_info",schema = "jwt")
public class UserEntity implements Serializable {
@Id
@Column(name = "user_id")
private String userId;
@Column(name = "secret")
private String secret;
2.Token实体
/**
* @Author:hk
* @Date: 2019/11/11 16:31
* @Description:
*/
@Entity
@Data
@Table(name="token_info",schema = "jwt")
public class TokenInfoEntity implements Serializable {
@Id
@GeneratedValue
@Column(name = "token_id")
private Long id;
@Column(name = "user_id")
private String userId;
@Column(name = "token")
private String token;
@Column(name = "build_time")
private String buildTime;
}
3.创建用户JPA与TokenJPA
生成Token
创建TokenController
import com.example.jwtdemo.entity.TokenInfoEntity;
import com.example.jwtdemo.entity.UserEntity;
import com.example.jwtdemo.repository.TokenRepository;
import com.example.jwtdemo.repository.UserRepository;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* @Author:hk
* @Date: 2019/11/11 16:31
* @Description:
*/
@RestController
@RequestMapping(value = "/jwt")
public class TokenController {
@Autowired
private TokenRepository tokenRepository;
@Autowired
private UserRepository userRepository;
/**
* 获取或更新token
*/
@RequestMapping(value = "/token")
public String token(@RequestParam String userId,@RequestParam String secret){
String token=null;
if(userId==null || userId.trim().equals("")){
return "缺少userId";
}else if(secret==null || secret.trim().equals("")){
return "缺少secret";
}else{
UserEntity userEntity=userRepository.findById(userId).get();
if(userEntity.getSecret()==null || secret.trim().equals("")){
return "secret 不存在";
}else{
TokenInfoEntity tokenInfoEntity=tokenRepository.findByUserId(userId);
if(tokenInfoEntity==null){
token=createToken(userId);//获取新的token
TokenInfoEntity tokenInfo=new TokenInfoEntity();
tokenInfo.setUserId(userId);
tokenInfo.setToken(token);
tokenInfo.setBuildTime(String.valueOf(System.currentTimeMillis()));
tokenRepository.save(tokenInfo);
}else{
//判断数据库中token是否过期
long buildTime=Long.valueOf(tokenInfoEntity.getBuildTime());
long currentTime = System.currentTimeMillis();
long second = TimeUnit.MILLISECONDS.toSeconds(currentTime - buildTime);
if (second > 0 && second < 7200) {
token = new String(tokenInfoEntity.getToken());
}else{
token=createToken(userId);//获取新的token
tokenInfoEntity.setToken(token);
tokenInfoEntity.setBuildTime(String.valueOf(System.currentTimeMillis()));
tokenRepository.save(tokenInfoEntity);
}
}
}
}
return token;
}
/**
* 创建新的token
*/
private String createToken(String userId){
//获取当前时间
Date now = new Date(System.currentTimeMillis());
//过期时间
Date expiration = new Date(now.getTime() + 7200000);
return Jwts
.builder()
.setSubject(userId)
.setIssuedAt(now)
.setIssuer("Online YAuth Builder")
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS256, "Token1.0.0")
.compact();
}
}
生成Token方法的内容大致是,检查userId以及secret-->检查是否存在该userId的对应Token-->根据存在与否、过期与否执行更新或者写入操作-->返回用户请求。
在createNewToken方法中是JWT生成Token的方法,我们默认了过期时间为7200秒,上面是毫秒单位,我们生成token需要指定subject也就是我们的用户对象,设置过期时间、生成时间、还有签名生成规则等。token生成方法已经编写完成,下面我们需要在除了获取token的路径排除在外拦截所有的路径,验证路径是否存在header包含token,并且验证token是否正确,jwt会自动给我们验证过期,如果过期会抛出对应的异常。
Token验证拦截器
我们在拦截器中需要验证头信息,Token的值是否存在,Subject用户是否存在等。
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @Author:hk
* @Date: 2019/11/11 16:35
* @Description:
*/
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
//排除token路径
if(request.getRequestURI().equals("/token"))
{
return true;
}
String header=request.getHeader("Token");
try {
//如果没有header信息
if (header == null || header.trim().equals("")) {
throw new SignatureException("not found Token");
}
//获取jwt实体对象接口实例
final Claims claims = Jwts.parser().setSigningKey("Token1.0.0")
.parseClaimsJws(header).getBody();
//从数据库中获取token
TokenInfoEntity tokenInfoEntity = getDAO(TokenRepository.class,request).findByUserId(claims.getSubject());
//数据库中的token值
String token = new String(tokenInfoEntity.getToken());
//不存在,提示获取token
if(token == null || token.trim().equals("")) {
throw new SignatureException("not found token info, please get token agin.");
}
//判断内存中的token是否与客户端传来的一致
if(!token.equals(header))
{
throw new SignatureException("not found token info, please get token agin");
}
}catch (SignatureException | ExpiredJwtException e){
PrintWriter writer = response.getWriter();
writer.write("need refresh token");
writer.close();
return false;
} catch (final Exception e){
PrintWriter writer = response.getWriter();
writer.write(e.getMessage());
writer.close();
return false;
}
return true;
}
/**
* 根据传入的类型获取spring管理的对应dao
* @param clazz 类型
* @param request 请求对象
* @param <T>
* @return
*/
private <T> T getDAO(Class<T> clazz,HttpServletRequest request)
{
BeanFactory factory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
return factory.getBean(clazz);
}
}
Claims就是我们生成Token是的对象,我们把传递的头信息token通过JWT可以逆转成Claims对象,并且通过getSubject可以获取到我们用户的userId。
配置拦截器
我们创建一个JWTConfiguration配置类,将我们创建的拦截器添加到SpringBoot项目中
/**
* @Author:hk
* @Date: 2019/11/11 16:34
* @Description:
*/
@Configuration
public class JwtConfiguraiton extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtInterceptor()).addPathPatterns("/api/**");
}
}
我们配置JWT拦截器只拦截/api/下的所有路径。
运行测试
在启动项目之前我们先来配置一个TestController,并且提供一个访问内容的API接口.
/**
* @Author:hk
* @Date: 2019/11/11 16:28
* @Description:
*/
@RestController
@RequestMapping(value = "/api")
public class TestController {
@RequestMapping(value = "/test")
public String Test(){
return "Test Success!";
}
}
访问地址:127.0.0.1:8080/api/test,界面输出内容如下图
我们在拦截器中配置的无论是不存在token还是token需要刷新都是返回"need refresh token"错误信息
下面我们通过/token地址获取jwt生成的token值
使用获取到的Token通过Postman工具来访问我们的/api/test方法