accessToken
:用户获取数据权限refreshToken
:用来获取新的accessToken双 token 验证机制,其中 accessToken 过期时间较短,refreshToken 过期时间较长。当 accessToken 过期后,使用 refreshToken 去请求新的 token。
引入依赖
<!-- 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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
application.yml配置
spring.redis.host=43.139.59.28
spring.redis.port=6379
spring.redis.timeout=10s
spring.redis.password=123
# 加密密钥
jwt.secret=zhangxb
# header 名称
jwt.header=Authorization
# accessToken有效单位为秒
jwt.expire.accessToken=3600
# refreshToken有效单位为秒
jwt.expire.refreshToken=30
JwtToken工具类
import com.alibaba.fastjson.JSONObject;
import com.example.demo.model.UserToken;
import com.example.demo.model.UserTokenInfo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
/**
* @author: zxb
* @createTime: 2022-11-23 14:55
* @version: 1.0.0
* @Description: JwtToken 工具类
*/
@Component
public class JwtTokenUtil {
/**
* 获取黑名单前缀
*/
public static final String TOKEN_BLACKLIST_PREFIX = "TOKEN_BLACKLIST-";
@Value("${jwt.secret}")
public String secret;
@Value("${jwt.header}")
public String header;
@Value("${jwt.expire.accessToken}")
public Integer accessTokenExpire;
@Value("${jwt.expire.refreshToken}")
public Integer refreshTokenExpire;
@Resource
RedisUtil redisUtil;
/**
* 保存 刷新令牌 与 访问令牌 关联关系 到redis
*
* @param userToken
* @param refreshTokenExpireDate
*/
public void tokenAssociation(UserToken userToken, Date refreshTokenExpireDate) {
Long time = (refreshTokenExpireDate.getTime() - System.currentTimeMillis()) / 1000 + 100;
redisUtil.set(userToken.getRefreshToken(), userToken.getAccessToken(), time);
}
/**
* 根据 刷新令牌 获取 访问令牌
*
* @param refreshToken
*/
public String getAccessTokenByRefresh(String refreshToken) {
Object value = redisUtil.get(refreshToken);
return value == null ? null : String.valueOf(value);
}
/**
* 添加至黑名单
*
* @param token
* @param expireTime
*/
public void addBlacklist(String token, Date expireTime) {
Long expireTimeLong = (expireTime.getTime() - System.currentTimeMillis()) / 1000 + 100;
redisUtil.set(getBlacklistPrefix(token), "1", expireTimeLong);
}
/**
* 校验是否存在黑名单
*
* @param token
* @return true 存在 false不存在
*/
public Boolean checkBlacklist(String token) {
return redisUtil.hasKey(getBlacklistPrefix(token));
}
/**
* 获取黑名单前缀
*
* @param token
* @return
*/
public String getBlacklistPrefix(String token) {
return Constants.TOKEN_BLACKLIST_PREFIX + token;
}
/**
* 生成双令牌
*
* @param userTokenInfo
* @return
*/
public UserToken createToekns(UserTokenInfo userTokenInfo) {
Date nowDate = new Date();
Date accessTokenExpireDate = new Date(nowDate.getTime() + accessTokenExpire * 1000);
Date refreshTokenExpireDate = new Date(nowDate.getTime() + refreshTokenExpire * 1000);
UserToken userToken = new UserToken();
BeanUtils.copyProperties(userTokenInfo, userToken);
userToken.setAccessToken(createToken(userTokenInfo, nowDate, accessTokenExpireDate));
userToken.setRefreshToken(createToken(userTokenInfo, nowDate, refreshTokenExpireDate));
// 创建 刷新令牌 与 访问令牌 关联关系
tokenAssociation(userToken, refreshTokenExpireDate);
return userToken;
}
/**
* 生成token
*
* @param userTokenInfo
* @return
*/
public String createToken(UserTokenInfo userTokenInfo, Date nowDate, Date expireDate) {
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(JSONObject.toJSONString(userTokenInfo))
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 获取 token 中注册信息
*
* @param token
* @return
*/
public Claims getTokenClaim(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e) {
claims=e.getClaims();
}
return claims;
}
/**
* 验证 token 是否过期失效
*
* @param token
* @return true 过期 false 未过期
*/
public Boolean isTokenExpired(String token) {
return getExpirationDate(token).before(new Date());
}
/**
* 获取 token 失效时间
*
* @param token
* @return
*/
public Date getExpirationDate(String token) {
return getTokenClaim(token).getExpiration();
}
/**
* 获取 token 发布时间
*
* @param token
* @return
*/
public Date getIssuedAtDate(String token) {
return getTokenClaim(token).getIssuedAt();
}
/**
* 获取用户信息
*
* @param token
* @return
*/
public UserTokenInfo getUserInfoToken(String token) {
String subject = getTokenClaim(token).getSubject();
UserTokenInfo userTokenInfo = JSONObject.parseObject(subject, UserTokenInfo.class);
return userTokenInfo;
}
/**
* 获取用户名
*
* @param token
* @return
*/
public String getUserName(String token) {
UserTokenInfo userInfoToken = getUserInfoToken(token);
return userInfoToken.getUserName();
}
/**
* 获取用户Id
*
* @param token
* @return
*/
public Long getUserId(String token) {
UserTokenInfo userInfoToken = getUserInfoToken(token);
return userInfoToken.getUserId();
}
}
JwtFilter 拦截器
import com.example.demo.model.UserTokenInfo;
import com.example.demo.util.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author: zxb
* @createTime: 2022-10-17 19:03
* @version: 1.0.0
* @Description: Jwt 拦截器
*/
@Slf4j
@Component
public class JwtFilter extends HandlerInterceptorAdapter {
@Resource
JwtTokenUtil jwtTokenUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 获取token
String token = request.getHeader(jwtTokenUtil.header);
if (StringUtils.isEmpty(token)) {
token = request.getParameter(jwtTokenUtil.header);
}
if (StringUtils.isEmpty(token)) {
// 只是简单DEMO,这里直接返回false,可以自己进行添加
log.error("token 不能为空!");
return false;
}
// 判断token是否超时
if (jwtTokenUtil.isTokenExpired(token)) {
log.error("token 已失效!");
return false;
}
// 判断 token 是否已在黑名单
if (jwtTokenUtil.checkBlacklist(token)) {
log.error("token 已被加入黑名单!");
return false;
}
// 获取用户信息
UserTokenInfo userInfoToken = jwtTokenUtil.getUserInfoToken(token);
// 通过用户信息去判断用户状态,等业务
return true;
}
}
WebConfig 类
import com.example.demo.component.JwtFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* @author: zxb
* @createTime: 2022-10-17 19:02
* @version: 1.0.0
* @Description: 请求拦截器
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private JwtFilter jwtFilter;
/**
* 不需要拦截地址
*/
public static final String[] EXCLUDE_URLS = {
"/login/**"
};
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtFilter)
.addPathPatterns("/**")
.excludePathPatterns(EXCLUDE_URLS);
}
}
UserTokenInfo类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserTokenInfo {
private Long userId;
private String userName;
private String realName;
}
UserToken类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserToken {
private Long userId;
private String userName;
private String realName;
private String accessToken;
private String refreshToken;
}
ResponseResult类
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* @Author
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
public class ResponseResult<T> {
/**
* 状态码
*/
private Integer code;
/**
* 提示信息,如果有错误时,前端可以获取该字段进行提示
*/
private String msg;
/**
* 查询到的结果数据,
*/
private T data;
public ResponseResult(T userToken) {
this.data=userToken;
}
public ResponseResult(String msg) {
this.msg=msg;
}
}
LoginController
import com.example.demo.model.ResponseResult;
import com.example.demo.model.UserToken;
import com.example.demo.model.UserTokenInfo;
import com.example.demo.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
/**
* @author zxb
* @since 2023-10-18
*/
@RestController
@RequestMapping("/login")
public class LoginController {
@Autowired
JwtTokenUtil jwtTokenUtil;
/**
* 登录
*
* @return
*/
@PostMapping("/login")
public ResponseResult<UserToken> login() {
// 业务验证:入参校验 + 用户信息校验查询
// 验证登录成功
UserTokenInfo userTokenInfo = new UserTokenInfo();
userTokenInfo.setUserId(1L);
userTokenInfo.setUserName("zxb");
userTokenInfo.setRealName("zhang");
// 生成Token
UserToken userToken = jwtTokenUtil.createToekns(userTokenInfo);
return new ResponseResult<>(userToken);
}
/**
* 刷新令牌
*
* @param refreshToken
* @return
*/
@PostMapping("/refreshToken/{refreshToken}")
public ResponseResult<UserToken> refreshToken(@PathVariable("refreshToken") String refreshToken) {
// 判断token是否超时
if (jwtTokenUtil.isTokenExpired(refreshToken)) {
return new ResponseResult<>("TOKEN_INVALID");
}
// 判断refreshToken是否在黑名单
if (jwtTokenUtil.checkBlacklist(refreshToken)){
return new ResponseResult<>("TOKEN_INVALID");
}
// 刷新令牌 放入黑名单
jwtTokenUtil.addBlacklist(refreshToken, jwtTokenUtil.getExpirationDate(refreshToken));
// 访问令牌 放入黑名单
String odlAccessToken = jwtTokenUtil.getAccessTokenByRefresh(refreshToken);
if (!StringUtils.isEmpty(odlAccessToken)) {
jwtTokenUtil.addBlacklist(odlAccessToken, jwtTokenUtil.getExpirationDate(odlAccessToken));
}
// 生成新的 访问令牌 和 刷新令牌
UserTokenInfo userInfoToken = jwtTokenUtil.getUserInfoToken(refreshToken);
// 生成Token
UserToken userToken = jwtTokenUtil.createToekns(userInfoToken);
return new ResponseResult<>( userToken);
}
/**
* 登出
*
* @return
*/
@PostMapping("/logOut/{token}")
public ResponseResult logOut(@PathVariable("token") String token) {
// 放入黑名单
jwtTokenUtil.addBlacklist(token, jwtTokenUtil.getExpirationDate(token));
return new ResponseResult<>("SUCCESS");
}
/**
* 注销
*
* @return
*/
@PostMapping("/logOff/{token}")
public ResponseResult logOff(@PathVariable("token") String token) {
// 修改用户状态业务
// 放入黑名单
jwtTokenUtil.addBlacklist(token, jwtTokenUtil.getExpirationDate(token));
return new ResponseResult<>("SUCCESS");
}
}
测试
刷新token
再次刷新token失败
因为之前的刷新refreshToken已经使用过一次失效了,不能再次刷新token。
携带accessToken访问其他接口成功
不携带accessToken访问其他接口失败