Skip to content

JWT令牌的生成与校验

项目地址:misszb(该项目里实现的是微信小程序登录获取token)

项目地址:misscmszb(该项目实现了普通登录后获取token)

概念

token就是一个将信息加密之后的密文,而jwt也是token的实现方式之一,用于服务器端进行身份验证和授权访问控制。

jwt由三部分组成:

  • 1、Header(标头),一般用于指明token的类型和加密算法

  • 2.PayLoad(载荷),存储token有效时间及各种自定义信息,如用户名,id、发行者等

  • 3.Signature(签名),是用标头提到的算法对前两部分进行加密,在签名认证时,防止止信息被修改

常用方法:

JWTCreator.Builder jwtBuilder = JWT.create() // 获取jwt生成器

JWT.require("传入算法").build // 生成jwt校验对象

// 生成token
String token = jwtBuilder.withHeader(headers)
 
                //接下来为设置PayLoad,Claim中的键值对可自定义
                //设置用户名
                .withClaim("username", USERNAME) 
                   
                //是否为VIP用户
                .withClaim("isVIP", true)
                
                //设置用户id
                .withClaim("userId", 123)
 
                //token失效时间,这里为一天后失效
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))
                //设置该jwt的发行时间,一般为当前系统时间
                .withIssuedAt(new Date(System.currentTimeMillis()))
 
                //token的发行者(可自定义)
                .withIssuer("issuer")
 
                //进行签名,选择加密算法,以一个字符串密钥为参数
                .sign(Algorithm.HMAC256(KEY));

JWT令牌的生成

token.controller

通过LoginType判断是微信小程序获取token还是普通的登录获取token,当前实现的是微信小程序获取token

java
@PostMapping("")
    public Map<String, String> getToken(@RequestBody @Validated TokenGetDTO userData) {
        Map<String, String> map = new HashMap<>();
        String token = null;
        System.out.println(userData.getLoginType());
        switch (userData.getLoginType()) {
            case USER_WX:
                token = wxAuthenticationService.code2Session(userData.getAccount());
                break;
            case USER_Email:
                break;
            default:
                throw new NotFoundException(10003);
        }
        map.put("token", token);
        return map;
    }

备注

为什么要要返回map,因为map返回到前端是对象形式,好取值

TokenGetDTO.java

java
package com.zb.misszb.dto;

import com.zb.misszb.core.enumeration.LoginType;
import com.zb.misszb.dto.validators.TokenPassword;
import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotBlank;

@Getter
@Setter
public class TokenGetDTO {
    @NotBlank(message = "account不允许为空")
    private String account;
    @TokenPassword(min=5, max=30, message = "{token.password}")
    private String password;

    private LoginType loginType;
}

说明:

account code值或账号

password 密码 微信小程序端不用传

loginType 登录方式 微信小程序端传0,其他端传1

loginType.java

java
package com.zb.misszb.core.enumeration;

public enum  LoginType {
    USER_WX(0, "微信登录"),
    USER_Email(1, "邮箱登录");

    private Integer value;

    LoginType(Integer value, String description) {
        this.value = value;
    }
}

wxAuthenticationService.java

功能:

1、访问微信服务器,通过code换取openid和uuid

2、通过openid查询user:如果存在,就通过user的id生成jwt;如果不存在,就先把user保存到数据库里,再通过user的id生成jwt

java
package com.zb.misszb.service.authentication.impl;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zb.misszb.exception.http.ParameterException;
import com.zb.misszb.model.User;
import com.zb.misszb.repository.UserRepository;
import com.zb.misszb.service.authentication.WxAuthenticationService;
import com.zb.misszb.util.JwtToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Service
public class WxAuthenticationServiceImpl implements WxAuthenticationService {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private UserRepository userRepository;

    @Value("${wx.code2session}")
    private String code2SessionUrl;
    @Value("${wx.appid}")
    private String appid;
    @Value("${wx.appSecret}")
    private String appSecret;

    @Override
    public String code2Session(String code) {
        String url = MessageFormat.format(this.code2SessionUrl, this.appid, this.appSecret, code);
        RestTemplate rest = new RestTemplate();
        String sessionText = rest.getForObject(url, String.class);
        Map<String, Object> session = new HashMap<>();
        try {
            session = objectMapper.readValue(sessionText, HashMap.class);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        return this.registerUser(session);
    }

    private String registerUser(Map<String, Object> session) {
        String openid = (String) session.get("openid");
        if (openid == null) {
            throw new ParameterException(20004);
        }
        Optional<User> userOptional = userRepository.findByOpenid(openid);
        if (userOptional.isPresent()) {
            System.out.println(userOptional.get().getId());
            // TODO:返回JWT令牌
            return JwtToken.makeToken(userOptional.get().getId());
        }
        User user = User.builder()
                .openid(openid)
                .build();
        userRepository.save(user);
        // TODO:返回JWT令牌
        Long uid = user.getId();
        return JwtToken.makeToken(uid);
    }
}

pom.xml

xml
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.8.1</version>
</dependency>

JwtToken.java

包含具体生成jwt的方法

java
package com.zb.misszb.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.*;

@Component
public class JwtToken {

    // jwt加密使用的Key
    private static String jwtKey;

    // jwt过期时间
    private static Integer expiredTimeIn;

    private static Integer defaultScope = 8;

    // 将配置文件里的值注入并赋值给jwtKey
    @Value("${misszb.security.jwt-key}")
    public void setJwtKey(String jwtKey) {
        JwtToken.jwtKey = jwtKey;
    }

    @Value("${misszb.security.token-expired-in}")
    public void setExpiredTimeIn(Integer expiredTimeIn) {
        JwtToken.expiredTimeIn = expiredTimeIn;
    }

    /**
     * 生成token
     * @param uid 用户标识
     * @param scope 用户等级,不同的用户按等级分类,给予不同的token
     * @return
     */
    public static String makeToken(Long uid, Integer scope) {
        return JwtToken.getToken(uid, scope);
    }

    // 当前项目里没有用户等级,这个方法传入默认用户等级
    public static String makeToken(Long uid) {
        return JwtToken.getToken(uid, JwtToken.defaultScope);
    }

    // 生成token
    private static String getToken(Long uid, Integer scope) {
        Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
        Map<String, Date> map = JwtToken.calculateExpiredIssues();

        return JWT.create()
                .withClaim("uid", uid)
                .withClaim("scope", scope)
                .withExpiresAt(map.get("expiredTime"))
                .withIssuedAt(map.get("now"))
                .sign(algorithm);
    }

    // 处理token生成时间和过期时间
    private static Map<String, Date> calculateExpiredIssues() {
        Map<String, Date> map = new HashMap<>();
        // Calendar类为抽象类,不能实例化,想获取对象可以用getInstance方法
        Calendar calendar = Calendar.getInstance();
        // 获取当前时间
        Date now = calendar.getTime();
        // 动态修改当前时间,传入的参数为秒
        calendar.add(Calendar.SECOND, JwtToken.expiredTimeIn);
        map.put("now", now);
        map.put("expiredTime", calendar.getTime());
        return map;
    }

    // 解析token
    public static Optional<Map<String, Claim>> getClaims(String token) {
        DecodedJWT decodedJWT;
        Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        try {
            decodedJWT = jwtVerifier.verify(token);
        } catch (JWTVerificationException e) {
            return Optional.empty();
        }
        return Optional.of(decodedJWT.getClaims());
    }

    // 校验token
    public static Boolean verifyToken(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
            JWTVerifier verifier = JWT.require(algorithm).build();
            verifier.verify(token);
        } catch (JWTVerificationException e) {
            return false;
        }
        return true;
    }
}

令牌生成逻辑

一、微信小程序端:调用wx.login获取code值

js
  onGetToken() {
    // code
    wx.login({
      success: (res) => {
        if (res.code) {
          wx.request({
            url: 'http://localhost:8082/v1/token',
            method: 'POST',
            data: {
              account: res.code,
              loginType: 0
            },
            success: (res) => {
              console.log(res.data)
            }
          })
        }
      }
    })
  }

二、调用获取token接口。微信小程序端:传入code值和loinType;其他端传入账号、密码和loginType

js
  wx.request({
  url: 'http://localhost:8082/v1/token',
  method: 'POST',
  data: {
    account: res.code,
    loginType: 0
  },
  success: (res) => {
    console.log(res.data)
  }
})

说明:

account code值或账号

password 密码 微信小程序端不用传

loginType 登录方式 微信小程序端传0,其他端传1

三、微信小程序端:后端访问微信服务器,调用https://api.weixin.qq.com/sns/jscode2session方法,使用code值换取openid和uuid

java
    @Value("${wx.code2session}")
    private String code2SessionUrl;
    @Value("${wx.appid}")
    private String appid;
    @Value("${wx.appSecret}")
    private String appSecret;
    
    @Override
    public String code2Session(String code) {
        String url = MessageFormat.format(this.code2SessionUrl, this.appid, this.appSecret, code);
        RestTemplate rest = new RestTemplate();
        String sessionText = rest.getForObject(url, String.class);
        Map<String, Object> session = new HashMap<>();
        try {
            session = objectMapper.readValue(sessionText, HashMap.class);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        return this.registerUser(session);
    }

说明:

(一)、替换占位符

MessageFormat.format()根据顺序和占位符进行值的替换

code2SessionUrl为https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code

MessageFormat.format()方法第一个参数是需要被替换的值(String类型),其他参数是按顺序要替换成的值

String url = MessageFormat.format(this.code2SessionUrl, this.appid, this.appSecret, code);生成的是访问微信服务器获取的url

可以获取用户的openid和unionid,文档地址是:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html

(二)、java发起请求

java
  RestTemplate rest = new RestTemplate();
  String sessionText = rest.getForObject(url, String.class);

java要想发起请求,需要使用RestTemplate,掉用getForObject()方法,第一个参数是请求的url,第二个参数是返回的类型的元类

(三)、将字符串转换成HashMap

java
@Autowired
private ObjectMapper objectMapper;
    
Map<String, Object> session = new HashMap<>();
session = objectMapper.readValue(sessionText, HashMap.class);

ObjectMapper是jackson里的方法,objectMapper.readValue()方法将字符串转换成相应格式的对象,第一个参数是需要转换的字符串,第二个参数是转换成的格式

四、微信小程序端:通过openid判断数据库是否有对应的user,如果有就用user的id生成jwt;如果没有:就先保存到user表,再通过user的id生成jwt;其他端是通过账号和密码判断是否有对应的user,如果有就用user的id生成jwt;如果没有:就先保存到user表,再通过user的id生成jwt

java
 private String registerUser(Map<String, Object> session) {
        String openid = (String) session.get("openid");
        if (openid == null) {
            throw new ParameterException(20004);
        }
        Optional<User> userOptional = userRepository.findByOpenid(openid);
        if (userOptional.isPresent()) {
            System.out.println(userOptional.get().getId());
            // TODO:返回JWT令牌
            return JwtToken.makeToken(userOptional.get().getId());
        }
        User user = User.builder()
                .openid(openid)
                .build();
        userRepository.save(user);
        // TODO:返回JWT令牌
        Long uid = user.getId();
        return JwtToken.makeToken(uid);
    }

说明:

(一)、Optional的缺陷

在jsk8里面,Optional的orElseThrow当有值的时候会直接返回数值,没有值的时候可以传入处理函数

如果即想在有值的时候进行处理,又想在无值的时候处理,orElseThrow方法无法满足,这时候可以使用isPresent方法(是否有值)

备注

jdk11里,Optional新加了方法,可以传入俩个函数,处理有值和无值。不过只能进行简单的逻辑处理,如果想处理负责逻辑,还是建议if/else配合isPresent方法使用

(二)、实体数据的赋值和保存

java
User user = User.builder()
                .openid(openid)
                .build();
        userRepository.save(user);
        // TODO:返回JWT令牌
        Long uid = user.getId();

builder创建对象,给这个对象的openid赋值,使用save方法保存后,就可以获取到当前对象的id

五、生成token

java
package com.zb.misszb.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

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

@Component
public class JwtToken {

    // jwt加密使用的Key
    private static String jwtKey;

    // jwt过期时间
    private static Integer expiredTimeIn;

    private static Integer defaultScope = 8;

    // 将配置文件里的值注入并赋值给jwtKey
    @Value("${misszb.security.jwt-key}")
    public void setJwtKey(String jwtKey) {
        JwtToken.jwtKey = jwtKey;
    }

    @Value("${misszb.security.token-expired-in}")
    public void setExpiredTimeIn(Integer expiredTimeIn) {
        JwtToken.expiredTimeIn = expiredTimeIn;
    }

    /**
     * 生成token
     * @param uid 用户标识
     * @param scope 用户等级,不同的用户按等级分类,给予不同的token
     * @return
     */
    public static String makeToken(Long uid, Integer scope) {
        return JwtToken.getToken(uid, scope);
    }

    // 当前项目里没有用户等级,这个方法传入默认用户等级
    public static String makeToken(Long uid) {
        return JwtToken.getToken(uid, JwtToken.defaultScope);
    }

    private static String getToken(Long uid, Integer scope) {
        Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
        Map<String, Date> map = JwtToken.calculateExpiredIssues();

        return JWT.create()
                .withClaim("uid", uid)
                .withClaim("scope", scope)
                .withExpiresAt(map.get("expiredTime"))
                .withIssuedAt(map.get("now"))
                .sign(algorithm);
    }

    private static Map<String, Date> calculateExpiredIssues() {
        Map<String, Date> map = new HashMap<>();
        // Calendar类为抽象类,不能实例化,想获取对象可以用getInstance方法
        Calendar calendar = Calendar.getInstance();
        // 获取当前时间
        Date now = calendar.getTime();
        // 动态修改当前时间,传入的参数为秒
        calendar.add(Calendar.SECOND, JwtToken.expiredTimeIn);
        map.put("now", now);
        map.put("expiredTime", calendar.getTime());
        return map;
    }
}

说明:

(一)、加密生成签名

java
import com.auth0.jwt.algorithms.Algorithm;

Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);

使用Algorithm的HMAC256方法进行加密,参数是自己定义的一段值

(二)、使用Calendar处理时间

java
   private static Map<String, Date> calculateExpiredIssues() {
        Map<String, Date> map = new HashMap<>();
        // Calendar类为抽象类,不能实例化,想获取对象可以用getInstance方法
        Calendar calendar = Calendar.getInstance();
        // 获取当前时间
        Date now = calendar.getTime();
        // 动态修改当前时间,传入的参数为秒
        calendar.add(Calendar.SECOND, JwtToken.expiredTimeIn);
        map.put("now", now);
        map.put("expiredTime", calendar.getTime());
        return map;
    }

(二)、JWT赋值

java
JWT.create()
   .withClaim("uid", uid)
   .withClaim("scope", scope)
   .withExpiresAt(map.get("expiredTime"))
   .withIssuedAt(map.get("now"))
   .sign(algorithm);

JWT令牌校验

token.controller

java
package com.zb.misszb.api.v1;

import com.zb.misszb.dto.TokenDTO;
import com.zb.misszb.dto.TokenGetDTO;
import com.zb.misszb.exception.http.NotFoundException;
import com.zb.misszb.service.authentication.WxAuthenticationService;
import com.zb.misszb.util.JwtToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

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

@RestController
@RequestMapping(value = "token")
public class TokenController {
    @Autowired
    private WxAuthenticationService wxAuthenticationService;

    @PostMapping("/verify")
    public Map<String, Boolean> verify(@RequestBody TokenDTO token) {
        Map<String, Boolean> map = new HashMap<>();
        Boolean valid = JwtToken.verifyToken(token.getToken());
        map.put("is_valid", valid);
        return map;
    }
}

TokenDTO.java

java
package com.zb.misszb.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class TokenDTO {
    private String token;
}

JwtToken.java

java
package com.zb.misszb.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.*;

@Component
public class JwtToken {

    // jwt加密使用的Key
    private static String jwtKey;

    // jwt过期时间
    private static Integer expiredTimeIn;

    private static Integer defaultScope = 8;

    // 将配置文件里的值注入并赋值给jwtKey
    @Value("${misszb.security.jwt-key}")
    public void setJwtKey(String jwtKey) {
        JwtToken.jwtKey = jwtKey;
    }

    @Value("${misszb.security.token-expired-in}")
    public void setExpiredTimeIn(Integer expiredTimeIn) {
        JwtToken.expiredTimeIn = expiredTimeIn;
    }

    /**
     * 生成token
     * @param uid 用户标识
     * @param scope 用户等级,不同的用户按等级分类,给予不同的token
     * @return
     */
    public static String makeToken(Long uid, Integer scope) {
        return JwtToken.getToken(uid, scope);
    }

    // 当前项目里没有用户等级,这个方法传入默认用户等级
    public static String makeToken(Long uid) {
        return JwtToken.getToken(uid, JwtToken.defaultScope);
    }

    // 生成token
    private static String getToken(Long uid, Integer scope) {
        Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
        Map<String, Date> map = JwtToken.calculateExpiredIssues();

        return JWT.create()
                .withClaim("uid", uid)
                .withClaim("scope", scope)
                .withExpiresAt(map.get("expiredTime"))
                .withIssuedAt(map.get("now"))
                .sign(algorithm);
    }

    // 处理token生成时间和过期时间
    private static Map<String, Date> calculateExpiredIssues() {
        Map<String, Date> map = new HashMap<>();
        // Calendar类为抽象类,不能实例化,想获取对象可以用getInstance方法
        Calendar calendar = Calendar.getInstance();
        // 获取当前时间
        Date now = calendar.getTime();
        // 动态修改当前时间,传入的参数为秒
        calendar.add(Calendar.SECOND, JwtToken.expiredTimeIn);
        map.put("now", now);
        map.put("expiredTime", calendar.getTime());
        return map;
    }

    // 解析token
    public static Optional<Map<String, Claim>> getClaims(String token) {
        DecodedJWT decodedJWT;
        Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        try {
            decodedJWT = jwtVerifier.verify(token);
        } catch (JWTVerificationException e) {
            return Optional.empty();
        }
        return Optional.of(decodedJWT.getClaims());
    }

    // 校验token
    public static Boolean verifyToken(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
            // 通过密钥进行解密
            JWTVerifier verifier = JWT.require(algorithm).build();
            // 通过 verifier.verify() 方法检验 token,如果token不符合则抛出异常
            verifier.verify(token);
        } catch (JWTVerificationException e) {
            return false;
        }
        return true;
    }
}

令牌校验逻辑

一、校验token

java
    // 校验token
    public static Boolean verifyToken(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
            // 通过密钥进行解密
            JWTVerifier verifier = JWT.require(algorithm).build();
            // 通过 verifier.verify() 方法检验 token,如果token不符合则抛出异常
            verifier.verify(token);
        } catch (JWTVerificationException e) {
            return false;
        }
        return true;
    }

这是固定写法,传入token值就行

拦截器和token校验

添加拦截器,在拦截器里对token进行校验,校验通过就进行API操作,失败的话返回提示信息

InterceptorConfiguration.java

java
package com.zb.misszb.core.configuration;

import com.zb.misszb.core.interceptors.PermissionInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {
    @Bean
    public HandlerInterceptor getPermissionInterceptor() {
        return new PermissionInterceptor();
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.getPermissionInterceptor());
    }
}

备注

可以使用registry.addInterceptor()注册多个拦截器

PermissionInterceptor.java

具体的拦截器逻辑

java
package com.zb.misszb.core.interceptors;

import com.auth0.jwt.interfaces.Claim;
import com.zb.misszb.exception.http.ForbiddenException;
import com.zb.misszb.exception.http.UnAuthenticatedException;
import com.zb.misszb.util.JwtToken;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.Optional;

public class PermissionInterceptor implements HandlerInterceptor {
    // 请求在进入controller之前执行,其返回值表示是否中断后续操作,返回 true 表示继续向下执行,返回 false 表示中断后续操作。(返回true,false)
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Optional<ScopeLevel> scopeLevel = this.getScopeLevel(handler);
        if (!scopeLevel.isPresent()) {
            return true;
        }
        String bearerToken = request.getHeader("Authorization");
        if (!StringUtils.hasLength(bearerToken)) {
            throw new UnAuthenticatedException(10004);
        }
        if (!bearerToken.startsWith("Bearer")) {
            throw new UnAuthenticatedException(10004);
        }
        String tokens[] = bearerToken.split(" ");
        if (!(tokens.length == 2)) {
            throw new UnAuthenticatedException(10004);
        }
        String token = tokens[1];
        Optional<Map<String, Claim>> optionalMap = JwtToken.getClaims(token);
        Map<String, Claim> map = optionalMap
                .orElseThrow(() -> new UnAuthenticatedException(10004));

        boolean valid = this.hasPermission(scopeLevel.get(), map);
        return valid;
    }

    private boolean hasPermission(ScopeLevel scopeLevel, Map<String, Claim> map) {
        Integer level = scopeLevel.value();
        Integer scope = map.get("scope").asInt();
        if (level > scope) {
            throw new ForbiddenException(10005);
        }
        return true;
    }

    // 该方法在控制器处理请求方法调用之后、解析视图之前执行,可以通过此方法对请求域中的模型和视图做进一步修改。(无返回值)
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {

    }

    // 该方法在视图渲染结束后执行,可以通过此方法实现资源清理、记录日志信息等工作。(无返回值)
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {

    }

    // 获取ScopeLevel注解里的值
    private Optional<ScopeLevel> getScopeLevel(Object handler) {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            ScopeLevel scopeLevel = handlerMethod.getMethod().getAnnotation(ScopeLevel.class);
            if (scopeLevel == null) {
                return Optional.empty();
            }
            return Optional.of(scopeLevel);
        }
        return Optional.empty();
    }
}

说明:

一、定义拦截器,可以实现HandlerInterceptor接口,重写里面的preHandle、postHandle和afterCompletion方法

二、preHandle方法:请求在进入controller之前执行,其返回值表示是否中断后续操作,返回 true 表示继续向下执行,返回 false 表示中断后续操作。(返回true,false)

postHandle方法:该方法在控制器处理请求方法调用之后、解析视图之前执行,可以通过此方法对请求域中的模型和视图做进一步修改。(无返回值)

afterCompletion方法:该方法在视图渲染结束后执行,可以通过此方法实现资源清理、记录日志信息等工作。(无返回值)

ScopeLevel.java

java
package com.zb.misszb.core.interceptors;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface ScopeLevel {
    int value() default 4;
}

备注

自定义的ScopeLevel注解,用来标识API接口的访问等级,只有当前用户的scope大于等于level才能进行访问

JwtToken.java

java
package com.zb.misszb.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.*;

@Component
public class JwtToken {

    // jwt加密使用的Key
    private static String jwtKey;

    // jwt过期时间
    private static Integer expiredTimeIn;

    private static Integer defaultScope = 8;

    // 将配置文件里的值注入并赋值给jwtKey
    @Value("${misszb.security.jwt-key}")
    public void setJwtKey(String jwtKey) {
        JwtToken.jwtKey = jwtKey;
    }

    @Value("${misszb.security.token-expired-in}")
    public void setExpiredTimeIn(Integer expiredTimeIn) {
        JwtToken.expiredTimeIn = expiredTimeIn;
    }

    /**
     * 生成token
     * @param uid 用户标识
     * @param scope 用户等级,不同的用户按等级分类,给予不同的token
     * @return
     */
    public static String makeToken(Long uid, Integer scope) {
        return JwtToken.getToken(uid, scope);
    }

    // 当前项目里没有用户等级,这个方法传入默认用户等级
    public static String makeToken(Long uid) {
        return JwtToken.getToken(uid, JwtToken.defaultScope);
    }

    // 生成token
    private static String getToken(Long uid, Integer scope) {
        Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
        Map<String, Date> map = JwtToken.calculateExpiredIssues();

        return JWT.create()
                .withClaim("uid", uid)
                .withClaim("scope", scope)
                .withExpiresAt(map.get("expiredTime"))
                .withIssuedAt(map.get("now"))
                .sign(algorithm);
    }

    // 处理token生成时间和过期时间
    private static Map<String, Date> calculateExpiredIssues() {
        Map<String, Date> map = new HashMap<>();
        // Calendar类为抽象类,不能实例化,想获取对象可以用getInstance方法
        Calendar calendar = Calendar.getInstance();
        // 获取当前时间
        Date now = calendar.getTime();
        // 动态修改当前时间,传入的参数为秒
        calendar.add(Calendar.SECOND, JwtToken.expiredTimeIn);
        map.put("now", now);
        map.put("expiredTime", calendar.getTime());
        return map;
    }

    // 解析token
    public static Optional<Map<String, Claim>> getClaims(String token) {
        DecodedJWT decodedJWT;
        Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        try {
            decodedJWT = jwtVerifier.verify(token);
        } catch (JWTVerificationException e) {
            return Optional.empty();
        }
        return Optional.of(decodedJWT.getClaims());
    }

    // 校验token
    public static Boolean verifyToken(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
            // 通过密钥进行解密
            JWTVerifier verifier = JWT.require(algorithm).build();
            // 通过 verifier.verify() 方法检验 token,如果token不符合则抛出异常
            verifier.verify(token);
        } catch (JWTVerificationException e) {
            return false;
        }
        return true;
    }
}

备注

getClaims方法,获取token里的数据

拦截器笔记

java里常用的拦截器有三种:filter、interceptor、aop

filter依赖于servlet 不依赖spring

备注

servlet是一套标准接口(我们也可以自己实现) tomact是servlet容器 只有实现了servlet容器 才能接收http请求

如果一个项目里这三种都有,是这样的的顺序:filter => interceptor => aop => controller

http 与服务器之间有 request/ response request是按上面的顺序,response是相反的顺序

filer 能不用就不用 尤其是在spring项目里

推荐使用interceptor 简单

aop 粒度小,(可以对一个类,或者类的方法用aop)实现起来麻烦

双令牌机制

单令牌机制的缺陷

比如令牌的过期时间是5个小时,用户在4小时59分的时候操作,才做到一半的时候令牌过期,自动跳转到了登录页面,当用户登录后,忘了之前的操作,尤其是在APP中,会让用户认为APP有问题,进而不再使用。

双令牌机制的原理

用户登录后,返回access_token和refresh_token俩个令牌

access_token是用户用来访问接口,资源的凭证。生存周期短,一般在2小时

refresh_token是用来重新获取access_token的。生命周期长,一般为30天左右

当前端发现access_token过期了,会自动通过 refresh_token 重新获取 access_token,然后再次刷新access_token值和 refresh_token

备注

上面的双令牌机制可以保证用户在不重新登录的情况下一直访问接口

如果需要登录,那只有一种情况,就是用户长时间未操作,refresh_token过期了

备注

小程序端不需要双令牌,因为小程序是嵌在微信里的,它的登录方式是微信提供的,用户登录小程序的时候,不需要手动进行登录

优缺点

优点

  • 无状态性:JWT自身包含了身份验证所需的所有信息,因此服务器不需要存储Session信息,这增加了系统的可用性和伸缩性,并大大减轻了服务端的压力。

  • 数据保存在客户端:JWT将数据保存在客户端,可以分担数据库或服务器的存储压力。

  • 跨语言支持:JWT支持多种语言的实现,便于不同系统间的交互。

  • 支持过期和发布者校验:JWT可以设置过期时间,确保token只在一定时间内有效。

  • 多进程、多服务器集群没有影响,易于扩展

缺点

  • 密钥安全性问题:JWT的生成与解析过程依赖于密钥,密钥的安全性至关重要。如果密钥泄露,系统的安全性将受到威胁

  • 服务端无法管理客户端信息:由于JWT的无状态性,服务端无法主动管理客户端的信息,如用户登出后,如果JWT未过期,用户仍能访问受保护的资源。

  • 数据开销:JWT签名的大小通常比Session ID大很多,如果不有效控制有效载荷中的数据,可能导致网络开销增加

如有转载或 CV 的请标注本站原文地址