双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库。
2.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.2、创建两个注解,拦截器通过注释区分是否进行权限拦截
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 校验token
* @author
* @date 2019/12/9
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@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;
/**
* 跳过token
* @author
* @date 2019/12/9
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreCheckToken {
boolean required() default true;
}
2.3、编写CommonConstants
类
public class CommonConstants {
public static String BEARER="Bearer";//token前缀
public static String USER_ACCOUNT="userAccount";//用户账号
public static String USER_ID="userId";//用户Id
public static String PASSWORD="password";//密码
public static String SECRET_KEY="asd441qw1asdzx1asdas1d3aweqsad";//秘钥,当然也可以把用户的密码作秘钥
public static String IGNORE_LOGIN="login";//登录方法
}
2.4、编写JwtUtil工具类(生成token,解析token,校验token)
import com.admin.manager.common.constant.CommonConstants;
import com.admin.manager.storeService.config.RedisBean;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @author
* @date 2019/12/9
*/
public class JwtUtil {
/**
* 用户登录成功后生成Jwt
* 使用Hs256算法 私匙使用用户密码
*
* @param ttlMillis jwt过期时间
* @param params 登录成功的userId,userAccount,password
* @return
*/
public String createJWT(long ttlMillis, Map<String, String> params) {
//指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成JWT的时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("userId", params.get("userId"));
claims.put("userAccount", params.get("userAccount"));
claims.put("password", params.get("password"));
//生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
byte[] secretKey = Base64.getEncoder().encode(CommonConstants.SECRET_KEY.getBytes());
//生成签发人
String subject = params.get("userAccount");
//下面就是在为payload添加各种标准声明和私有声明了
//这里其实就是new一个JwtBuilder,设置jwt的body
JwtBuilder builder = Jwts.builder()
//如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
//设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setId(UUID.randomUUID().toString())
//iat: jwt的签发时间
.setIssuedAt(now)
//代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
.setSubject(subject)
//设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey);
String token = builder.compact();
//token存放redis中,并设置失效时间
RedisTemplate redisTemplate = RedisBean.redis;
redisTemplate.opsForValue().set("user:" + params.get("userId") + ":token", token, 6, TimeUnit.HOURS);
return token;
}
/**
* Token的解密
*
* @param token 加密后的token
* @param params 用户的对象
* @return
*/
public Claims parseJWT(String token, Map<String, String> params) {
//签名秘钥,和生成的签名的秘钥一模一样
String key = params.get("password");
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(key)
//设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
/**
* 验证token思路:(我这里是用同样的私钥操作的,因此我只判断了token的有效期! )
*
* 完整的参考下:
* 1.将传过来的token解析,解析出来用户Id,用户账号,用户密码。
* 2.比较解析出来的用户密码是否跟传过来的密码一致
* 3.如果不一致,提示:token错误
* 4.否则,验证token是否失效
* 5.如果失效,提示:token已失效
* 6.否则,进行下一步业务操作。
*/
public Boolean isVerify(String token) {
//签名秘钥,和生成的签名的秘钥一模一样
byte[] secretKey = Base64.getEncoder().encode(CommonConstants.SECRET_KEY.getBytes());
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(secretKey)
//设置需要解析的jwt
.parseClaimsJws(token).getBody();
String userId = (String) claims.get(CommonConstants.USER_ID);//用户Id
String userAccount = (String) claims.get(CommonConstants.USER_ACCOUNT);//用户账号
String password = (String) claims.get(CommonConstants.PASSWORD);//用户密码
String key = "user:" + userId + ":token";
RedisTemplate redisTemplate = RedisBean.redis;
//获取token的有效期
Long expire = redisTemplate.getExpire(key);
//如果返回-1,说明key已经过了有效期
if (expire == -1) {
return false;
} else {
//如果失效时间小于30分钟,将重新给key添加失效时间 (简称:续命)
if (expire <= 120) {
redisTemplate.opsForValue().set(key, token, 6, TimeUnit.HOURS);
}
return true;
}
}
}
2.5、编写拦截器拦截请求进行权限验证
import com.admin.manager.common.annotation.CheckToken;
import com.admin.manager.common.annotation.IgnoreCheckToken;
import com.admin.manager.common.constant.CommonConstants;
import com.admin.manager.common.exception.BusinessException;
import com.admin.manager.common.msg.Msg;
import com.admin.manager.common.pojo.BaseResponse;
import com.admin.manager.storeService.util.JwtUtil;
import com.alibaba.fastjson.JSON;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
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.io.IOException;
import java.lang.reflect.Method;
/**
* 定义拦截器
*
* @author
* @date 2019/12/10
*/
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//如果是登录,跳过校验
if (CommonConstants.IGNORE_LOGIN.equals(method.getName())) {
return true;
}
CheckToken checkToken = handlerMethod.getMethod().getAnnotation(CheckToken.class);
IgnoreCheckToken ignoreCheckToken = handlerMethod.getMethod().getAnnotation(IgnoreCheckToken.class);
//校验token
if (checkToken != null && ignoreCheckToken == null) {
String token = request.getHeader("token");
// System.out.println("校验token");
if (token == null) {
response = getMsg(response, Msg.TOKEN_NOT_EMPTY);
return false;
}
Boolean verify = new JwtUtil().isVerify(token);
if (verify) {
return true;
} else {
response = getMsg(response, Msg.TOKEN_EXPIRED);
return false;
}
}
//跳过token
else if (ignoreCheckToken != null && checkToken == null) {
return true;
}
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 {
}
private HttpServletResponse getMsg(HttpServletResponse response, String message) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("UTF-8");
response.getOutputStream().println(JSON.toJSONString(new BaseResponse(500, message)));
return response;
}
}
2.6、配置拦截器(ps:我使用的是springboot,大家使用ssm配置拦截器的方式不一样)
import com.admin.manager.storeService.Interceptor.AuthenticationInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
*
* 配置拦截器
* @author
* @date 2019/12/10
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private AuthenticationInterceptor authenticationInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this.authenticationInterceptor)
.addPathPatterns("/**"); // 拦截所有请求
}
}
2.7、在Controller中的实际应用
import com.admin.manager.common.annotation.CheckToken;
import com.admin.manager.common.annotation.IgnoreCheckToken;
import com.admin.manager.common.exception.BusinessException;
import com.admin.manager.common.pojo.ObjectResponse;
import com.admin.manager.common.rest.BaseController;
import com.admin.manager.storeService.biz.SysUserBiz;
import com.admin.manager.storeService.config.RedisBean;
import com.admin.manager.storeService.entity.SysUser;
import com.admin.manager.storeService.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.HashMap;
import java.util.Map;
@Controller
@RequestMapping("api/admin/user")
public class SysUserController extends BaseController<SysUserBiz, SysUser> {
@Autowired
private SysUserBiz userBiz;
/**
* 登录
*
* @param userAccount
* @param password
* @return
*/
@RequestMapping(value = "login")
public ObjectResponse login(@RequestParam String userAccount, @RequestParam String password) {
RedisTemplate redis = RedisBean.redis;
if (userAccount == null) {
throw new BusinessException("用户名不能为空");
}
if (password == null) {
throw new BusinessException("密码不能为空");
}
SysUser login = userBiz.login(userAccount, password);
Map<String, String> params = new HashMap();
params.put("userId", login.getId());
params.put("userAccount", login.getUserAccount());
params.put("password", login.getPassword());
String jwt = new JwtUtil().createJWT(10000, params);
if (jwt == null) {
throw new BusinessException("token生成失败");
}
params.put("token", jwt);
return new ObjectResponse(params);
}
//查看个人信息
@CheckToken
@GetMapping("/getMessage")
public String getMessage() {
return "你已通过验证";
}
//跳过token
@IgnoreCheckToken
@GetMapping("/ignore")
public String ignore(){
return "你已经跳过验证";
}
}