JWT【说明及工具类】

简介

JWT(Json Web Token),基于token的用户认证原理:让用户输入账号和密码,认证通过后获得一个token(令牌),在token有效期里用户可以带着token访问特定资源。
开始token并没有一个统一标准,大家都各自使用自己的方案。后来出现了JWT(Json Web Token)这个标准。
JWT本质上是一个对JSON对象加密后的字符串。当服务器认证用户通过后,一个包含用户信息的json对象被加密后返回给用户,json 对象:

{
    "expire": "2019-11-29 20:19:00",
    "permissions": [
        "sys:user:list",
        "sys:dept:list",
        "sys:role:list"
   ],
    "role": [
        "dev"
   ],
    "userName": "dev123"
}

之后,用户访问服务器时,都要返回这个json对象。服务器只靠这个对象就可以识别用户身份,不需要再去查数据库。为了防止用户篡改数据,服务器在生成对象时将添加一个签名。
服务器不保存任何会话数据,也就是说,服务器变得无状态,从而更容易扩展。

JWT 怎么用

以浏览器接收到服务器发过来的jwt后,可以存储在Cookie 或 localStorage 中。之后,浏览器每次与服务器通信时都会带上JWT。可以将JWT放在Cookie中,会自动发送(不跨域),或将JWT放在HTTP请求头的授权字段中。

Authorization: Bearer <token>

也可放在url中,或POST请求的数据体中。

JWT 结构

jwt有3个组成部分,每部分通过点号来分割 header.payload.signature

  • 头部(header) 是一个 JSON 对象
  • 载荷(payload) 是一个 JSON 对象,用来存放实际需要传递的数据
  • 签名(signature) 对header和payload使用密钥进行签名,防止数据篡改。

头部 header

Jwt的头部是一个JSON,然后使用Base64URL编码,承载两部分信息:

  • 声明类型typ,表示这个令牌(token)的类型(type),JWT令牌统一写为JWT
  • 声明加密的算法alg,通常直接使用HMACSHA256,就是HS256了,也可以使用RSA,支持很多算(HS256、HS384、HS512、RS256、RS384、RS512、ES256、ES384、ES512、PS256、PS384)
{
  "alg": "HS256",
  "typ": "JWT"
}
  • Base64URL 编码后(Base64编码后可能出现字符+和/,在URL中不能直接作为参数,Base64URL就是把字符+和/分别变成-和 _。JWT有可能放在url中,所以要用Base64URL编码。)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

载荷 payload

payload也是一个JSON字符串,是承载消息具体内容的地方,也需要使用Base64URL编码,就是存储我们要保存到客户端的信息,一般都是包含用户的基本信息,权限信息,时间戳等信息。
JWT指定了一些官方字段(claims)备用:

  • iss: 签发人
  • exp: 过期时间
  • iat: 签发时间
  • nbf: 生效时间
  • jti: 编号
  • sub: 主题
  • aud: 受众

除了官方字段,在这个部分还可以添加私有字段,例如:

{
  "sub": "1dfaafa7-fddf-46f2-b3d8-11bfe9ac7230",
  "jwt-roles-key_": [
    "普通用户角色"
 ],
  "iss": "yingxue.com",
  "jwt-permissions-key": [
    "sys:user:list",
    "sys:dept:list",
    "sys:role:list",
    "sys:permission:list",
    "sys:log:list"
 ],
  "jwt-user-name-key": "dev123",
  "exp": 1575005723,
  "iat": 1574998523
}

Base64URL编码的后:

eyJzdWIiOiIxZGZhYWZhNy1mZGRmLTQ2ZjItYjNkOC0xMWJmZTlhYzcyMzAiLCJqd3Qtcm9sZXMta2V5XyI6WyLmma7pgJrnlKjmiLfo
p5LoibIiXSwiaXNzIjoieWluZ3h1ZS5jb20iLCJqd3QtcGVybWlzc2lvbnMta2V5IjpbInN5czp1c2VyOmxpc3QiLCJzeXM6ZGVwdDps
aXN0Iiwic3lzOnJvbGU6bGlzdCIsInN5czpwZXJtaXNzaW9uOmxpc3QiLCJzeXM6bG9nOmxpc3QiXSwiand0LXVzZXItbmFtZS1rZXki
OiJkZXYxMjMiLCJleHAiOjE1NzUwMDU3MjMsImlhdCI6MTU3NDk5ODUyM30

签名 Signature

Signature部分是对前两部分的防篡改签名。将Header和Payload用Base64URL编码后,再用点(.)连接起来。然后使用签名算法和密钥对这个字符串进行签名:

signature = HMACSHA256(header + "." + payload, secret);

首先,需要指定一个密码(secret)。该密码保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法根据以下公式生成签名。signature = HMACSHA256(header + "." + payload, secret); 在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象。 以上三部分都是在服务器定义,当用户登陆成功后,根据用户信息,按照jwt规则生成token返回给客户端。
签名信息:

qYWHdAbYZlP6akHTrDm-MkIWia8mPW-TO75eu8r0-Vk

组合在一起
3部分组合在一起,构成了完整的jwt:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxZGZhYWZhNy1mZGRmLTQ2ZjItYjNkOC0xMWJmZTlhYzcyMzAiLCJqd3Qtcm9sZXMta2V5XyI
6WyLmma7pgJrnlKjmiLfop5LoibIiXSwiaXNzIjoieWluZ3h1ZS5jb20iLCJqd3QtcGVybWlzc2lvbnMta2V5IjpbInN5czp1c2VyOmx
pc3QiLCJzeXM6ZGVwdDpsaXN0Iiwic3lzOnJvbGU6bGlzdCIsInN5czpwZXJtaXNzaW9uOmxpc3QiLCJzeXM6bG9nOmxpc3QiXSwiand
0LXVzZXItbmFtZS1rZXkiOiJkZXYxMjMiLCJleHAiOjE1NzUwMDU3MjMsImlhdCI6MTU3NDk5ODUyM30.qYWHdAbYZlP6akHTrDm�MkIWia8mPW-TO75eu8r0-Vk

使用要点

  • JWT默认是不加密的,但也可以加密,不加密时不宜在jwt中存放敏感信息
  • 不要泄露签名密钥(secret)
  • jwt签发后无法撤回,有效期不宜太长
  • JWT 泄露会被人冒用身份,为防止盗用,JWT应尽量使用 https 协议传输

实战流程(JWT 使用姿势)

大家有没有发现,现在的网站通常第一次登录验证通过后,在后续的操作都不需要用户名密码,那后端怎么确定这次访问的用户是合法用户呢?其实当第一次登录后,服务器生成一个Token 便将此 Token 返回给客户端,以后客户端只需带上这个 Token 前来请求数据即可,无需再次带上用户名和密码。
那么我们后端该怎么实现上述的业务呢?

1. 有状态 token

所谓的有状态就是 把生成的 token 保存在服务器端。
实现步骤:

  • 当用户登录进来后端生成一个随机数 token(我通常用uuid) 然后把 token 做key userId 做为 value 存入 reids 并且设置失效时间。
  • 编写一个拦截器,设置要拦截的 api(即是受保护的api)和开放的api(用户登录、注册等接口)。 去 header 或者 cookie 拿 token,如果 token 为空或者 token 已经失效(拿 token 去 redis 检测是否失效)则告知客户端
    引导到登录页面。

2. 无状态 token

所谓的无状态 token 就是服务器不保存 token 信息,当用户登陆成功后,返回 token 给客户端,客户端保存起来每次请求都会带过来。其实我们用 token 的作用就是拿到用户ID 只有拿到了 ID 才能区别是哪个用户访问,那么 JWT 刚刚好满足要求,JWT是签发给客户端而且 用户 ID 直接存在 JWT 里面,客户端每次请求过来的时候我们直接解析 JWT 拿到用户 ID,这样就达到了识别用户的效果。
但是在使用 JWT 的时候都会遇到下列的烦恼?
无法作废已颁布的令牌。所有的认证信息都在 JWT 中,由于在服务端没有状态,即使你知道了某个 JWT 被盗取了,你也没有办法将其作废。在 JWT 过期之前(你绝对应该设置过期时间),你无能为力。
不易应对数据过期。与上一条类似,JWT 有点类似缓存,由于无法作废已颁布的令牌,在其过期前,你只能忍受“过期”的数据。

我的使用姿势:

  • 用户登录进来,会生产两个 token (一个过期时间比较短的 access_token ,一个过期时间比较长的 refresh_token ),创建一个拦截器拦截用户请求。
  • 当要更新jwt携带的数据时候,直接用refresh_token 刷新 access_token,而老的access_token 用redis 标记起来并设置过期时间(过期时间为该令牌剩余的过期时间)
  • 当要作废令牌的时候,直接把这个令牌在redis 标记起来,并且设置过期时间(过期时间为该令牌剩余的过期时间)。

JWT 工具类封装

我们在日常开发中会多次去验证客户端传入的 token,所以我们要把验证的方法抽出来,封装成一个工具类,每次直接用工具类调用就可以了

首先创建一个 JwtTokenUtil
@Slf4j
public class JwtTokenUtil {
}
application.yml 加入 JWT 相关配置参数
#JWT 密钥
jwt: secretKey: 78944878877848fg)
 accessTokenExpireTime: PT2H
 refreshTokenExpireTime: PT8H
 refreshTokenExpireAppTime: P30D
 issuer: ggk.com
创建配置读取类
@Configuration
@ConfigurationProperties(prefix = "jwt")
@Data
public class TokenSettings {
    private String secretKey;
    private Duration accessTokenExpireTime;
    private Duration refreshTokenExpireTime;
    private Duration refreshTokenExpireAppTime;
    private String  issuer; 
}
创建初始化配置代理类
@Component
public class InitializerUtil {
    private TokenSettings tokenSettings;
    public InitializerUtil(TokenSettings tokenSettings) {
        JwtTokenUtil.setTokenSettings(tokenSettings);
   }
}
修改JwtTokenUtil加入签发 token 方法
@Slf4j
public class JwtTokenUtil {
    private static String secretKey;
    private static Duration accessTokenExpireTime;
    private static Duration refreshTokenExpireTime;
    private static Duration refreshTokenExpireAppTime;
    private static String issuer;

    public static void setTokenSettings(TokenSettings tokenSettings){
        secretKey=tokenSettings.getSecretKey();
        accessTokenExpireTime=tokenSettings.getAccessTokenExpireTime();
        refreshTokenExpireTime=tokenSettings.getRefreshTokenExpireTime();
        refreshTokenExpireAppTime=tokenSettings.getRefreshTokenExpireAppTime();
        issuer=tokenSettings.getIssuer();
    }

    /**
     * 签发/生成token
     * issuer 签发人
     * subject 代表这个JWT的主体,即他的所有人,一般是用户ID
     * claims 储存在jwt里的信息(键值对),一般是放些用户的权限/角色信息
     * ttlMillis 有效时间(毫秒)
     * secret 密钥
     */
    public static String generateToken(String issuer, String subject, Map<String, Object> claims, long ttlMillis, String secret) {
        //加密方式
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //当前时间戳,并转为日期
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        //String printBase64Binary(byte[])就是将字节数组做base64编码,byte[] parseBase64Binary(String) 就是将Base64编码后的String还原成字节数组。
        byte[] signingKey = DatatypeConverter.parseBase64Binary(secret);
        //这里其实就是new一个JwtBuilder,设置jwt的body
        JwtBuilder builder = Jwts.builder();
        //如果claims不为空,就加到JWT的载荷里面去
        if(null!=claims){
            builder.setClaims(claims);
        }
        if (!StringUtils.isEmpty(subject)) {
            builder.setSubject(subject);
        }
        if (!StringUtils.isEmpty(issuer)) {
            builder.setIssuer(issuer);
        }
        //签发时间
        builder.setIssuedAt(now);
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            //过期时间
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }
        builder.signWith(signatureAlgorithm, signingKey);
        return builder.compact();
    }

    // 刷新token和业务token 只是过期时间不一样

    /**
     * 生成 access_token   正常请求资源时携带的凭证
     */
    public static String getAccessToken(String subject, Map<String,Object> claims){

        return generateToken(issuer,subject,claims,accessTokenExpireTime.toMillis(),secretKey);
    }

    /**
     * 生产 PC refresh_token
     */
    public static String getRefreshToken(String subject,Map<String,Object> claims){
        return generateToken(issuer,subject,claims,refreshTokenExpireTime.toMillis(),secretKey);
    }

    /**
     * 生产 App端 refresh_token
     */
    public static String getRefreshAppToken(String subject,Map<String,Object> claims){
        return generateToken(issuer,subject,claims,refreshTokenExpireAppTime.toMillis(),secretKey);
    }

    /**
     * 解析令牌 获取数据声明
     * 拿到用户及用户的角色、权限等信息
     */
    public static Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            //用密钥(必字节数组)解析jwt,获取body(有效载荷)
            claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(secretKey)).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            //解析不了,这个token就是无效的
            claims = null;
        }
        return claims;
    }

    /**
     * 获取用户id
     */
    public static String getUserId(String token){
        String userId=null;
        try {
            Claims claims = getClaimsFromToken(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            log.error("error={}",e);
        }
        return userId;
    }

    /**
     * 获取用户名
     * 用于首页展示
     */
    public static String getUserName(String token){
        String username=null;
        try {
            //解析token获取claims
            Claims claims = getClaimsFromToken(token);
            //claims中的key当作自定义的常量
            username = (String) claims .get(Constant.JWT_USER_NAME);
        } catch (Exception e) {
            log.error("error={}",e);
        }
        return username;
    }

    /**
     * 验证token 是否过期
     */
    public static Boolean isTokenExpired(String token) {
        try {
            //首先解析,如果能解析成功,证明我服务器签发的
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            //过期时间和当前时间比较,如果过期时间在当前时间之前,返回true,表示已过期;否则返回false,没过期
            return expiration.before(new Date());
        } catch (Exception e) {
            log.error("error={}",e);
            //解析失败,抛出异常,返回true,表示已过期
            return true;
        }
    }
    /**
     * 校验令牌
     */
    public static Boolean validateToken(String token) {
        Claims claimsFromToken = getClaimsFromToken(token);
        return (null!=claimsFromToken && !isTokenExpired(token));
    }

    /**
     * 刷新token
     * 如果是过期刷新,claims/载荷 不变;
     * 如果主动刷新,claims/载荷 改变【一般是权限/角色改变的时候去主动刷新】
     */
    public static String refreshToken(String refreshToken,Map<String, Object> claims) {
        String refreshedToken;
        try {
            Claims parserclaims = getClaimsFromToken(refreshToken);
            /**
             * 如果传入的claims为空,说明是过期刷新,原先的用户信息不变,claims引用上个token里的内容
             */
            if(null==claims){
                claims=parserclaims;
            }
            /**
             * 不为空,根据传入的claims【用户信息】,生成新的Token
             */
            refreshedToken = generateToken(parserclaims.getIssuer(),parserclaims.getSubject(),claims,accessTokenExpireTime.toMillis(),secretKey);
        } catch (Exception e) {
            refreshedToken = null;
            log.error("error={}",e);
        }
        return refreshedToken;
    }

    /**
     * 获取token的剩余过期时间
     */
    public static long getRemainingTime(String token){
        long result=0;
        try {
            long nowMillis = System.currentTimeMillis();
            result= getClaimsFromToken(token).getExpiration().getTime()-nowMillis;
        } catch (Exception e) {
            log.error("error={}",e);
        }
        return result;
    }
}

SpringBoot+shiro+jwt 实现用户认证签发 token

首先用户登录进来,我们先验证用户名/密码,验证通过后我们会生成两个 token(access_token、refresh_token 他们的唯一区别是一个过期时间短一个过期时间长)然后把一些必要的参数封装成 LoginRespVO 响应回客户端。token 主要包含 用户id、用户登录名、用户所拥有的角色(这里我们先写 mock 数据)、用户所拥有的权限(这里我们先写 mock 数据)、和签发单位标识。

代码说明:

  • LoginReqVO - 接收客户端表单提交数据
  • LoginRespVO - 响应客户端数据
  • UserController.java – 控制层
  • UserService.java & UserServiceImpl.java – 服务层
  • UserMapper.java & UserMapper.xml – 数据访问层
  • PasswordEncoder & PasswordUtils - 密码校验工具类

插入数据脚本

INSERT INTO `sys_user` (`id`, `username`, `salt`, `password`, `phone`, `dept_id`, `real_name`,
`nick_name`, `email`, `status`, `sex`, `deleted`, `create_id`, `update_id`, `create_where`,
`create_time`, `update_time`) VALUES ('9a26f5f1-cbd2-473d-82db-1d6dcf4598f8', 'admin', 
'324ce32d86224b00a02b', 'ac7e435db19997a46e3b390e69cb148b', '13888888888', '24f41c71-5a95-4ef4-9493-
174574f3b0c5', NULL, NULL, 'yingxue@163.com', '1', NULL, '1', NULL, NULL, '3', '2019-09-22 19:38:05', 
NULL);
INSERT INTO `sys_user` (`id`, `username`, `salt`, `password`, `phone`, `dept_id`, `real_name`,
`nick_name`, `email`, `status`, `sex`, `deleted`, `create_id`, `update_id`, `create_where`,
`create_time`, `update_time`) VALUES ('9a26f5f1-cbd2-473d-82db-1d6dcf4598f4', 'dev123', 
'324ce32d86224b00a02b', 'ac7e435db19997a46e3b390e69cb148b', '13666666666', '24f41c71-5a95-4ef4-9493-
174574f3b0c5', NULL, NULL, 'yingxue@163.com', '1', NULL, '1', NULL, NULL, '3', '2019-09-22 19:38:05', 
NULL);
LoginReqVO
@Data
public class LoginReqVO {
    @ApiModelProperty(value = "账号")
    private String username;
    @ApiModelProperty(value = "用户密码")
    private String password;
    @ApiModelProperty(value = "登录类型(1:pc;2:App)")
    @NotBlank(message = "登录类型不能为空")
    private String type;
}
LoginRespVO
@Data
public class LoginRespVO {
    @ApiModelProperty(value = "token")
    private String accessToken;
    @ApiModelProperty(value = "刷新token")
    private String refreshToken;
    @ApiModelProperty(value = "用户名")
    private String username;
    @ApiModelProperty(value = "用户id")
    private String id;
    @ApiModelProperty(value = "电话")
    private String phone; 
}
UserService 接口
public interface UserService {
    /**
     * 用户登录接口
     */
    LoginRespVO login(LoginReqVO vo);
}
UserService实现类
@Service
@Slf4j
public class UserServiceImpl implements UserService {   
/**
     * 获取用户的角色
     * 这里先用伪代码代替
     * 后面我们讲到权限管理系统后 再从 DB 读取
     */
    private List<String> getRolesByUserId(String userId){
        List<String> list=new ArrayList<>();
        if("9a26f5f1-cbd2-473d-82db-1d6dcf4598f8".equals(userId)){
            list.add("admin");
        }else{
            list.add("test");
        }
        return  list;
     }
}

实现具体登录业务

image.png

新增密码校验工具类 PasswordEncoder、PasswordUtils

/**
*  密码加密,匹配
*/
public class PasswordEncoder {

    private final static String[] hexDigits = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d",
            "e", "f" };

    private final static String MD5 = "MD5";
    private final static String SHA = "SHA";
    
    private Object salt;
    private String algorithm;

    public PasswordEncoder(Object salt) {
        this(salt, MD5);
    }
    
    public PasswordEncoder(Object salt, String algorithm) {
        this.salt = salt;
        this.algorithm = algorithm;
    }

    /**
     * 密码加密
     */
    public String encode(String rawPass) {
        String result = null;
        try {
            MessageDigest md = MessageDigest.getInstance(algorithm);
            // 加密后的字符串
            result = byteArrayToHexString(md.digest(mergePasswordAndSalt(rawPass).getBytes("utf-8")));
        } catch (Exception ex) {
        }
        return result;
    }

    /**
     * 密码匹配验证
     * @param encPass 密文
     * @param rawPass 明文
     * @return
     */
    public boolean matches(String encPass, String rawPass) {
        String pass1 = "" + encPass;
        String pass2 = encode(rawPass);

        return pass1.equals(pass2);
    }

    private String mergePasswordAndSalt(String password) {
        if (password == null) {
            password = "";
        }

        if ((salt == null) || "".equals(salt)) {
            return password;
        } else {
            return password + "{" + salt.toString() + "}";
        }
    }

    /**
     * 转换字节数组为16进制字串
     * 
     * @param b
     *            字节数组
     * @return 16进制字串
     */
    private String byteArrayToHexString(byte[] b) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++) {
            resultSb.append(byteToHexString(b[i]));
        }
        return resultSb.toString();
    }

    /**
     * 将字节转换为16进制
     * @param b
     * @return
     */
    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n = 256 + n;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    public static void main(String[] args) {
        String a=null;
        String b=null;

        if(a!=b||!a.equals(b)){
            System.out.println(a==b);
        }
    }
}
/**
* @ClassName:       PasswordUtils
*                   密码工具类
*/
public class PasswordUtils {

    /**
     * 匹配密码
     * @param salt 盐
     * @param rawPass 明文 
     * @param encPass 密文
     * @return
     */
    public static boolean matches(String salt, String rawPass, String encPass) {
        return new PasswordEncoder(salt).matches(encPass, rawPass);
    }
    
    /**
     * 明文密码加密
     * @param rawPass 明文
     * @param salt
     * @return
     */
    public static String encode(String rawPass, String salt) {
        return new PasswordEncoder(salt).encode(rawPass);
    }

    /**
     * 获取加密盐
     * @return
     */
    public static String getSalt() {
        return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 20);
    }
}
修改 UserServiceImpl.java 加入如下代码
  @Override
      public LoginRespVO login(LoginReqVO vo) {
          SysUser sysUser=sysUserMapper.getUserInfoByName(vo.getUsername());
          if (null==sysUser){
              throw new BusinessException(BaseResponseCode.NOT_ACCOUNT);
         }
          if (sysUser.getStatus()==2){
              throw new BusinessException(BaseResponseCode.USER_LOCK);
         }
          if(!PasswordUtils.matches(sysUser.getSalt(),vo.getPassword(),sysUser.getPassword())){
              throw new BusinessException(BaseResponseCode.PASSWORD_ERROR);
         }
          LoginRespVO respVO=new LoginRespVO();
          BeanUtils.copyProperties(sysUser,respVO);
          Map<String,Object> claims=new HashMap<>();
          claims.put(Constant.JWT_PERMISSIONS_KEY,getPermissionsByUserId(sysUser.getId()));
          claims.put(Constant.JWT_ROLES_KEY,getRolesByUserId(sysUser.getId()));
          claims.put(Constant.JWT_USER_NAME,sysUser.getUsername());
          String access_token=JwtTokenUtil.getAccessToken(sysUser.getId(),claims);
          String refresh_token;
          if(vo.getType().equals("1")){
              refresh_token=JwtTokenUtil.getRefreshToken(sysUser.getId(),claims);
         }else {
              refresh_token=JwtTokenUtil.getRefreshAppToken(sysUser.getId(),claims);
         }
          respVO.setAccessToken(access_token);
          respVO.setRefreshToken(refresh_token);
          return respVO;
     }

实现登出业务

创建 登出接口

    /**
     * 退出登录
     */
    void logout(String accessToken,String refreshToken);

Contants 加入几个静态常量

    /**
     * refresh_token 主动退出后加入黑名单 key
     */
    public static final String JWT_REFRESH_TOKEN_BLACKLIST="jwt-refresh-token-blacklist_";
    /**
     * access_token 主动退出后加入黑名单 key
     */
    public static final String JWT_ACCESS_TOKEN_BLACKLIST="jwt-access-token-blacklist_";

实现登出的具体业务

首先要调用 subject.logout(); 这个主要是清空 shiro 的一些缓存信息,然后就是我们系统具体业务了 把 access_token 加入黑名单、refresh_token 加入黑名单。

@Override
    public void logout(String accessToken, String refreshToken) {
        if(StringUtils.isEmpty(accessToken)||StringUtils.isEmpty(refreshToken)){
            throw new BusinessException(BaseResponseCode.DATA_ERROR);
       }
        Subject subject = SecurityUtils.getSubject();
        log.info("subject.getPrincipals()={}",subject.getPrincipals());
        if (subject.isAuthenticated()) {
            subject.logout();
       }
        String userId=JwtTokenUtil.getUserId(accessToken);
        /**
         * 把token 加入黑名单 禁止再登录
         */
       redisService.set(Constant.JWT_ACCESS_TOKEN_BLACKLIST+accessToken,userId,JwtTokenUtil.getRemainingTime(accessToken),TimeUnit.MILLISECONDS);
        /**
         * 把 refreshToken 加入黑名单 禁止再拿来刷新token
         */
         redisService.set(Constant.JWT_REFRESH_TOKEN_BLACKLIST+refreshToken,userId,JwtTokenUtil.getRemainingTime(refreshToken),TimeUnit.MILLISECONDS);
   }

创建业务接口

创建 登录接口

@RestController
@Api(tags = "组织模块-用户管理")
@RequestMapping("/api")
public class UserController {
    @Autowired
    private UserService userService;
    @PostMapping("/user/login")
    @ApiOperation(value = "用户登录接口")
    public DataResult<LoginRespVO> login(@RequestBody LoginReqVO vo){
        DataResult<LoginRespVO> result=DataResult.success();
        result.setData(userService.login(vo));
        return result;
   }
}

静态 Constant 类新增两个静态常量

    /**
     * 正常token
     */
    public static final String ACCESS_TOKEN="authorization";
    /**
     * 刷新token
     */
    public static final String REFRESH_TOKEN="refresh_token";

创建 用户登出接口

    @GetMapping("/user/logout")
    @ApiOperation(value = "用户登出接口")
    public DataResult logout(HttpServletRequest request){
        try {
            String accessToken=request.getHeader(Constant.ACCESS_TOKEN);
            String refreshToken=request.getHeader(Constant.REFRESH_TOKEN);
            userService.logout(accessToken,refreshToken);
       } catch (Exception e) {
            log.error("logout error{}",e);
       }
        return DataResult.success();
   }

创建引导客户端去登录接口

    @GetMapping("/user/unLogin")
    @ApiOperation(value = "引导客户端去登录")
    public DataResult unLogin(){
        DataResult result= DataResult.getResult(BaseResponseCode.TOKEN_ERROR);
        return result;
   }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349