一、构建环境
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 "已通过验证";
}