JWT+Redis 实现接口 Token 校验

1、业务逻辑

有一些接口,需要用户登录以后才能访问,用户没有登录则无法访问。 因此,对于一些限制用户访问的接口,可以在请求头中增加一个校验参数,用于判断接口对应的用户是否登录。 而对于一些不需要登录即可访问的接口,则需要进行放行操作。

2、代码逻辑
  1. 用户访问登录接口,校验通过则生成token标识,并且将token存入缓存;
  2. 请求其它接口时,在请求头中添加token参数;
  3. 后台收到请求后,根据接口路径的匹配程度决定是否对此次请求进行token校验;
  4. 需要校验的请求,会在到达controller层之前被拦截,进行对应的逻辑判断和校验;
  5. 校验通过的请求会顺利到达controller层,不通过则立即将校验失败的结果返回。
3、代码具体实现
(1)生成token

采用JWT方式生成token,登录成功后将用户的部分信息加密后生成token,并且保存到redis中。


  com.auth0
  java-jwt
  3.14.0
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.mashibing.internalcommon.dto.TokenResult;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTUtils {

    // JWT签名
    private static final String SIGN = "i_am_study"; 

    // 用户信息
    private static final String JWT_KEY_PHONE = "phone";
    private static final String JWT_KEY_IDENTITY = "identity";
    private static final String TOKEN_TYPE = "tokenType";
    private static final String TOKEN_TIME = "tokenTime";

    /**
     * 根据用户信息生成JWT
     * @param passengerPhone 手机号
     * @param identity 用户身份类型标识
     * @param tokenType  token类型标识
     * @return
     */
    public static String generatorToken(String passengerPhone, String identity, String tokenType){
        Map map = new HashMap<>();
        map.put(JWT_KEY_PHONE, passengerPhone);
        map.put(JWT_KEY_IDENTITY, identity);
        map.put(TOKEN_TYPE, tokenType);
        map.put(TOKEN_TIME, Calendar.getInstance().getTime().toString());
        // 整合 map
        JWTCreator.Builder builder = JWT.create();
        map.forEach(
                (k, v) -> {
                    builder.withClaim(k, v);
                }
        );
        String sign = builder.sign(Algorithm.HMAC256(SIGN));
        return sign;
    }

    /**
     * 解析JWT获取用户信息
     * @param token JWT类型字符串
     * @return
     */
    public static TokenResult paseToken(String token) {
        DecodedJWT verify = JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
        String phone = verify.getClaim(JWT_KEY_PHONE).asString();
        String identity = verify.getClaim(JWT_KEY_IDENTITY).asString();
        TokenResult tokenResult = new TokenResult();
        tokenResult.setIdentity(identity);
        tokenResult.setPhone(phone);
        return tokenResult;
    }

    /**
     * 验证JWT合法性 
     * @param token JWT字符串
     * @return
     */
    public static TokenResult checkToken(String token) {
        TokenResult tokenResult = null;
        try {
             tokenResult = JWTUtils.paseToken(token);
        } catch (Exception e) {
        }
        return tokenResult;
    }

}
(2)Redis获取token

用户登录采用的是手机号+验证码的方式,根据手机号从Redis中拿到验证码,并与用户输入的验证码进行比较,如果相等则返回双token。

/**
 * 用户根据手机号和验证码登录 
 * @param driverPhone  手机号 
 * @param verificationCode  验证码 
 * @return
 */
public ResponseResult checkVerficationCode(String driverPhone, String verificationCode) {
    // 1.根据手机号获取验证码
    String key = RedisPrefixUtils.generatorKeyByPhone(driverPhone, IdentityConstant.DRIVER_IDENTITY);
    String codeRedis = redisTemplate.opsForValue().get(key);
    System.out.println("从redis获取的验证码是: " + codeRedis);
    // 2.校验验证码
    if (StringUtil.isNullOrEmpty(codeRedis)) {
        return ResponseResult.fail(CommonStatusEnum.VERIFICATON_CODE_EXPIRE.getCode(),CommonStatusEnum.VERIFICATON_CODE_EXPIRE.getValue());
    }
    if (!verificationCode.trim().equals(codeRedis)){
        return ResponseResult.fail(CommonStatusEnum.VERIFICATION_CODE_ERRO.getCode(),CommonStatusEnum.VERIFICATION_CODE_ERRO.getValue());
    }
    // 4.生成token返回
    String accessToken = JWTUtils.generatorToken(driverPhone, IdentityConstant.DRIVER_IDENTITY, TokenConstant.ACCESS_TOKEN_TYPE);
    String refreshToken = JWTUtils.generatorToken(driverPhone, IdentityConstant.DRIVER_IDENTITY, TokenConstant.REFRESH_TOKEN_TYPE);
    // 生成key
    String accessTokenKey = RedisPrefixUtils.generatorTokenKey(driverPhone, IdentityConstant.DRIVER_IDENTITY, TokenConstant.ACCESS_TOKEN_TYPE);
    String refreshTokenKey = RedisPrefixUtils.generatorTokenKey(driverPhone, IdentityConstant.DRIVER_IDENTITY, TokenConstant.REFRESH_TOKEN_TYPE);
    // 存到redis
    redisTemplate.opsForValue().set(accessTokenKey, accessToken,  30, TimeUnit.DAYS);
    redisTemplate.opsForValue().set(refreshTokenKey, refreshToken,  31, TimeUnit.DAYS);
    // 结果返回
    TokenResponse tokenResponse = new TokenResponse();
    tokenResponse.setAcccessToken(accessToken);
    tokenResponse.setRefreshToken(refreshToken);
    return ResponseResult.success(tokenResponse);
}
(3)校验token

重写HandlerInterceptor接口的preHandle方法,拿到token进行判断 。 只需要从请求头中拿到token,然后根据token拿到对应的存储到redis中的key,根据key查询redis判断是否存在对应token,存在则放行,不存在则不放行。

  • 在拦截器中编写token校验逻辑
import net.sf.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

public class JwtInterceptor implements HandlerInterceptor {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("Authorization");
        boolean result = true;
        String resultString = "";
        // 解析token,校验token的准确性 
        TokenResult tokenResult = JWTUtils.checkToken(token);
        if(tokenResult == null){
            resultString = "token valid";
            result = false;
        }else {
            // 解析token,获取token中携带的参数信息 
            tokenResult = JWTUtils.paseToken(token);
            // 判断redis中是否存在对应token,即判断token是否到期 
            String tokenKey = RedisPrefixUtils.generatorTokenKey(tokenResult.getPhone(), IdentityConstant.DRIVER_IDENTITY, TokenConstant.ACCESS_TOKEN_TYPE);
            String redisToken = stringRedisTemplate.opsForValue().get(tokenKey);
            if (redisToken==null ||  !redisToken.trim().equals(token)){
                resultString = "token valid";
                result = false;
            }
        }
        if (!result) {
            // 告知前端
            PrintWriter out = response.getWriter();
            out.print(JSONObject.fromObject(ResponseResult.fail(resultString)).toString());
        }
        return result;
    }
}
  • 将重写之后的拦截器加入到spring容器中
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// 将拦截器注册
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Bean
    public JwtInterceptor jwtInterceptor(){
        return new JwtInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor())
                // 拦截的路径
                .addPathPatterns("/**")
                // 不拦截的路径
                .excludePathPatterns(
                        "/noauth",
                        "/error",
                        "/verification-code",
                        "/verification-code-check"
                );
    }
    
}

你可能感兴趣的:(spring,java,后端)