JWT入门实践 ——抛开shiro框架做一个登录认证

JTW官网:https://jwt.io/introduction/

目录

一、JWT基本介绍

1、什么是JWT

2、什么时候使用 JWT 

3、JWT的结构

4、JWT 的特点

二、代码示例

 三、源码分析


一、JWT基本介绍

1、什么是JWT

JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,作为 JSON 对象在各方之间安全的传递信息。这个信息可以通过数字签名进行验证并信任。JWTs 可以使用密钥(结合 HMAC 算法)或者 使用 RSA 、 ECDSA 加密的公钥私钥对进行签名。

2、什么时候使用 JWT 

  • 授权:这是 JWT 最普遍的使用场景。当用户登录之后,每次请求中都包含 JWT ,服务端允许用户访问那些只有携带 token 才能访问的路由、服务、资源。目前在单点登录中广泛使用到 JWT ,因为它体积小,且能够在不同域名之间使用。
  • 信息交换: JWT 是一种能够在各方之间安全传输信息的好方式。因为 JWTs 能够签名,比如使用公钥私钥对,你能够确定发送者的身份。另外,签名是使用 Header 和 Payload 通过特定算法计算而来,所以你也可以验证内容是否被篡改。

3、JWT的结构

JWT 包含三部分,之间以点(.)连接(头部.负载.签名)

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

demo:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1ODk3NzI5OTgsInVzZXJuYW1lIjoiYWFhIn0.X_wNgENIpih0SAk2pirwmusp0i0dgEXrfmDVhh__L74

Header部分 是一个 JSON 对象,典型的header包含两部分:

  • alg:使用的签名算法,比如 HMAC SHA256 或 RSA
  • typ:token的类型,比如 JWT
{
  "alg": "HS256",
  "typ": "JWT"
}

用 Base64Url 将这个 JSON 对象编码后,作为 JWT 的第一部分

Payload

Payload 部分也是 JSON 对象,用来存放数据。JWT 有7个官方字段:

  • iss (issuer):签发人
  • exp (expiration time):过期时间,以秒为单位
  • iat (Issued At):签发时间,能够算出JWT的存在时间
  • nbf (Not Before):生效时间
  • jti (JWT ID):JWT 的唯一标识。用来防止 JWT 重复。
  • sub (subject):主题(很少使用)
  • aud (audience):token的受众(很少被使用)
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

除了官方设定的字段,自己也可以自定义字段, JWT 默认不加密,任何人都可以读取,所以不要把敏感信息存放在这个部分。

用 Base64Url 将这个 JSON 对象编码后,作为 JWT 的第二部分

Signature

使用 Header 指定的算法对 Header、Payload、密钥三部分进行签名,生成的字符串作为 JWT 的第三部分。

签名可以用来验证数据是否被篡改。

4、JWT 的特点

  1. JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次
  2. JWT 不加密的情况下,不能将敏感数据写入 JWT
  3. JWT 不仅可以用于认证,也可以用于交换信息
  4. JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦签发了 JWT ,在到期之前就会始终有效(目前有这样一个疑惑)
  5. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  6. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
  7. JWT是无状态的,不能用于登录登出判断。
  8. 不要将token存于数据库,否则在分布式系统内失去了token存在的意义。

二、代码示例

写一个代码示例,和shiro分离开,自己写一个登录验证。

使用swagger工具进行接口测试。

JWT依赖:

 <!--引入JWT依赖,由于是基于Java,所以需要的是java-jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

swagger配置:将token验证放在header里

@Configuration
@EnableSwagger2
public class swaggerConfig {

    @Bean
    public Docket createRestApi() {
        ParameterBuilder tokenPar = new ParameterBuilder();
        List<Parameter> pars = new ArrayList<Parameter>();
        tokenPar.name("Authorization").description("Authorization")
                .modelRef(new ModelRef("string")).parameterType("header").required(false).build();
        pars.add(tokenPar.build());
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("mptest.mybatistest"))
                .paths(PathSelectors.any())
                .build().globalOperationParameters(pars)  ;
    }

    @SuppressWarnings("deprecation")
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("个人测试")
                .description("个人测试用api")
                .termsOfServiceUrl("termsOfServiceUrl")
                .contact("测试")
                .version("1.0")
                .build();
    }

}

通过过滤器来过滤token是否正确。只过滤验证的接口,不过滤登录接口。

将token中的账号解析出来,与数据库比较,再将密码作为密钥进行解密验证。token验证不通过则拦截,我只做了简单处理,具体逻辑没有写。

@WebFilter(filterName = "JWTFilter",urlPatterns = "/login/verity")
public class JWTFilter implements Filter {
    @Autowired
    private UserDao userDao;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
         HttpServletRequest request1=(HttpServletRequest) request;
        try{ String token = request1.getHeader("Authorization");
        DecodedJWT jwt = JWT.decode(token);
        String username=jwt.getClaim("username").asString();
        User user = userDao.selectOne(new QueryWrapper<User>().eq("username",username).last("limit 1"));
        JWTVerifier verifier= JWT.require(Algorithm.HMAC256(user.getPassword())).build();

            jwt = verifier.verify(token);
            chain.doFilter(request, response);
        }catch (Exception e){ //之后修改的内容
             HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            String json="{\"code\":401,\"message\":\"token验证失败\"}";
            httpServletResponse.setHeader("Content-type", "application/json;charset=UTF-8");
            httpServletResponse.getWriter().write(json);
            return;
        }
    }

    @Override
    public void destroy() {
    }
}

controller层里两个接口:一个登录、一个验证。注意一定要给token一个有效时间,否则签发的token将一直存在。

@RestController
@RequestMapping("login")
public class LoginController {

    //过期时间
    private static long time=1000*60;
    //密钥
    private static String secret="1234";

    @Autowired
    private UserDao userDao;

    @PostMapping("/login")
    public String login(User user){
        //指定签名算法,header部分
        Algorithm algorithm=Algorithm.HMAC256(user.getPassword().getBytes(StandardCharsets.UTF_8));
        //生成有效时间 
        Date expire=new Date(System.currentTimeMillis()+time);
        String token = JWT.create().withClaim("username",user.getUsername()).withExpiresAt(expire).sign(algorithm);
        System.out.println("token:"+token);
        return token;
    }
    @GetMapping("/verity")
    public String verity(){
        String s = "验证成功";
        return s;
    }
}

登录测试:获得token

验证测试:正确情况

验证测试:错误情况

 三、源码分析

controller中有这么一行代码:

  String token = JWT.create().withClaim("username",user.getUsername()).withExpiresAt(expire).sign(algorithm);

以它为起点来分析token是怎么生成的。

查看sign方法:进入到JWTCreator类

public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCreationException {
            if (algorithm == null) {
                throw new IllegalArgumentException("The Algorithm cannot be null.");
            } else {
                this.headerClaims.put("alg", algorithm.getName());
                this.headerClaims.put("typ", "JWT");
                String signingKeyId = algorithm.getSigningKeyId();
                if (signingKeyId != null) {
                    this.withKeyId(signingKeyId);
                }

                return (new JWTCreator(algorithm, this.headerClaims, this.payloadClaims)).sign();
            }
        }

sign方法首先将签名的方法和jwt类型放入到了headerClaims中,然后我们看一下headerClaims这个参数。

内部静态类中有这两个map集合。headerClaims 用来存头部信息,payloadClaims 用来存负载信息。

 private final Map<String, Object> payloadClaims = new HashMap();
        private Map<String, Object> headerClaims = new HashMap();

JWTCreator类中withClaim是用于添加负载信息的。

 public JWTCreator.Builder withClaim(String name, String value) throws IllegalArgumentException {
            this.assertNonNull(name);
            this.addClaim(name, value);
            return this;
        }

JWT官方规定好的方法名和添加自定义的方法名不同。如到期时间:

 public JWTCreator.Builder withExpiresAt(Date expiresAt) {
            this.addClaim("exp", expiresAt);
            return this;
        }

还是这个方法sign(Algorithm algorithm),返回值构造了一个JWTCreator

 private JWTCreator(Algorithm algorithm, Map<String, Object> headerClaims, Map<String, Object> payloadClaims) throws JWTCreationException {
        this.algorithm = algorithm;

        try {
            ObjectMapper mapper = new ObjectMapper();
            SimpleModule module = new SimpleModule();
            module.addSerializer(ClaimsHolder.class, new PayloadSerializer());
            mapper.registerModule(module);
            mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
            this.headerJson = mapper.writeValueAsString(headerClaims);
            this.payloadJson = mapper.writeValueAsString(new ClaimsHolder(payloadClaims));
        } catch (JsonProcessingException var6) {
            throw new JWTCreationException("Some of the Claims couldn't be converted to a valid JSON format.", var6);
        }
    }

 该构造方法主要是将map集合转化成了字符串

构造完成后调用了一个无参的sign方法

private String sign() throws SignatureGenerationException {
        String header = Base64.encodeBase64URLSafeString(this.headerJson.getBytes(StandardCharsets.UTF_8));
        String payload = Base64.encodeBase64URLSafeString(this.payloadJson.getBytes(StandardCharsets.UTF_8));
        String content = String.format("%s.%s", header, payload);
        byte[] signatureBytes = this.algorithm.sign(content.getBytes(StandardCharsets.UTF_8));
        String signature = Base64.encodeBase64URLSafeString(signatureBytes);
        return String.format("%s.%s", content, signature);
    }

该方法将头部和负载信息进行base64加密后以.隔开拼接成字符串,然后再将该字符串进行签名,将签名结果以同样的方式进行拼接,最后返回一个完整的token。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值