去中心化的JWT( Json Web Token)

通常情况下, 将api直接暴露出来是非常危险的. 每一个api呼叫, 用户都应该附上额外的信息, 以供我们认证和授权. 而JWT是一种既能满足这样需求, 而又简单安全便捷的方法. 前端login获取JWT之后, 只需在每一次HTTP呼叫的时候添加上JWT作为HTTP Header即可。
实际的业务流程是:

  • 需要调用者先使用用户名密码去签定身份
  • 鉴定成功,服务器返回一个 Token
  • 调用者之后再调用其他 API 时就在 HTTP Authorization Header 中带着这个 Token
  • 生成的token中包含调用者的身份权限认证信息

#1. 什么是JWT
了解JWT的同学可以跳过这一部分

废话少说, 我们先看看什么是JWT. JSON Web Token其实就是一个包含认证数据的JSON, 大概长这样子package basic;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtUtil {
static final long EXPIRATION_TIME = 3600_000; // 1 hour
static final String SECRET = "ThisIsASecret";
static final String TOKEN_PREFIX = "Bearer";
static final String HEADER_STRING = "Authorization";

public static String generateToken(String username) {
HashMap<String, Object> map = new HashMap<>();
//you can put any data in the map
map.put("username", username);
String jwt = Jwts.builder()
.setClaims(map)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
return jwt;
}

public static String validateToken(String token) {
if (token != null) {
// parse the token.
Map<String,Object> body = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody();
String username = (String) (body.get("username"));
if(username == null || username.isEmpty())
throw new TokenValidationException("Wrong token without username");
else
return username;
}else{
throw new TokenValidationException("Missing token");
}
}

static class TokenValidationException extends RuntimeException {
public TokenValidationException(String msg) {
super(msg);
}
}
}
分三个部分,
第一部分`{"alg":"HS512"}`是签名算法
第二部分 `{"exp":1495176357,"username":"admin"}`是一些数据(你想放什么都可以), 这里有过期日期和用户
第三部分`    ')4'76-DM(H6fJ::$ca4~tI2%Xd-$nL(l`非常重要,是签名Signiture, 服务器会验证这个以防伪造. 因为JWT其实是明文传送, 任何人都能篡改里面的内容. 服务端通过验证签名, 而验证这个JWT是自己生成的.

原理也不是很复杂, 我用一行代码就能表示出来
首先我们将JWT第一第二部分的内容, 加上你的秘钥(key), 然后用某个算法(比如hash算法)求一下, 求得的内容就是你的签名. 验证的时候只需要验证你用JWT算出来的值是否等于JWT里面的签名.
因为别人没有你的key, 所以也就没法伪造签名.
######简单粗暴一行代码解释什么是签名:
```
 int signiture = ("{alg:HS512}{exp:1495176357,username:admin}" + key).hashCode();
```
######完整的JWT:
```
{"alg":"HS512"}{"exp":1495176357,"username":"admin"}    ')4'76-DM(H6fJ::$ca4~tI2%Xd-$nL(l
```
通常我们都是把JWT用base64编码之后放在http的header里面, 并且每一次呼叫api都附上这个JWT, 并且服务器每次也验证JWT是否过期
######通常我们用到的JWT:
```
Base64编码后
Bearer eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE0OTUxNzYzNTcsInVzZXJuYW1lIjoiYWRtaW4ifQ.mQtCfLKfI0J7c3HTYt7kRN4AcoixiUSDaZv2ZKOjq2JMZjBhf1DmE0Fn6PdEkyJZhYZJTMLaIPwyR-uu6BMKGw
```

***
#2. 三个class实现JWT
整个demo一共有三个class
Application.java JwtAuthenticationFilter.java 和 JwtUtil.java
####首先我们看一看Application.java

第一步创建一个hello world api
```
    @GetMapping("/protected")
    public @ResponseBody Object hellWorld() {
        return "Hello World! This is a protected api";
    }
```
第二步创建一个 login的api, 我们会验证用户的密码, 如果正确, 那么我们会生成jwt, 然后以Header的形式返回给用户. 这时前端拿到的这个jwt就类似于拿到了一个临时的密码, 之后所有的HTTP RESTful api请求都附上这个"临时密码"即可.
```
    @PostMapping("/login")
    public void login(HttpServletResponse response,
                      @RequestBody final AccountCredentials credentials) throws IOException {
        if(validCredentials(credentials)) {
            String jwt = JwtUtil.generateToken(credentials.username);
            response.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + jwt);
        }else
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Wrong credentials");
    }
```
最后我们再注册一个Bean, 这个JwtAuthenticationFilter继承了OncePerRequestFilter, 任何请求都会经过我们的JwtAuthenticationFilter, 我们会在filter里面验证JWT的令牌(token).
而入参`"/*.html", "/", "/login"`是这里的排除条件, 当用户访问我们的index.html或者/login的时候并不需要令牌.
完整版[Application.java](https://github.com/ZhongjunTian/spring-boot-jwt-demo/blob/master/basic/src/main/java/basic/Application.java)
```
@Bean
    public FilterRegistrationBean jwtFilter() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter("/*.html", "/", "/login");
        registrationBean.setFilter(filter);
        return registrationBean;
    }
```
####接着我们看一下JwtAuthenticationFilter.java
这里我们继承了OncePerRequestFilter, 保证了用户请求任何资源都会运行这个doFilterInternal. 这里我们会从HTTP Header里面截取JWT, 并且验证JWT的签名和过期时间, 如果有问题, 我们会返回HTTP 401错误.
PS: 源代码还有一个protectUrlPattern变量, 只有符合这个模板的URL才会被保护.
完整版[JwtAuthenticationFilter.java](https://github.com/ZhongjunTian/spring-boot-jwt-demo/blob/master/basic/src/main/java/basic/JwtAuthenticationFilter.java)
```
public class JwtAuthenticationFilter extends OncePerRequestFilter {
     //......一些不重要的代码......
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
       try {
           if(pathMatcher.match(protectUrlPattern, request.getServletPath())) {
               String token = request.getHeader(HEADER_STRING);
               JwtUtil.validateToken(token);
           }
       } catch (Exception e) {
           response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
           return;
       }
       filterChain.doFilter(request, response);
    }
    //......一些不重要的代码......
}
```
####最后我们看一下JwtUtil.java
这里就两个函数, 第一个输入是用户名, 返回一个有效期3600秒的JWT
` public static String generateToken(String username) `
第二个函数是验证JWT是否有效, 如果JWT有效则返回用户名, 某种 throws Exception
` public static String validateToken(String token) `


完整版[JwtUtil.java]

package basic;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtUtil {
    static final long EXPIRATION_TIME = 3600_000; // 1 hour
    static final String SECRET = "ThisIsASecret";
    static final String TOKEN_PREFIX = "Bearer";
    static final String HEADER_STRING = "Authorization";

    public static String generateToken(String username) {
        HashMap<String, Object> map = new HashMap<>();
        //you can put any data in the map
        map.put("username", username);
        String jwt = Jwts.builder()
                .setClaims(map)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
        return jwt;
    }

    public static String validateToken(String token) {
        if (token != null) {
            // parse the token.
            Map<String,Object> body = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody();
            String username = (String) (body.get("username"));
            if(username == null || username.isEmpty())
                throw new TokenValidationException("Wrong token without username");
            else
                return username;
        }else{
            throw new TokenValidationException("Missing token");
        }
    }

    static class TokenValidationException extends RuntimeException {
        public TokenValidationException(String msg) {
            super(msg);
        }
    }
}


  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值