在日常的开发中,常见的认证机制有几种,其中使用token是目前前后端分离项目最受欢迎的一种认证手段。token字面意思为令牌(一串字符串),目的就是客户端在访问服务器时要带上这个令牌,服务器然后去根据这个令牌进行认证是否允许你去访问。这个过程中,服务器时完全没有存储任何关于认证的信息的,因此使用token的认证方式也被称之为无状态的认证方式,而生成token又有很多种方式,比如服务器直接生成一个字符串(UUID)给客户端,并且存储在内存或者redis中,下次客户端访问时必须要带上这个token,但是既然token的生成方式有这么多种,使用的方法又是不同的,那么这时候jwt就产生了。
1.什么是JWT?
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
2.JWT组成
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名
头部(header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
载荷(playload)
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分。
(1)标准中注册的声明(建议但不强制使用)
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
(2)公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
(3)私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。这个指的就是自定义的claim。比如前面那个结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
签证(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
然后这三部分分别都用.连接成一个完整的字符串,构成最终的jwt。
3.在Java中使用JWT(JJWT)
3.1什么是JJWT?
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(ApacheLicense,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
3.2JJWT的使用,下面代码演示工程使用的是springboot
首先加入JJWT的依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
JJWT工具类:
@ConfigurationProperties("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();
}
}
在需要认证之后才能访问的模块中定义拦截器:
@Component
public class JwtInterceptor extends HandlerInterceptorAdapter {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//取出http header
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
try {
Claims claims = jwtUtil.parseJWT(token);
if (claims!=null){
String role = (String) claims.get("role");
if ("admin".equals(role)){
request.setAttribute("admin_claim",token);
}
if ("user".equals(role)){
request.setAttribute("user_claim",token);
}
}
}catch (Exception ex){
ex.printStackTrace();
throw new RuntimeException("令牌错误!");
}
}
return true;
}
}
在拦截器中,前台登录之后会把token放在请求头中,格式为(Bearer +token),把请求头的token拿到之后 通过上面JJWT的工具类进行解析,当然我们上面说过,在JWT的载荷中可以放自定义的认证信息,我们这里就放role作为角色的校验条件,当解析到的role是admin的时候就把token设置到以admin_claim为key的request域中,当role是user的时候就把token设置到user_claim为key的request域中,然后统一放行,拦截器return ture。
public Result addProblem(Problem problem) {
String token = (String) request.getAttribute("user_claim");
if (token == null) {
return new Result(false, StatusCode.ACCESSERROR, "权限不足");
}
problem.setId(String.valueOf(idWorker.nextId()));
problemRepository.save(problem);
return new Result(true, StatusCode.OK, "添加问题成功");
}
请求经过拦截器来到service层,在service层中取出当前request域中token,比如上面的添加问题的接口中需要有user角色的权限,就取出user_claim为key的value判断是否为空,为空的话就说明该请求认证不通过,返回拒绝访问。