JWT学习——原理/实现

背景:

在学习SpringCloud框架时,用到JWT做用户的认证登录和鉴权。为了更好的理解其工作原理,接下来就是具体学习JWT了。

什么是JWT?

JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,用于在各方之间作为JSON对象安全地传输信息。 此信息可以通过数字签名进行验证和信任。 JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。

虽然JWT可以加密以在各方之间提供保密,但我们将专注于签名token。 签名token可以验证其中包含的声明的完整性,而加密token则隐藏其他方的声明。 当使用公钥/私钥对签名token时,签名还证明只有持有私钥的一方是签署它的一方。

何时使用JWT?

  • 授权:这是我们使用JWT最广泛的应用场景。一次用户登录,后续请求将会包含JWT,对于那些合法的token,允许用户连接路由,服务和资源。如今在单独签名上是一种使用JWT的广泛特征。因为他们开销很小并且可以在不通领域轻松使用。

  • 信息交换:JSON Web Token是一种在各方面之间安全信息传输的好的方式 因为JWT可以签名 - 例如,使用公钥/私钥对 - 您可以确定发件人是他们所说的人。 此外,由于使用标头和有效负载计算签名,您还可以验证内容是否未被篡改。

JSON Web Token由什么组成?

JSON Web Token由三部分组成,以dots "  .  " 分割。

他们是:

  • Header
  • Payload
  • Signature

 因此,一个JWT通常以下面这种形式出现。

xxxxx.yyyyy.zzzzz

分别看看不通部分。

Header

 标头通常由两部分组成:token的类型,即JWT,以及正在使用的签名算法,例如HMAC SHA256或RSA。

For example:

{
  "alg": "HS256",
  "typ": "JWT"
}

然后,这个JSON被编码为Base64Url,形成JWT的第一部分。

Payload

token的第二部分是payload(有效负载),其中包含claims(声明)。Claims是关于一个实体(通常是用户)和其他数据类型的声名。claims有三种类型:registered,public,and private claims。

  • 已注册的声明:这些是一组预定义声明,不是强制性的,但建议使用,以提供一组有用的,可互操作的声明。 其中一些是:iss(发行人),exp(到期时间),sub(主题),aud(观众)and others。(请注意,声明名称只有三个字符,因为JWT意味着紧凑。)
  • 公开声明:这些可以由使用JWT的人随意定义。 但为避免冲突,应在IANA JSON Web Token Registry中定义它们,或者将其定义为包含防冲突命名空间的URI。
  • 私人声明:这些声明是为了在同意使用它们的各方之间共享信息而创建的,并且既不是注册声明也不是公开声明。

 An example payload could be:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Signature(重点)

To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that.

要创建签名部分,您必须采用编码header,编码的payload,a secret,标头中指定的算法,并对其进行签名。这段只读中文不好理解附上英文原版。

for example如果你想使用HMAC SHA256算法,签名会以下面的方式创建:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

签名用于验证消息在此过程中未被更改,并且,在使用私钥签名的token的情况下,它还可以验证JWT的发件人是否是它所声称的人。

Putting all together

输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递,而与基于XML的标准(如SAML)相比更加紧凑。

下面显示了一个JWT,它具有先前的头和有效负载编码,并使用机密签名。 编码JWT

如果您想使用JWT并将这些概念付诸实践,您可以使用jwt.io Debugger来解码,验证和生成JWT。 

 JWT.io Debugger

How do JSON Web Tokens work? 

在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web令牌。 由于令牌是凭证,因此必须非常小心以防止出现安全问题。 一般情况下,您不应该将令牌保留的时间超过要求。

每当用户想要访问受保护的路由或资源时,用户代理应该使用承载模式发送JWT,通常在Authorization标头中。 标题的内容应如下所示:

Authorization: Bearer <token>

在某些情况下,这可以是无状态授权机制。 服务器的受保护路由将在Authorization标头中检查有效的JWT,如果存在,则允许用户访问受保护的资源。 如果JWT包含必要的数据,则可以减少查询数据库以进行某些操作的需要,尽管可能并非总是如此。

如果在Authorization标头中发送token,则跨域资源共享(CORS)将不会成为问题,因为它不使用cookie。

下图显示了如何获取JWT并用于访问API或资源:

How does a JSON Web Token work

  1. 应用程序或客户端向授权服务器请求授权。 这是通过其中一个不同的授权流程执行的。 例如,典型的OpenID Connect兼容Web应用程序将使用授权代码流通过/ oauth / authorize端点。
  2. 授予授权后,授权服务器会向应用程序返回访问token。
  3. 应用程序使用访问token来访问受保护资源(如API)。

请注意,使用签名token,token中包含的所有信息都会向用户或其他方公开,即使他们无法更改。 这意味着您不应该在token中放置秘密信息。 

Why should we use JSON Web Tokens?

让我们来谈谈JSON Web Tokens(JWT)与Simple Web Tokens(SWT)和Security Assertion Markup Language Tokens(SAML)相比的好处。

由于JSON比XML更简洁,因此在编码时它的大小也更小,使得JWT比SAML更紧凑。这使得JWT成为在HTML和HTTP环境中传递的不错选择。

在安全方面,SWT只能使用HMAC算法通过共享密钥对称签名。但是,JWT和SAML token可以使用X.509证书形式的公钥/私钥对进行签名。与签名JSON的简单性相比,使用XML数字签名对XML进行签名而不会引入模糊的安全漏洞非常困难。

JSON解析器在大多数编程语言中很常见,因为它们直接映射到对象。相反,XML没有自然的文档到对象映射。这使得使用JWT比使用SAML断言更容易。

关于使用,JWT用于互联网规模。这突出了在多个平台(尤其是移动平台)上轻松进行JSON Web Token的客户端处理。

Comparing the length of an encoded JWT and an encoded SAML

比较编码的JWT和编码的SAML的长度

如果您想了解有关JSON Web Tokens的更多信息,甚至开始使用它们在您自己的应用程序中执行身份验证,请浏览到Auth0上的JSON Web Token登录页面

 好了以上部分都是官方对JWT的介绍,下面来看看身份认证使用session、直接使用token以及JWT的过程

 

 

这三者间的比较呢

session对 多服务、移动端等的支持不好。

只用token的话解决了多服务、移动端之间通信的问题,且是一个独立的应用。但是增加了外部存储(redis)的依赖和代码实现相对复杂。

使用JWT的话代码简单,且没用session的问题,并且有自己的安全机制(多种加密方式等,通信安全不会被外部修改等优点)

 最后来看看实际操作中Jwthelper工具类的编写,以及service中的调用

首先是Jwthelper工具类

package com.hzys.basicservice.user.utils;

import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.Map;

import org.apache.commons.lang3.time.DateUtils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.common.collect.Maps;

public class JwtHelper {
  
  private static final String  SECRET = "session_secret";
  
  private static final String  ISSUER = "mooc_user";
  
  
  public static String genToken(Map<String, String> claims){
    try {
      //创建一个发布者为ISSUER和过期时间为一天的token
      JWTCreator.Builder builder = JWT.create().withIssuer(ISSUER).withExpiresAt(DateUtils.addDays(new Date(), 1));
      //将Map中的值存放入token中
      claims.forEach((k,v) -> builder.withClaim(k, v));
      //签名用的算法
      Algorithm algorithm = Algorithm.HMAC256(SECRET);
      //签名
      return builder.sign(algorithm).toString();
    } catch (IllegalArgumentException | UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }
  }
  
  public static Map<String, String> verifyToken(String token)  {
    Algorithm algorithm = null;
    try {
      //签名算法
      algorithm = Algorithm.HMAC256(SECRET);
    } catch (IllegalArgumentException | UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }
    //根据签名和发布者创建JWT验证对象
    JWTVerifier verifier = JWT.require(algorithm).withIssuer(ISSUER).build();
    //验证token
    DecodedJWT jwt =  verifier.verify(token);
    //获取token中存放的信息
    Map<String, Claim> map = jwt.getClaims();
    Map<String, String> resultMap = Maps.newHashMap();
    //将token中信息放入新的HashMap中
    map.forEach((k,v) -> resultMap.put(k, v.asString()));
    return resultMap;
  }

}

然后是service调用

 

package com.hzys.basicservice.user.service;

import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import com.hzys.basicservice.user.common.UserException;
import com.hzys.basicservice.user.mapper.UserMapper;
import com.hzys.basicservice.user.model.User;
import com.hzys.basicservice.user.utils.BeanHelper;
import com.hzys.basicservice.user.utils.HashUtils;
import com.hzys.basicservice.user.utils.JwtHelper;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.alibaba.fastjson.JSON;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;


@Service
public class UserService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private MailService mailService;


    @Value("${file.prefix}")
    private String imgPrefix;

    /**
     * 1.首先通过缓存获取
     * 2.不存在将从通过数据库获取用户对象
     * 3.将用户对象写入缓存,设置缓存时间5分钟
     * 4.返回对象
     *
     * @param id
     * @return
     */
    public User getUserById(Long id) {
        String key = "user:" + id;
        String json = redisTemplate.opsForValue().get(key);
        User user = null;
        if (Strings.isNullOrEmpty(json)) {
            user = userMapper.selectById(id);
            user.setAvatar(imgPrefix + user.getAvatar());
            String string = JSON.toJSONString(user);
            redisTemplate.opsForValue().set(key, string);
            redisTemplate.expire(key, 5, TimeUnit.MINUTES);
        } else {
            user = JSON.parseObject(json, User.class);
        }
        return user;
    }

    public List<User> getUserByQuery(User user) {
        List<User> users = userMapper.select(user);
        users.forEach(u -> {
            u.setAvatar(imgPrefix + u.getAvatar());
        });
        return users;
    }

    /**
     * 注册
     *
     * @param user
     * @param enableUrl
     * @return
     */
    public boolean addAccount(User user, String enableUrl) {
        user.setPasswd(HashUtils.encryPassword(user.getPasswd()));
        BeanHelper.onInsert(user);
        userMapper.insert(user);
        registerNotify(user.getEmail(), enableUrl);
        return true;
    }

    /**
     * 发送注册激活邮件
     *
     * @param email
     * @param enableUrl
     */
    private void registerNotify(String email, String enableUrl) {
        String randomKey = HashUtils.hashString(email) + RandomStringUtils.randomAlphabetic(10);
        redisTemplate.opsForValue().set(randomKey, email);
        redisTemplate.expire(randomKey, 1, TimeUnit.HOURS);
        String content = enableUrl + "?key=" + randomKey;
        mailService.sendSimpleMail("房产平台激活邮件", content, email);
    }

    public boolean enable(String key) {
        String email = redisTemplate.opsForValue().get(key);
        if (StringUtils.isBlank(email)) {
            throw new UserException(UserException.Type.USER_NOT_FOUND, "无效的key");
        }
        User updateUser = new User();
        updateUser.setEmail(email);
        updateUser.setEnable(1);
        userMapper.update(updateUser);
        return true;
    }

    /**
     * 校验用户名密码、生成token并返回用户对象
     * @param email
     * @param passwd
     * @return
     */
    public User auth(String email, String passwd) {
        if (StringUtils.isBlank(email) || StringUtils.isBlank(passwd)) {
            throw new UserException(UserException.Type.USER_AUTH_FAIL,"User Auth Fail");
        }
        User user = new User();
        user.setEmail(email);
        user.setPasswd(HashUtils.encryPassword(passwd));
        user.setEnable(1);
        List<User> list =  getUserByQuery(user);
        if (!list.isEmpty()) {
            User retUser = list.get(0);
            onLogin(retUser);
            return retUser;
        }
        throw new UserException(UserException.Type.USER_AUTH_FAIL,"User Auth Fail");
    }

    private void onLogin(User user) {
        String token =  JwtHelper.genToken(ImmutableMap.of("email", user.getEmail(), "name", user.getName(),"ts",Instant.now().getEpochSecond()+""));
        renewToken(token,user.getEmail());
        user.setToken(token);
    }

    private String renewToken(String token, String email) {
        redisTemplate.opsForValue().set(email, token);
        redisTemplate.expire(email, 30, TimeUnit.MINUTES);
        return token;
    }

    public User getLoginedUserByToken(String token) {
        Map<String, String> map = null;
        try {
            map = JwtHelper.verifyToken(token);
        } catch (Exception e) {
            throw new UserException(UserException.Type.USER_NOT_LOGIN,"User not login");
        }
        String email =  map.get("email");
        Long expired = redisTemplate.getExpire(email);
        if (expired > 0L) {
            renewToken(token, email);
            User user = getUserByEmail(email);
            user.setToken(token);
            return user;
        }
        throw new UserException(UserException.Type.USER_NOT_LOGIN,"user not login");

    }

    private User getUserByEmail(String email) {
        User user = new User();
        user.setEmail(email);
        List<User> list = getUserByQuery(user);
        if (!list.isEmpty()) {
            return list.get(0);
        }
        throw new UserException(UserException.Type.USER_NOT_FOUND,"User not found for " + email);
    }

    public void invalidate(String token) {
        Map<String, String> map = JwtHelper.verifyToken(token);
        redisTemplate.delete(map.get("email"));
    }

    @Transactional(rollbackFor = Exception.class)
    public User updateUser(User user) {
        if (user.getEmail() == null) {
            return null;
        }
        if (!Strings.isNullOrEmpty(user.getPasswd()) ) {
            user.setPasswd(HashUtils.encryPassword(user.getPasswd()));
        }
        userMapper.update(user);
        return userMapper.selectByEmail(user.getEmail());
    }

    public void resetNotify(String email,String url) {
        String randomKey = "reset_" + RandomStringUtils.randomAlphabetic(10);
        redisTemplate.opsForValue().set(randomKey, email);
        redisTemplate.expire(randomKey, 1,TimeUnit.HOURS);
        String content = url +"?key="+  randomKey;
        mailService.sendSimpleMail("房产平台重置密码邮件", content, email);

    }

    public String getResetKeyEmail(String key) {
        return  redisTemplate.opsForValue().get(key);
    }

    public User reset(String key, String password) {
        String email = getResetKeyEmail(key);
        User updateUser = new User();
        updateUser.setEmail(email);
        updateUser.setPasswd(HashUtils.encryPassword(password));
        userMapper.update(updateUser);
        return getUserByEmail(email);
    }
}

再加上user的model类

package com.hzys.basicservice.user.model;

import java.util.Date;

import org.springframework.web.multipart.MultipartFile;

import com.alibaba.fastjson.annotation.JSONField;


public class User {
  
  private Long id;
  private String  name;
  private String  phone;
  private String  email;
  private String  aboutme;
  private String  passwd;
  private String  confirmPasswd;
  private Integer type;
  private Date    createTime;
  private Integer enable;
  
  private String  avatar;
  
  @JSONField(deserialize=false,serialize=false)
  private MultipartFile avatarFile;
  
  private String newPassword;
  
  private String key;
  
  private Long  agencyId;
  
  private String token;
  
  private String enableUrl;
  
  private String agencyName;
  
  
  public Long getId() {
    return id;
  }
  public void setId(Long id) {
    this.id = id;
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public String getPhone() {
    return phone;
  }
  public void setPhone(String phone) {
    this.phone = phone;
  }
  public String getEmail() {
    return email;
  }
  public void setEmail(String email) {
    this.email = email;
  }
  public String getAboutme() {
    return aboutme;
  }
  public void setAboutme(String aboutme) {
    this.aboutme = aboutme;
  }
  public String getPasswd() {
    return passwd;
  }
  public void setPasswd(String passwd) {
    this.passwd = passwd;
  }
 
  public Integer getType() {
    return type;
  }
  public void setType(Integer type) {
    this.type = type;
  }
  public String getAgencyName() {
    return agencyName;
  }
  public void setAgencyName(String agencyName) {
    this.agencyName = agencyName;
  }
  public Date getCreateTime() {
    return createTime;
  }
  public void setCreateTime(Date createTime) {
    this.createTime = createTime;
  }
  public String getConfirmPasswd() {
    return confirmPasswd;
  }
  public void setConfirmPasswd(String confirmPasswd) {
    this.confirmPasswd = confirmPasswd;
  }
  
  
  public String getKey() {
    return key;
  }
  public void setKey(String key) {
    this.key = key;
  }
  public String getToken() {
    return token;
  }
  public void setToken(String token) {
    this.token = token;
  }
  public Integer getEnable() {
    return enable;
  }
  public void setEnable(Integer enable) {
    this.enable = enable;
  }
  
  public MultipartFile getAvatarFile() {
    return avatarFile;
  }
  public String getEnableUrl() {
    return enableUrl;
  }
  public void setEnableUrl(String enableUrl) {
    this.enableUrl = enableUrl;
  }
  public void setAvatarFile(MultipartFile avatarFile) {
    this.avatarFile = avatarFile;
  }
  public String getNewPassword() {
    return newPassword;
  }
  public String getAvatar() {
    return avatar;
  }
  public void setAvatar(String avatar) {
    this.avatar = avatar;
  }
  public void setNewPassword(String newPassword) {
    this.newPassword = newPassword;
  }
  public Long getAgencyId() {
    return agencyId;
  }
  public void setAgencyId(Long agencyId) {
    this.agencyId = agencyId;
  }

  

}

userMapper

package com.hzys.basicservice.user.mapper;

import java.util.List;

import com.hzys.basicservice.user.model.User;
import org.apache.ibatis.annotations.Mapper;



@Mapper
public interface UserMapper {
  
  User selectById(Long id);
  
  List<User> select(User user);
  
  int update(User user);
  
  int insert(User account);
  
  int delete(String email);
  
  User selectByEmail(String email);

}

最后是UserController

package com.hzys.basicservice.user.controller;


import com.hzys.basicservice.user.common.RestResponse;
import com.hzys.basicservice.user.model.User;
import com.hzys.basicservice.user.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;


@RestController
@RequestMapping("user")
public class UserController {
    private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private UserService userService;
    //-----------查询------------------

    @RequestMapping("getById")
    public RestResponse<User> getUserById(Long id) {
        User user = userService.getUserById(id);
        return RestResponse.success(user);

    }

    @RequestMapping("getByList")
    public RestResponse<List<User>> getUserByList(@RequestBody User user){
        List<User> list = userService.getUserByQuery(user);
        return RestResponse.success(list);
    }

    /**
     * 注册
     * @param user
     * @return
     */
    @RequestMapping("add")
    public RestResponse<User> add(@RequestBody User user) {
        userService.addAccount(user, user.getEnableUrl());
        return RestResponse.success();
    }


    /**
     * 主要激活key的验证
     */
    @RequestMapping("enable")
    public RestResponse<Object> enable(String key){
        userService.enable(key);
        return RestResponse.success();
    }

    //------------------------登录/鉴权-------------------------
    @RequestMapping("auth")
    public RestResponse<User> auth(@RequestBody User user) {
        User finalUser = userService.auth(user.getEmail(), user.getPasswd());
        return RestResponse.success(finalUser);
    }

    @RequestMapping("get")
    public RestResponse<User> getUser(String token){
        User finalUser = userService.getLoginedUserByToken(token);
        return RestResponse.success(finalUser);
    }

    @RequestMapping("logout")
    public RestResponse<Object> logout(String token){
        userService.invalidate(token);
        return RestResponse.success();
    }
}

以上实例来自慕课网学习视频如有兴趣可以前往慕课网学习观看。谢谢,如有不对之处,望不吝赐教!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值