JWT是json web token缩写。它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证。
一个JWT其实就是一个字符串,由三部分组成:头部(header)、载荷(payload)、签名(signature),以“.”拼接
头部用于描述关于该JWT的最基本的信息,header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。
例如:{"typ":"JWT ","alg":"HS256"}
头部指明了签名算法是hs256,用base64对这个json进行编码后就得到了JWT的第一部分
载荷是存放有效信息的地方,包含了声明(要求),声明有三种类型:
(1)标准中注册的声明:(建议但是不强制使用)
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
(2)公共声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
(3)私有声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
例如:{"sub":"1234567890","name":"John Doe","admin":true}
签名是一个字符串,是由" header(Base64编码后)+‘.’+payload(Base64编码后) " 拼接的字符串通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(ApacheLicense,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
public class CreateJwt {
public static void main(String[] args) {
JwtBuilder builder =
Jwts.builder() // 生成jwt,下面是载荷的内容
.setId("111") // 当前登录用户的id
.setSubject("皮卡丘") // 用户名
.setIssuedAt(new Date()) // 签发时间
.signWith(SignatureAlgorithm.HS256, "pikapika"); // 加密方式是hs256,盐是pikapika
// 输出token
System.out.println(builder.compact());
}
}
执行完成后输出的结果如下:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMTEiLCJzdWIiOiLnmq7ljaHkuJgiLCJpYXQiOjE1NDY0ODU4ODd9.v64lCobI5odEHY2Vl4h4kQh1cThzsU8MAs3gGM3nrms
public class ParseJwt {
public static void main(String[] args) {
String token =
"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMTEiLCJzdWIiOiLnmq7ljaHkuJgiLCJpYXQiOjE1NDY0ODU4ODd9."
+ "v64lCobI5odEHY2Vl4h4kQh1cThzsU8MAs3gGM3nrms";
Claims claims = Jwts.parser().setSigningKey("pikapika").parseClaimsJws(token).getBody();
// 获取具体的参数
System.out.println("用户id:" + claims.getId());
System.out.println("登录时间:" + claims.getIssuedAt());
System.out.println(
"登录时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(claims.getIssuedAt()));
System.out.println("用户名:" + claims.getSubject());
}
}
输出结果如下:
把盐或者token写错,会出现相同的报错,所以在解析token的时候就会验证token
取值
记住修改自定义添加的属性后,要修改token值
没过期前运行输出的结果:
时间超过过期时间再运行就报错
Exception in thread "main" io.jsonwebtoken.ExpiredJwtException: JWT expired at 2019-01-03T14:20:54+0800. Current time: 2019-01-03T14:21:12+0800
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:365)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:458)
at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:518)
at com.tensquare.jjwt.ParseJwt.main(ParseJwt.java:26)
在项目中加入依赖
package util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* Created by Administrator on 2018/4/11.
*/
@ConfigurationProperties("jwt.config") //要从配置文件jwt.config获取属性的值
public class JwtUtil {
private String key ;//盐
private long ttl ;//过期时间
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getTtl() {
return ttl;
}
public void setTtl(long ttl) {
this.ttl = ttl;
}
/**
* 生成JWT
*
* @param id
* @param subject
* @return
*/
public String createJWT(String id, String subject, String roles) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder().setId(id)
.setSubject(subject)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, key).claim("roles", roles);
if (ttl > 0) {
builder.setExpiration( new Date( nowMillis + ttl));
}
return builder.compact();
}
/**
* 解析JWT
* @param jwtStr
* @return
*/
public Claims parseJWT(String jwtStr){
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody();
}
}
在要用到jwt的工程的配置文件里面添加
jwt:
config:
key: tensquare
ttl: 3600000 #过期时间一小时
登录的方法:
登录成功后要添加生成token令牌的方法
@PostMapping("/login")
public Result login(@RequestBody Map map) {
String loginname = map.get("loginname");
String password = map.get("password");
Admin admin = adminService.login(loginname, password);
if (admin == null) {
return new Result(false, StatusCode.LOGINERROR, "用户名或密码错误");
}
//如果登录成功就生成token令牌
String jwt = jwtUtil.createJWT(admin.getId(), admin.getLoginname(), "admin");
Map rmap=new HashMap<>();//前端需要什么信息给它返回什么信息
rmap.put("token",jwt);
rmap.put("role","admin");
rmap.put("loginname",admin.getLoginname());
return new Result(true, StatusCode.OK, "登录成功",rmap);
}
//假设一个业务场景,只有当前用户是admin角色时才能删除user
首先要注入两个类
另外要与前端商定好,token怎么携带,这里假设的是将其放在请求头设置的 Authorization属性里面,内容为Bearer+空格+token
删除用户user
public void deleteById(String id) {
String authHeader = request.getHeader("Authorization");
//首先需要判断该请求头是否存在
if(authHeader==null|| "".equals(authHeader)){
throw new RuntimeException("权限不足");
}
if(!authHeader.startsWith("Bearer ")){
throw new RuntimeException("权限不足");
}
//获取token,然后解析
String token = authHeader.substring(7);
try {
Claims claims = jwtUtil.parseJWT(token);
String role = (String) claims.get("roles");
if(role==null || !role.equals("admin")){
throw new RuntimeException("权限不足");
}
} catch (Exception e) {
throw new RuntimeException("权限不足");
}
userDao.deleteById(id);
}
只有按照要求的格式在header里面填写参数时,才能删除成功。
如果每一个方法都去验证该用户是否登录是否有权限,代码就会很冗余,不利于维护,所以我们用拦截器来实现。
下面是自定义拦截器类,我们要在请求处理之前进行拦截
package com.tensquare.user;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import util.JwtUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** ClassName: JwtInterceptor Description: Author:yqwang103 CreateDime:2019/1/3 16:45 */
// 自定义拦截器实现HandlerInterceptor接口或者继承HandlerInterceptorAdapter均可
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired private JwtUtil jwtUtil;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("经过了拦截器。。。");
// 不管怎样结果都是放行,具体能不能操作还是要在业务中去判断 ,现在做的只是解析token
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
// 解析token
try {
Claims claims = jwtUtil.parseJWT(token);
if (claims != null) {
String role = (String) claims.get("roles");
if ("admin".equals(role)) {
request.setAttribute("admin_claims", claims); // 此时是管理员
}
if ("user".equals(role)) {
request.setAttribute("user_claims", claims); // 此时是普通用户
}
}
} catch (Exception e) {
throw new RuntimeException("令牌有误");
}
}
return true; // 返回true就是放行,返回false就是拦截
}
}
以前用spring的时候要在配置文件里面设置拦截路径,因为小鱼这个项目使用的springboot,所以要书写自定义配置类
@Configuration // 表明它是一个配置类
public class ApplicationConfig extends WebMvcConfigurationSupport {
@Autowired private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry
.addInterceptor(jwtInterceptor)
.addPathPatterns("/**") //所有的路径都拦截
.excludePathPatterns("/login/**");//除了登录的路径不拦截
}
}
对上面的删除代码进行修改:
没有header
有header并且是正确的header