JWT Token验证
一、引入依赖和配置
在pom.xml中引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
在application.properties中加入token配置
#token
token.expirationSeconds=3600
token.validTime=7
二、Spring Security引入JWT
1. 生成jwt文件
命令行输入
keytool -genkey -alias jwt -keyalg RSA -keysize 1024 -keystore jwt.jks -validity 365
在上面的命令中,-alias选项为别名,-keypass和-storepass为密码选项,-validity为配置jks文件的过期时间(单位:天)。
生成后放入项目中,我是放到了resources中
2. JWT工具类
JwtTokenUtil.java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;
import java.util.Map;
/**
* @Auther: kiritoghy
* @Date: 19-7-23 下午7:09
*/
public class JwtTokenUtil {
@Autowired
private static RedisUtil redisUtil;
// 寻找证书文件
private static InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("jwt.jks"); // 寻找证书文件
private static PrivateKey privateKey = null;
private static PublicKey publicKey = null;
static { // 将证书文件里边的私钥公钥拿出来
try {
KeyStore keyStore = KeyStore.getInstance("JKS"); // java key store 固定常量
keyStore.load(inputStream, "123456".toCharArray());
privateKey = (PrivateKey) keyStore.getKey("jwt", "123456".toCharArray()); // jwt 为 命令生成整数文件时的别名
publicKey = keyStore.getCertificate("jwt").getPublicKey();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 生成token
* @param subject (主体信息)
* @param expirationSeconds 过期时间(秒)
* @param claims 自定义身份信息
* @return
*/
public static String generateToken(String subject, int expirationSeconds, Map<String,Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setExpiration(new Date(System.currentTimeMillis() + expirationSeconds * 1000))
// .signWith(SignatureAlgorithm.HS512, salt) // 不使用公钥私钥
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
}
/**
* @author: kiritoghy
* @date: 2018-10-19 09:10
* @deprecation: 解析token,获得subject中的信息
*/
public static String parseToken(String token) {
String subject = null;
try {
subject = getTokenBody(token).getSubject();
} catch (ExpiredJwtException e) {
subject = e.getClaims().getSubject();
}
return subject;
}
//获取token自定义属性
public static Map<String,Object> getClaims(String token){
Map<String,Object> claims = null;
try {
claims = getTokenBody(token);
}catch (ExpiredJwtException e) {
claims = e.getClaims();
}
return claims;
}
// 是否已过期
public static boolean isExpiration(String expirationTime){
/*return getTokenBody(token).getExpiration().before(new Date());*/
//通过redis中的失效时间进行判断
String currentTime = DateUtil.getTime();
if(DateUtil.compareDate(currentTime,expirationTime)){
//当前时间比过期时间小,失效
return true;
}else{
return false;
}
}
private static Claims getTokenBody(String token){
return Jwts.parser()
.setSigningKey(publicKey)
.parseClaimsJws(token)
.getBody();
}
}
3. JWT Filters
JwtAuthenticationTokenFilter.java
这个是jwt Filter,用来验证用户携带的token,能够实现登录验证
import com.alibaba.fastjson.JSON;
import com.uestc.labelproject.service.impl.SelfUserServiceImpl;
import com.uestc.labelproject.utils.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @Auther: kiritoghy
* @Desc:验证token,判断是否登录
* @Date: 19-7-23 下午8:40
*/
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Value("${token.expirationSeconds}")
private int expirationSeconds;
@Value("${token.validTime}")
private int validTime;
@Autowired
SelfUserServiceImpl selfUserService;
@Autowired
RedisUtil redisUtil;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String authHeader = httpServletRequest.getHeader("Authorization");
//获取请求的ip地址
String currentIp = AccessAddressUtil.getIpAddress(httpServletRequest);
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String authToken = authHeader.substring("Bearer ".length());
String username = JwtTokenUtil.parseToken(authToken);
String ip = (String)JwtTokenUtil.getClaims(authToken).get("ip");
//进入黑名单验证
if (redisUtil.isBlackList(authToken)) {
log.info("用户:{}的token:{}在黑名单之中,拒绝访问",username,authToken);
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(ResultGenerator.genFailResult("Token失效,请重新登录")));
return;
}
//判断token是否过期
/*
* 过期的话,从redis中读取有效时间(比如七天登录有效),再refreshToken(根据以后业务加入,现在直接refresh)
* 同时,已过期的token加入黑名单
*/
if (redisUtil.hasKey(authToken)) {//判断redis是否有保存
String expirationTime = redisUtil.hget(authToken,"expirationTime").toString();
if (JwtTokenUtil.isExpiration(expirationTime)) {
//获得redis中用户的token刷新时效
String tokenValidTime = (String) redisUtil.getTokenValidTimeByToken(authToken);
String currentTime = DateUtil.getTime();
//这个token已作废,加入黑名单
log.info("{}已作废,加入黑名单",authToken);
redisUtil.hset("blacklist", authToken, DateUtil.getTime());
if (DateUtil.compareDate(currentTime, tokenValidTime)) {
//超过有效期,不予刷新
log.info("{}已超过有效期,不予刷新",authToken);
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(ResultGenerator.genFailResult("Token失效,请重新登录")));
return;
} else {//仍在刷新时间内,则刷新token,放入请求头中
String usernameByToken = (String) redisUtil.getUsernameByToken(authToken);
username = usernameByToken;//更新username
ip = (String) redisUtil.getIPByToken(authToken);//更新ip
//获取请求的ip地址
Map<String, Object> map = new HashMap<>();
map.put("ip", ip);
String jwtToken = JwtTokenUtil.generateToken(usernameByToken, expirationSeconds, map);
//更新redis
Integer expire = validTime * 24 * 60 * 60 * 1000;//刷新时间
redisUtil.setTokenRefresh(jwtToken,usernameByToken,ip);
//删除旧的token保存的redis
redisUtil.deleteKey(authToken);
//新的token保存到redis中
redisUtil.setTokenRefresh(jwtToken,username,ip);
log.info("token仍在有效期,redis已删除旧token:{},\n新token:{}已更新redis",authToken,jwtToken);
authToken = jwtToken;//更新token,为了后面
httpServletResponse.setHeader("Authorization", "Bearer " + jwtToken);
}
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
/*
* 加入对ip的验证
* 如果ip不正确,进入黑名单验证
*/
if (!StringUtil.equals(ip, currentIp)) {//地址不正确
log.info("用户:{}的ip地址变动,进入黑名单校验",username);
//进入黑名单验证
if (redisUtil.isBlackList(authToken)) {
log.info("用户:{}的token:{}在黑名单之中,拒绝访问",username,authToken);
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(ResultGenerator.genFailResult("Token失效,请重新登录")));
return;
}
//黑名单没有则继续,如果黑名单存在就退出后面
}
UserDetails userDetails = selfUserService.loadUserByUsername(username);
if (userDetails != null) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
参考资料
https://www.cnblogs.com/stulzq/p/9678501.html
这篇博客比较详细的描述了关于token的管理,包括token的作用,token带来的风险,例如在token有效的时间内,可能会被恶意利用。因此为了尽量避免这种情况,对如何避免恶意获取token和被恶意获取后怎么控制失效也进行了描述,其中提到了黑名单,因此我又在原来基础上,添加了redis,用来刷新token和作为黑名单。
当然应该还有其他更好的解决办法,但这作为我的第一个项目,暂时就做到这里了。
Comments | 0 条评论