jwt的加密原理,和token的简单操作

1. 两种token认证方式

传统的token认证

用户登录,服务端给前端返回token,并将token保存在服务端。
以后用户再来访问时,需要携带token,服务端获取token后再去数据库获取token做校验。

JWT的token认证

用户登录,服务端给用户返回一个token(服务端不保存)
以后用户再来访问时,需要携带token,服务端获取token做校验

两种认证方式对比:
jwt相对于传统的token认证,无需将token保存在服务端。

因为HTTP request 本身是stateless的,所以要不在server端使用session来判断,要不就用JWT,也就是bearer token,包含的有效期信息,以及user 信息来进行状态判断是否接受HTTP的request(比如用户是否已经登录),来避免存储session,以及服务器集群之间还要实现session同步的麻烦,现在只要定义一个secret_key就行。

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

Authorization: Bearer 

JOSN Web Token(jwt)包含头部(header),载荷(claim set), 和签名(signature)。可以在载荷中存放预定义的元数据,只要是JOSON格式就可以了。

2. jwt的token加密解密过程

2.1 生成token

用户登录成功后,使用jwt创建一个token,并返回给用户,token格式如下

Base64URL(header)//第一段header
.base64UrlEncode(payload)//第二段payload
.HMACSHA256(Base64URL(header).base64UrlEncode(payload),secret)//第三段verify signature

例子:

eyJhbGciOiJIUzI1NiJ9//第一段
.eyJvcGVuSWQiOiJvam5NVjVKQ3htdTI1Zjl6ai1SYU5xN0JiZTJvIiwianRpIjoidG9rZW5JZCIsImlhdCI6MTY0NDEzNDI5MywiZXhwIjoxNjQ0MTM2OTIwfQ//第二段
.MRx_xPGNa9lzDGj4nrcdENCA2OgIp4En0TL_GH-_0BI//第三段

注意:jwt生成的token是由三段字符串拼接而成,使用 . 连接起来

1.token的第一段字符串:由下面的json数据通过base64(可逆)加密算法得到。

{
  "alg": "HS256",   //第三段字符串的不可逆加密类型HS256
  "typ": "JWT"   //token类型JWT
}

2.token的第二段字符串:是由下面的payload信息通过base64(可逆)加密算法得到

// payload信息 为自定义值,一般不放敏感信息
{
  "sub": "1234567890",   //用户id
  "name": "John Doe",		//用户名
  "exp": 1516239022,	//token过期时间
  "openId": "fasdkhgflksdhfgsdkjlf"
}

3.token的第三段字符串构成:
1)先将第一段和第二段的密文拼接起来
2)对拼接起来的密文字符串和自定义的盐进行 上边指定的HS256加密
3)对HS256加密后的密文再做base64加密

注意:第一、二部分可以通过Base64解密得到,但第三部分不可以!

生成token代码如下

  /**
     * 创建JWT
     */
    public static String createJWT(Map<String, Object> claims, Long time) {
        //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        Date now = new Date(System.currentTimeMillis());

        SecretKey secretKey = generalKey();
        //生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        //下面就是在为payload添加各种标准声明和私有声明了
        //这里其实就是new一个JwtBuilder,设置jwt的body
        JwtBuilder builder = Jwts.builder()
                //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setId(jwtId)
                //iat: jwt的签发时间
                .setIssuedAt(now)
                //设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey);
        if (time >= 0) {
            long expMillis = nowMillis + time;
            Date exp = new Date(expMillis);
            //设置过期时间
            builder.setExpiration(exp);
        }
        return builder.compact();
        //然后返回token
    }

2.2 验证(解密)token

当用户再来访问时,需要携带token,后端需要对token进行校验

  • ①:获取token
  • ②:对token进行切割成三部分
  • ③:对第二段字符串进行base64解密,检测token是否超时?
  • ④:对第一二段字符串拼接,再次进行HS256加密,得到密文字符串
  • ⑤:对token的第三段HS256加密

    /**
     * 验证jwt
     */
    public static Claims verifyJwt(String token) {
        //签名秘钥,和生成的签名的秘钥一模一样
        SecretKey key = generalKey();
        Claims claims;
        try {
            claims = Jwts.parser()  //得到DefaultJwtParser
                    .setSigningKey(key)         //设置签名的秘钥
                    .parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }//设置需要解析的jwt
        return claims;

    }

ps : token一旦生成,在过期时间内永久有效,即使项目重启!想要失效token必须等待过期,或者重置盐值!

3. token登出、改密后失效

使用jwt时,一般修改密码或退出登录时,需要把正在使用的token做失效处理,防止别的客户端使用失效token访问信息。

  • 方案一:在每次修改密码或者退出登录后,修改一下自定义的盐值。当进行下次访问时,会根据自定义盐值验证token,修改了自定义盐值,自然访问不通过。
  • 方案二:利用数据库,存放一个修改或者登出的时间,在创建token时,标注上创建时间。如果这个创建时间小于修改或登出的时间,就表示它是修改或者登出之前的token,为过期token(有点不是很懂,好像就是判断token是否过期)
	/**
     * 根据userId和openid生成token
     */
    public static String generateToken(String openId) {
        Map<String, Object> map = new HashMap<>();
        map.put("openId", openId);
        return createJWT(map, tokenExpiredTime);//直接调用上面的createJWT方法
    }
	/**
     * token是否过期,就是拿到
     * @return  true:过期
     * lastLoginDate 数据库记录的最后一次登出时间
     * issueDate token 创建时间
     */
    public boolean isTokenExpired(Date expiration,Date lastLoginDate,Date issueDate) {
        //token创建时间小于数据库记录的最后一次登出时间 过期
        if(lastLoginDate == null){
            return expiration.before(new Date());
        }else{
            return issueDate.before(lastLoginDate);
        }
    }



拦截器的判断:

 			if(jwtUtils.isTokenExpired(claims.getExpiration(),user.getLoginDate(),claims.getIssuedAt())){
               Result result = ResultGenerator.genFailResult(ResultCode.UNAUTHORIZED,"token失效,请重新登录");
               SendMsgUtil.sendJsonMessage(response,result);
               return false;
           }

4. token的自动续期、一定时间内无操作掉线

场景:用户登陆后,token的过期时间为30分钟,如果在这30分钟内没有操作,则重新登录,如果30分钟内有操作,就给token自动续一个新的时间。避免用户正在操作时掉线重登!

实现①:在jwt生成token时先不设置过期时间,过期时间的操作放在redis中。()这一次官网项目好像就有这个)

  • ①:在登陆时,把用户信息(或者token)放进redis,并设置过期时间
  • ②:如果30分钟内用户有操作,前端带着token来访问,过滤器解析token得到用户信息,去redis中验证用户信息,验证成功则在redis中增加过期时间,验证失败,返回token错误。实现了token时间的自动更新。
  • ③:如果30分钟内用户无操作,redis中的用户信息已过期,此时再进行操作,token解析出的用户信息在redis中验证失败,则重新登录。实现了一定时间内无操作掉线!

实现②:使用access_token、refresh_token 解决

  • 登录获取token(包括访问令牌access_token,刷新令牌refresh_token),其中access_token设置过期时间为5分钟,refresh_token设置过期时间为30分钟。不能同时过期

  • 前端保存access_tokenrefresh_token,每次请求带着access_token去访问服务器资源

  • 服务器校验access_token有效性,通过解析access_token看是否能解析出用户信息。如果用户信息为null,说明token无效,返回401,让用户重新登录

  • 服务器端校验access_token是否过期

  • 如果access_token没有过期,则token正常,继续执行业务逻辑

    • 如果access_token过期,计算 过期后到当前的时间大小 是否在refresh_token过期时间之内(是否大于30 - 5 - 5 = 20分钟,为什么不是30 - 5 = 25分钟呢?主要是想对正在请求的用户token做一个缓存,保证在最后五分钟内,新、老token都有效!防止正在进行的请求token突然失效!),

      • 如果大于refresh_token的过期时间,则表示用户长时间无操作,token真正过期了,返回401,让用户重新登录
    • 如果小于refresh_token的过期时间,则继续让该access_token访问业务,但返回给前端标识,提示token已过期,让前端带着refresh_token去服务器获取新的access_token,并保存在前端,后续使用新的access_token去访问!

5.JWT工具类

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

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import com.google.common.io.BaseEncoding;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;


@Component
public class JwtUtils {
    private static long tokenExpiredTime;

    private static String jwtId;

    private static String jwtSecret;

    /**
     * 创建JWT
     */
    public static String createJWT(Map<String, Object> claims, Long time) {
        //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        Date now = new Date(System.currentTimeMillis());

        SecretKey secretKey = generalKey();
        //生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        //下面就是在为payload添加各种标准声明和私有声明了
        //这里其实就是new一个JwtBuilder,设置jwt的body
        JwtBuilder builder = Jwts.builder()
                //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setId(jwtId)
                //iat: jwt的签发时间
                .setIssuedAt(now)
                //设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey);
        if (time >= 0) {
            long expMillis = nowMillis + time;
            Date exp = new Date(expMillis);
            //设置过期时间
            builder.setExpiration(exp);
        }
        return builder.compact();
    }


    /**
     * 验证jwt
     */
    public static Claims verifyJwt(String token) {
        //签名秘钥,和生成的签名的秘钥一模一样
        SecretKey key = generalKey();
        Claims claims;
        try {
            claims = Jwts.parser()  //得到DefaultJwtParser
                    .setSigningKey(key)         //设置签名的秘钥
                    .parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }//设置需要解析的jwt
        return claims;

    }

    /**
     * 由字符串生成加密key
     *
     * @return SecretKey
     */
    public static SecretKey generalKey() {
        String stringKey = jwtSecret;
        byte[] encodedKey = BaseEncoding.base64().decode(stringKey);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "HmacSHA256");
        return key;
    }

    /**
     * 根据userId和openid生成token
     */
    public static String generateToken(String openId) {
        Map<String, Object> map = new HashMap<>();
        map.put("openId", openId);
        return createJWT(map, tokenExpiredTime);
    }

    @Value("${jwt.token-expired-time}")
    public void setTokenExpiredTime(long tokenExpiredTime) {
        JwtUtils.tokenExpiredTime = tokenExpiredTime;
    }

    @Value("${jwt.id}")
    public void setJwtId(String jwtId) {
        JwtUtils.jwtId = jwtId;
    }

    @Value("${jwt.secret}")
    public void setJwtSecret(String jwtSecret) {
        JwtUtils.jwtSecret = jwtSecret;
    }

    public static long getTokenExpiredTime() {
        return tokenExpiredTime;
    }

    public static String getJwtId() {
        return jwtId;
    }

    public static String getJwtSecret() {
        return jwtSecret;
    }
}

6.整合springboot

搭建springboot+mybatis-plus+jwt环境

引入依赖:

    <dependencies>



        
        <dependency>
            <groupId>com.google.guavagroupId>
            <artifactId>guavaartifactId>
            <version>31.0.1-jreversion>
        dependency>

        
        
        <dependency>
            <groupId>com.auth0groupId>
            <artifactId>java-jwtartifactId>
            <version>3.4.0version>
        dependency>
        
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.22version>
        dependency>

        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.2.6version>
        dependency>
        
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwt-apiartifactId>
            <version>0.10.7version>
            <scope>compilescope>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>com.fenggroupId>
            <artifactId>springbootmybatisplusartifactId>
            <version>0.0.1-SNAPSHOTversion>
            <scope>compilescope>
        dependency>

    dependencies>

yaml中的配置信息:

server:
  port: 8989

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
    username: root
    password: nihao123
    druid:
      initialSize: 10   #初始化连接个数
      minIdle: 10       #最小空闲连接个数
      maxActive: 100    #最大连接个数
      maxWait: 60000    #获取连接时最大等待时间,单位毫秒。
      timeBetweenEvictionRunsMillis: 60000  #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      minEvictableIdleTimeMillis: 30000     #配置一个连接在池中最小生存的时间,单位是毫秒
      validationQuery: select 'x' #用来检测连接是否有效的sql,要求是一个查询语句。
      testWhileIdle: true       #建议配置为true,不影响性能,并且保证安全性。如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
      testOnBorrow: true        #申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
      testOnReturn: false       #归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
      poolPreparedStatements: false #是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
      maxPoolPreparedStatementPerConnectionSize: -1 #要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
      filters: stat,wall #通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat,日志用的filter:log4j,防御sql注入的filter:wall
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
      useGlobalDataSourceStat: false # 合并多个DruidDataSource的监控数据
jwt:
  #设置token的过期时间,单位为秒
  token-expired-time: 36000 #10小时
  #设置token的id
  id: tokenId
  #设置密钥
  secret: aPbOBbnH4gnZBzIYEY7mxWNu49kYljNPMeva9Fjrwwqzw0bFlO0kPXZTCGaVcw0j
#
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:com/feng/mapper/xml/*.xml
  global-config:
    db-config:
      logic-delete-value: 1
      logic-not-delete-value: 0
      id-type: auto

创建一个简单的数据表

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(80) DEFAULT NULL COMMENT '用户名',
  `password` varchar(40) DEFAULT NULL COMMENT '用户密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

记得插入账号和密码信息

entity类

import lombok.Data;
import lombok.experimental.Accessors;

/**
 * @Description:
 * @Author Ladidol
 * @Date: 2022/3/21 11:01
 * @Version 1.0
 */
@Data
@Accessors(chain=true)//这是干啥用的哦
public class User {
    private String id;
    private String name;
    private String password;
}

service里面的

public interface UserService extends IService<User> {
    User login(User user);//登录接口
}
@Service
@Transactional
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Resource
    UserMapper userMapper;

    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public User login(User user) {
        User userDB = userMapper.login(user);
        if(userDB!=null){
            return userDB;
        }
        throw  new RuntimeException("登录失败~~");
    }
}

mapper里面:

@Mapper
public interface UserMapper extends BaseMapper<User> {
    User login(User user);
}

xml文件中


DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.feng.mapper.UserMapper">
    
    <select id="login" parameterType="com.feng.entity.User" resultType="com.feng.entity.User">
        select *
        from user
        where name = #{name}
          and password = #{password}
    select>
mapper>

controller类

@RestController
@RequestMapping
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;
    
    /**
     * @Description: 登录判断,得到一个token
     * @Author: ladidol
     * @Date: 2022/3/21 13:40 
     * @Param: [user]
     * @Return: java.util.Map
     */
    @GetMapping("/user/login")
    public Map<String,Object> login(@RequestBody User user) {
        Map<String,Object> result = new HashMap<>();
        log.info("用户名: [{}]", user.getName());
        log.info("密码: [{}]", user.getPassword());
        try {
            User userDB = userService.login(user);
            Map<String, Object> map = new HashMap<>();//用来存放payload
            map.put("id",userDB.getId());
            map.put("username", userDB.getName());
            String token = JwtUtils.createJWT(map,10000000L);
            result.put("state",true);
            result.put("msg","登录成功!!!");
            result.put("token",token); //成功返回token信息
        } catch (Exception e) {
            e.printStackTrace();
            result.put("state","false");
            result.put("msg",e.getMessage());
        }
        return result;
    }




    /**
     * @Description: 再次判断token
     * @Author: ladidol
     * @Date: 2022/3/21 13:40 
     * @Param: [token]
     * @Return: java.util.Map
     */
    @GetMapping("/test/test")
    public Map<String, Object> test(String token) {
        Map<String, Object> map = new HashMap<>();
        try {
            JwtUtils.verifyJwt(token);
            map.put("msg", "验证通过~~~");
            map.put("state", true);
        } catch (TokenExpiredException e) {
            map.put("state", false);
            map.put("msg", "Token已经过期!!!");
        } catch (SignatureVerificationException e){
            map.put("state", false);
            map.put("msg", "签名错误!!!");
        } catch (AlgorithmMismatchException e){
            map.put("state", false);
            map.put("msg", "加密算法不匹配!!!");
        } catch (Exception e) {
            e.printStackTrace();
            map.put("state", false);
            map.put("msg", "无效token~~");
        }
        return map;
    }
}

添加拦截器:

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  String token = request.getHeader("token");
  Map<String,Object> map = new HashMap<>();
  try {
    JWTUtils.verify(token);
    return true;
  } catch (TokenExpiredException e) {
    map.put("state", false);
    map.put("msg", "Token已经过期!!!");
  } catch (SignatureVerificationException e){
    map.put("state", false);
    map.put("msg", "签名错误!!!");
  } catch (AlgorithmMismatchException e){
    map.put("state", false);
    map.put("msg", "加密算法不匹配!!!");
  } catch (Exception e) {
    e.printStackTrace();
    map.put("state", false);
    map.put("msg", "无效token~~");
  }
  String json = new ObjectMapper().writeValueAsString(map);
  response.setContentType("application/json;charset=UTF-8");
  response.getWriter().println(json);
  return false;
}
@Component
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JwtTokenInterceptor()).
          excludePathPatterns("/user/**")
          .addPathPatterns("/**");
    }
}

可以在postman里面测试:

登录接口:

jwt的加密原理,和token的简单操作_第1张图片

测试接口:

jwt的加密原理,和token的简单操作_第2张图片
欢迎点赞关注哦!
也欢迎到访我的博客!传送门!

你可能感兴趣的:(java,springboot学习,mybatis-plus,jwt,token,登录)