快速构建基于JWT的Token认证

一、构建环境

Springboot:1.5.9.RELEASE       JDK版本:1.8.0_11      Maven版本:3.3.9      IDE版本:IntelliJ IDEA 2019.2.4

 二、构建项目

1. 引入maven依赖

<!--引入JWT依赖,由于是基于Java,所以需要的是java-jwt-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>

2.  创建两个注解,拦截器根据是否有该注解判定是否进行token认证。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 接口token验证
 *
 * @author alex
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckToken {
    boolean required() default true;
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 登录验证
 *
 * @author alex
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginToken {
    boolean required() default true;
}

3. 创建JWT操作工具类及token信息加密类,主要实现生成token、解析token、校验token三个功能。

/**
 * JWT工具类
 *
 * @author Alex
 * 参考官网:https://jwt.io/
 * JWT的数据结构为:A.B.C三部分数据,由字符点"."分割成三部分数据
 * A-header头信息
 * B-payload 有效负荷 一般包括:已注册信息(registered claims),公开数据(public claims),私有数据(private claims)
 * C-signature 签名信息 是将header和payload进行加密生成的
 */
public class JwtUtils {

    private static Logger logger = LoggerFactory.getLogger(JwtUtils.class);

    /**
     * 生成JWT字符串
     *
     * @param userId     - 用户编号
     * @param userName   - 用户名
     * @param identities - 客户端信息(变长参数),目前包含浏览器信息,用于客户端拦截器校验,防止跨域非法访问
     *                   格式:A.B.C
     *                   A-header头信息
     *                   B-payload 有效负荷
     *                   C-signature 签名信息 是将header和payload进行加密生成的
     */
    public static String generateJwt(String userId, String userName, String... identities) {
        // 签名算法,选择SHA-256
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        // 获取当前系统时间
        long nowTimeMillis = System.currentTimeMillis();
        Date now = new Date(nowTimeMillis);
        // 将BASE64SECRET常量字符串使用base64解码成字节数组
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(IConstantsFiled.SecretConstant.BASE64SECRET);
        // 使用HmacSHA256签名算法生成一个HS256的签名秘钥Key
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
        // 添加构成JWT的参数
        Map<String, Object> headMap = new HashMap<>(2);
        headMap.put("alg", SignatureAlgorithm.HS256.getValue());
        headMap.put("typ", "JWT");
        JwtBuilder builder = Jwts.builder().setHeader(headMap)
                // 加密后的用户编号
                .claim("userId", AesSecretUtils.encryptToStr(userId, IConstantsFiled.SecretConstant.DATAKEY))
                // 客户名称
                .claim("userName", userName)
                //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
//                .setId(UUID.randomUUID().toString())
                // 客户端浏览器信息
                .claim("userAgent", identities[0])
                // Signature
                .signWith(signatureAlgorithm, signingKey);
        //添加Token过期时间
        if (IConstantsFiled.SecretConstant.EXPIRESSECOND >= 0) {
            long expMillis = nowTimeMillis + IConstantsFiled.SecretConstant.EXPIRESSECOND;
            Date expDate = new Date(expMillis);
            builder.setExpiration(expDate).setNotBefore(now);
        }
        return builder.compact();
    }

    /**
     * 解析JWT
     *
     * @param jsonWebToken - JWT
     * @return Claims对象
     */
    public static Claims parseJwt(String jsonWebToken) {
        Claims claims = null;
        try {
            if (StringUtil.isNotBlank(jsonWebToken)) {
                // 解析jwt
                SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
                byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(IConstantsFiled.SecretConstant.BASE64SECRET);
                Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
                claims = Jwts.parser().setSigningKey(signingKey).parseClaimsJws(jsonWebToken).getBody();
            } else {
                logger.warn("[JWTHelper]-json web token 为空");
            }
        } catch (Exception e) {
            logger.error("[JWTHelper]-JWT解析异常:可能因为token已经超时或非法token");
        }
        return claims;
    }

    /**
     * 校验JWT是否有效
     *
     * @param jsonWebToken - JWT
     * @return 返回json字符串的demo:
     * {"freshToken":"A.B.C","userName":"Judy","userId":"123", "userAgent":"xxxx"}
     * freshToken-刷新后的jwt
     * userName-客户名称
     * userId-客户编号
     * userAgent-客户端浏览器信息
     * @author Alex
     */
    public static String validateJwt(String jsonWebToken) {
        Map<String, Object> retMap = null;
        Claims claims = parseJwt(jsonWebToken);
        if (claims != null) {
            // 解密客户编号
            String decryptUserId = AesSecretUtils.decryptToStr((String) claims.get("userId"), IConstantsFiled.SecretConstant.DATAKEY);
            retMap = new HashMap<>(4);
            // 此处做一个永久的token验证,内定生成一个永久时效的token,注意,仅在开发时保留
            if ("consmation".equals(decryptUserId) && "consmation".equals(claims.get("userName"))) {
                retMap.put("isForever", "true");
            } else {
                retMap.put("isForever", "false");
            }
            // 加密后的客户编号
            retMap.put("userId", decryptUserId);
            // 客户名称
            retMap.put("userName", claims.get("userName"));
            // 客户端浏览器信息
            retMap.put("userAgent", claims.get("userAgent"));
            // 刷新JWT
            retMap.put("freshToken", generateJwt(decryptUserId, (String) claims.get("userName"), (String) claims.get("userAgent")));
        } else {
            logger.warn("[JWTHelper]-JWT解析出claims为空");
        }
        return retMap != null ? JSONObject.toJSONString(retMap) : null;
    }
}
import org.apache.commons.lang3.ArrayUtils;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;

/**
 * AES加密工具类
 *
 * @author alex
 * @date 2019年11月13日14:20:03
 */
public class AesSecretUtils {

    /**
     * 秘钥的大小
     */
    private static final int KEYSIZE = 128;

    /**
     * @param data - 待加密内容
     * @param key  - 加密秘钥
     */
    public static byte[] encrypt(String data, String key) {
        if (StringUtil.isNotBlank(data)) {
            try {
                KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
                // 选择一种固定算法,为了避免不同java实现的不同算法,生成不同的密钥,而导致解密失败
                SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
                random.setSeed(key.getBytes());
                keyGenerator.init(KEYSIZE, random);
                SecretKey secretKey = keyGenerator.generateKey();
                byte[] enCodeFormat = secretKey.getEncoded();
                SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
                // 创建密码器
                Cipher cipher = Cipher.getInstance("AES");
                byte[] byteContent = data.getBytes(StandardCharsets.UTF_8);
                // 初始化
                cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
                return cipher.doFinal(byteContent);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * AES加密
     *
     * @param data - 待加密内容
     * @param key  - 加密秘钥
     * @return 加密后的字符串
     */
    public static String encryptToStr(String data, String key) {

        return StringUtil.isNotBlank(data) ? parseByte2HexStr(encrypt(data, key)) : null;
    }


    /**
     * AES解密
     *
     * @param data - 待解密字节数组
     * @param key  - 秘钥
     * @return 解密后的字节数组
     */
    public static byte[] decrypt(byte[] data, String key) {
        if (ArrayUtils.isNotEmpty(data)) {
            try {
                KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
                //选择一种固定算法,为了避免不同java实现的不同算法,生成不同的密钥,而导致解密失败
                SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
                random.setSeed(key.getBytes());
                keyGenerator.init(KEYSIZE, random);
                SecretKey secretKey = keyGenerator.generateKey();
                byte[] enCodeFormat = secretKey.getEncoded();
                SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
                // 创建密码器
                Cipher cipher = Cipher.getInstance("AES");
                // 初始化
                cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
                // 加密
                return cipher.doFinal(data);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * AES解密
     *
     * @param enCryptdata - 待解密字节数组
     * @param key         - 秘钥
     * @return 解密后的字符串
     */
    public static String decryptToStr(String enCryptdata, String key) {
        return StringUtil.isNotBlank(enCryptdata) ? new String(decrypt(parseHexStr2Byte(enCryptdata), key)) : null;
    }

    /**
     * 二进制转换成16进制
     *
     * @param buf - 二进制数组
     * @return 16进制字符串
     */
    public static String parseByte2HexStr(byte buf[]) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < buf.length; i++) {
            String hex = Integer.toHexString(buf[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            sb.append(hex.toUpperCase());
        }
        return sb.toString();
    }

    /**
     * 将16进制转换为二进制
     *
     * @param hexStr - 16进制字符串
     * @return 转换后的字节数组
     */
    public static byte[] parseHexStr2Byte(String hexStr) {
        if (hexStr.length() < 1) {
            return null;
        }
        byte[] result = new byte[hexStr.length() / 2];
        for (int i = 0; i < hexStr.length() / 2; i++) {
            int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
            int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
            result[i] = (byte) (high * 16 + low);
        }
        return result;
    }
}

 

4. 编写拦截器进行,权限验证。

备注:ErrorReportException类,为自定义的继承异常类,用于直接返回Response信息,此处自行替换成其它方式。 

          GenericCodeEnum类,为返回值的特定枚举类,用于直接返回Response中的Json数据,此处自行替换成其它数据。

import com.alibaba.fastjson.JSONObject;
import com.consmation.iintegratedsupserver.annotation.CheckToken;
import com.consmation.iintegratedsupserver.annotation.LoginToken;
import com.consmation.iintegratedsupserver.enums.GenericCodeEnum;
import com.consmation.iintegratedsupserver.error.ErrorReportException;
import com.consmation.iintegratedsupserver.util.JwtUtils;
import com.consmation.iintegratedsupserver.util.StringUtil;
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.lang.reflect.Method;

/**
 * JWT验证拦截器
 * @author alex
 */
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        // 从 http 请求头中取出 token
        String token = httpServletRequest.getHeader("token");
        // 如果不是映射到方法直接通过
        if (!(object instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) object;
        Method method = handlerMethod.getMethod();
        // 检查是否有LoginToken注释,有则跳过认证
        if (method.isAnnotationPresent(LoginToken.class)) {
            LoginToken loginToken = method.getAnnotation(LoginToken.class);
            if (loginToken.required()) {
                return true;
            }
        }
        // 检查有没有需要用户权限的注解
        if (method.isAnnotationPresent(CheckToken.class)) {
            CheckToken checkToken = method.getAnnotation(CheckToken.class);
            if (checkToken.required()) {
                // 执行认证
                if (StringUtil.isBlank(token)) {
                    throw new ErrorReportException(GenericCodeEnum.ERROR_RTNCODE_TOKEN_ILLEGAL.getCode(), GenericCodeEnum.ERROR_RTNCODE_TOKEN_ILLEGAL.getMsg());
                }
                // 校验jwt是否有效,有效则返回json信息,无效则返回空
                String retJson = JwtUtils.validateJwt(token);
                if (StringUtil.isNotBlank(retJson)) {
                    JSONObject jsonObject = JSONObject.parseObject(retJson);
                    // 此处判断是否是永久的token,仅在开发时保留
                    if ("true".equals(jsonObject.getString("isForever"))) {
                        return true;
                    }
                    // 校验客户端信息
                    String userAgent = httpServletRequest.getHeader("User-Agent");
                    if (userAgent.equals(jsonObject.getString("userAgent"))) {
                        // 获取刷新后的jwt值,设置到响应头中
                        httpServletResponse.setHeader("User-Token", jsonObject.getString("freshToken"));
                        // 将客户编号设置到session中
//                        httpServletRequest.getSession().setAttribute(GlobalConstant.SESSION_CUSTOMER_NO_KEY, jsonObject.getString("userId"));
                        return true;
                    } else {
                        throw new ErrorReportException(GenericCodeEnum.ERROR_RTNCODE_TOKEN_ILLEGAL.getCode(), GenericCodeEnum.ERROR_RTNCODE_TOKEN_ILLEGAL.getMsg());
                    }
                } else {
                    throw new ErrorReportException(GenericCodeEnum.ERROR_RTNCODE_TOKEN_ILLEGAL.getCode(), GenericCodeEnum.ERROR_RTNCODE_TOKEN_ILLEGAL.getMsg());
                }
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }
}

 5. 配置拦截器,将拦截器注入容器。

/**
 * 拦截器配置类
 *
 * @author Alex
 * @date 2019年11月13日14:48:32
 */
@Configuration
public class InterceptorConfig  implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry interceptorRegistry) {
        // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
        interceptorRegistry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");
    }
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
}

6. 测试及实际应用

@RestController
@RequestMapping(value = "UserServer")
public class UserController {

    // 登录验证
    @PostMapping(value = "LoginVerify")
    @LoginToken
    public JSONObject loginByLoginName(@RequestBody Map<String, String> paramMap, HttpServletRequest request) {
        String loginName = paramMap.get("loginname");
        String password = paramMap.get("password");
        if (StringUtil.isEmpty(loginName) || StringUtil.isEmpty(password)) {
            throw new ErrorReportException(GenericCodeEnum.ERROR_BINDING_PARAM.getCode(), GenericCodeEnum.ERROR_BINDING_PARAM.getMsg());
        }
        JSONObject jsonObject = new JSONObject();
        /*此处省略登录验证过程,假定验证成功,将生成的token信息保存在返回信息里返回即可*/
       jsonObject.put("token", JwtUtils.generateJwt(userinfo.getStLoginname(), userinfo.getStName(), request.getHeader("User-Agent")));
        return jsonObject;
    }

    // 验证token权限
    // 调用此接口时,需要将登录接口获取到的token值添加到request header中
    @CheckToken
    @PostMapping(value = "GetUserInfo")
    public String getUserInfo() {
        return "已通过验证";
    }

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值