🧑🎓 个人主页:Silence Lamb
📖 本章内容:【基于JWT实现登陆验证】
一、JWT消息构成
1.1【头部信息】
- 头部(header)
- 载荷(payload)
- 签证(signature)
🌳 Jwt的头部承载两部分信息
- 声明类型–这里是Jwt
- 声明加密的算法–通常直接使用 HMAC SHA256
1.2【载荷信息】
🌳载荷就是存放有效信息的地方
- 标准中注册的声明的数据
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
- 自定义数据
定义数据:存放我们想放在token中存放的key-value值
1.3【签证信息】
🌳 签证信息
- base64加密后的header和base64加密后的payload连接组成的字符串
- 然后通过header中声明的加密方式进行加盐secret组合加密
- 然后就构成了Jwt的第三部分
二、配置Redis
2.1【引入依赖】
spring:
redis:
host: 127.0.0.1
port: 6379
database: 0
timeout: 1800000
lettuce:
pool:
max-active: 20
max-wait: -1
max-idle: 5
min-idle: 0
2.2【Redis配置类】
/**
* @author Silence Lamb
* @apiNote Redis序列化配置
*/
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
//创建RedisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
//设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
//创建JSON序列化工具
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// key 和 hashKey 采用String序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// value 和 hashValue 采用JSON序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
2.3【Redis工具类】
**
* @author Silence Lamb
* @apiNote Redis工具类
*/
@SuppressWarnings(value = {"rawtypes" })
@Component
public class RedisUtils {
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout) {
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获取有效时间
*
* @param key Redis键
* @return 有效时间
*/
public long getExpire(final String key) {
return redisTemplate.getExpire(key);
}
/**
* 判断 key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key) {
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public boolean deleteObject(final Collection collection) {
return redisTemplate.delete(collection) > 0;
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList) {
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext()) {
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey) {
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 删除Hash中的某条数据
*
* @param key Redis键
* @param hKey Hash键
* @return 是否成功
*/
public boolean deleteCacheMapValue(final String key, final String hKey) {
return redisTemplate.opsForHash().delete(key, hKey) > 0;
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern) {
return redisTemplate.keys(pattern);
}
}
三、整合JWT
3.1【JWT配置】
- 引入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
- JWT配置信息
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: K0TmVM#8O9u2end6V~QpYZ!!Xt
# 令牌有效期(默认30分钟)
expireTime: 3000
spring:
redis:
host: 127.0.0.1
port: 6379
database: 0
timeout: 1800000
lettuce:
pool:
max-active: 20
max-wait: -1
max-idle: 5
min-idle: 0
- 读取配置信息
/**
* @author Silence Lamb
* @apiNote 读取JWT配置信息
*/
@Data
@Component
@ConfigurationProperties(prefix = "token")
public class TokenProperties {
/**
* 令牌自定义标识
*/
private String header;
/**
* 令牌秘钥
*/
private String secret;
/**
* 令牌有效期(默认30分钟)
*/
private int expireTime;
}
3.2【常量信息】
- Resis缓存常量
/**
* @author Silence Lamb
* @apiNote 缓存常量
*/
public class RedisConstant {
public static final String LOGIN_CODE_KEY= "login:code:";
public static final Long LOGIN_CODE_TTL = 2L;
public static final String LOGIN_TOKEN_KEY = "login:token:";
public static final Long LOGIN_TOKEN_TTL = 36000L;
public static final Long CACHE_NULL_TTL = 2L;
}
- 用户实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
/**
* 用户ID
*/
private String userID;
/**
* 用户名
*/
private String userName;
/**
* 账户密码
*/
private String passWord;
/**
* 用户唯一标识
*/
private String token;
/**
* token过期时间
*/
private Long expireTime;
/**
* 登录时间
*/
private Date loginTime;
}
3.3【Token工具类】
/**
* @author Silence Lamb
* @apiNote Token工具类
*/
@Component
public class TokenUtils {
@Resource
public TokenProperties tokenProperties;
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
/**
* 从数据生成令牌
*
* @param claims 数据
* @return token
*/
public String createToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, tokenProperties.getSecret())
.compact();
}
/**
* 从令牌中获取数据
*
* @param token 令牌
* @return 数据声明
*/
public Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(tokenProperties.getSecret())
.parseClaimsJws(token)
.getBody();
}
/**
* 获取缓存的token键值
*
* @param uuid 唯一标识
* @return 缓存的token键值
*/
public String getTokenKey(String uuid) {
return RedisConstant.LOGIN_TOKEN_KEY + uuid;
}
/**
* 获取缓存的code键值
*
* @param phone 手机号
* @return 缓存的验证码键值
*/
public String getCodeKey(String phone) {
return RedisConstant.LOGIN_CODE_KEY + phone;
}
}
1【创建Token令牌】
/**
* 创建令牌
*
* @param user 用户信息
* @return 令牌
*/
public String createToken(User user) {
//生成uuid作为存储在redis中token的键值
String uuid = UUID.randomUUID().toString();
//将生成的token存储起来
user.setToken(uuid);
//将用户信息放进JWT中
setUserAgent(user);
// 并记录重置token有效期
refreshToken(user);
Map<String, Object> claims = new HashMap<>();
claims.put(RedisConstant.LOGIN_TOKEN_KEY, uuid);
return createToken(claims);
}
2【刷新令牌有效期】
/**
* 刷新令牌有效期
* @param user 用户信息
*/
private void refreshToken(User user) {
user.setLoginTime(new Date());
long time = user.getLoginTime().getTime();
user.setExpireTime(time + tokenProperties.getExpireTime() * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(user.getToken());
redisUtils.setCacheObject(userKey, user, tokenProperties.getExpireTime(), TimeUnit.MINUTES);
}
3【从请求中获取token】
/**
* 从请求中获取token
*
* @param request 请求域
* @return token
*/
public String getToken(HttpServletRequest request) {
String token = request.getHeader(tokenProperties.getHeader());
if (StringUtils.isNotEmpty(token) && token.startsWith(RedisConstant.TOKEN_PREFIX)) {
token = token.replace(RedisConstant.TOKEN_PREFIX, "");
}
return token;
}
4【超时刷新令牌有效期】
/**
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param user 用户信息
*/
public void verifyToken(User user) {
long expireTime = user.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
refreshToken(user);
}
}
5【从令牌中获取用户名】
/**
* 从请求中获取用户身份信息
*
* @return 用户信息
*/
public User getLoginUser(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token)) {
try {
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(RedisConstant.LOGIN_TOKEN_KEY);
return redisUtils.getCacheObject(getTokenKey(uuid));
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims.getSubject();
}
6【设置用户身份信息】
/**
* 设置用户身份信息
*/
public void setLoginUser(User user) {
if (StringUtils.isNotNull(user) && StringUtils.isNotEmpty(user.getToken())) {
refreshToken(user);
}
}
7【删除用户身份信息】
/**
* 删除用户身份信息
*/
public void deleteLoginUser(String token) {
if (StringUtils.isNotEmpty(token)) {
String userKey = getTokenKey(token);
redisUtils.deleteObject(userKey);
}
}
四、登录拦截器
-
用户首先进入token拦截器-判断是否携带token
- true:进入登陆检验用户信息拦截器
-
登录拦截器-判断用户是否存在,账户密码是否正确
- true:放行-可以访问其他服务
- false:直接拦截用户请求
创建ThreadLocal保存用户信息
/**
* @author Silence Lamb
* @apiNote 创建ThreadLocal保存用户信息
*/
public class UserContext {
private static final ThreadLocal<User> contextHolder = new ThreadLocal<>();
//从线程获取用户信息
public static User getContext() {
return contextHolder.get();
}
//设置用户信息到线程中
public static void setContext(User user) {
contextHolder.set(user);
}
//防止线程泄露
public static void clearContext() {
contextHolder.remove();
}
}
4.1【Token拦截器】
/**
* @author Silence Lamb
* @apiNote 刷新Token拦截器
*/
public class RefreshTokenInterceptor implements HandlerInterceptor {
private final TokenUtils tokenUtils;
public RefreshTokenInterceptor(TokenUtils tokenUtils) {
this.tokenUtils = tokenUtils;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
User loginUser = tokenUtils.getLoginUser(request);
//3.判断用户是否存在
if (StringUtils.isNull(loginUser)) {
//4.不存在,拦截,返回 401 状态码
response.setStatus(401);
return true;
}
//5.存在,保存用户信息到Thread-local
tokenUtils.verifyToken(loginUser);
UserContext.setContext(loginUser);
//6.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.clearContext();
}
}
4.2【登录拦截器】
/**
* @author Sience Lamb
* @apiNote 登录拦截器
*/
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.判断Thread-local是否有用户
if (UserContext.getContext() == null) {
//4.没有用户则进行拦截,返回 401 状态码
response.setStatus(401);
return false;
}
//6.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.clearContext();
}
}
4.3【注册拦截器】
/**
* @author Silence Lamb
* @apiNote 配置拦截规则
*/
@Configuration
public class MyMvcConfigurer implements WebMvcConfigurer {
@Resource
private TokenUtils tokenUtils;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code",
"/user/login"
).order(1);
// token刷新的拦截器
registry.addInterceptor(new RefreshTokenInterceptor(tokenUtils)).addPathPatterns("/**").order(0);
}
}
4.4【测试登录拦截】
- 生成验证码
@Override
public AjaxResult sendCode(String phone) {
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
//2.手机不符合,返回错误信息
AjaxResult.error("手机格式错误");
}
//3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//4.保存验证码到session
log.debug("发送短信验证码成功,验证码:{}", code);
//5.返回OK
return AjaxResult.success().put("code", code);
}
- 收到验证码,用户进行登录
/**
* 用户登录
*
* @param loginBody 登录信息
*/
@Override
public AjaxResult login(LoginBody loginBody) {
//1.校验手机号
String phone = loginBody.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return AjaxResult.error("手机格式错误");
}
//2.校验验证码
String cacheCode = stringRedisTemplate.opsForValue().get(tokenUtils.getCodeKey(phone));
String code = loginBody.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
//3.验证码不一致:报错
return AjaxResult.error("验证码错误");
}
//4.验证码一致:根据手机号查找用户信息(从数据库中查询)
//5.用户不存在:不存在则创建新用户并保存到数据库中
User user = createUserWithPhone(phone);
//6.生成Token
user.setLoginTime(new Date());
String token = tokenUtils.createToken(user);
return AjaxResult.success(token);
}
/**
* 根据手机号创建新用户
*
* @param phone 手机号
* @return 新用户
*/
private User createUserWithPhone(String phone) {
User user = new User();
user.setPassWord(phone);
user.setUserName("silencelamb" + RandomUtil.randomNumbers(6));
return user;
}
-
收到验证码之后进行登录:http://localhost:80/user/login
{ "phone":13781570487, "code":"070793" }
{ "msg": "操作成功", "code": 200, "token": "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbjp0b2tlbjoiOiJiNDNlYzBlNC1lNWI4LTRjYzEtODFhMC1iNGViOTVmZTg5Y2UifQ.HbwZ7VEqyn8Hk1sxib--EWB5NFMhdyi_YmaPEPoQpIwLLWXBdE50czcwnFitcn99iBWyr6LXkD6g7YZMuJWVKQ" }
五、自定义注解
5.1【自定义注解】
- 自定义注解:用来跳过验证的PassToken
/**
* @author Silence Lamb
* @apiNote 用来跳过验证的PassToken
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
}
5.2【修改拦截器】
- 修改登录拦截器
@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();
//检查是否有passToken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
if (UserContext.getContext() == null) {
//4.没有用户则进行拦截,返回 401 状态码
response.setStatus(401);
return false;
}
//6.放行
return true;
}
- 创建Controller
@PassToken
@PostMapping("/PassToken")
public AjaxResult testPassToken(@RequestParam("id") String id) {
return AjaxResult.success("您已通过验证");
}