JWT验证用户信息功能与OAuth2协议
1 简介
JSON Web token简称JWT, 是用于对应用程序上的用户进行身份验证的标记。也就是说, 使用 JWTS 的应用程序不再需要保存有关其用户的 cookie 或其他session数据。此特性便于可伸缩性, 同时保证应用程序的安全。

在身份验证过程中, 当用户使用其凭据成功登录时, 将返回 JSON Web token, 并且必须在本地保存 (通常在本地存储中)。
每当用户要访问受保护的路由或资源 (端点) 时, 用户代理(user agent)必须连同请求一起发送 JWT, 通常在授权标头中使用Bearer schema。后端服务器接收到带有 JWT 的请求时, 首先要做的是验证token。
2 格式
-
JWT就是一个字符串,经过加密处理与校验处理的字符串,形式为:A.B.C
-
A由JWT头部信息header经过base64加密得到
#默认的头信息
{
"alg": "HS256",
"typ": "JWT"
}
#官网测试:https://jwt.io/
#base64加密后的字符串为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
-
B是payload,存放有效信息的地方,这些信息包含三个部分:
-
标准中注册的声明 (建议但不强制使用)
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
-
公共的声明
-
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
-
私有的声明
- 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
#存放的数据:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
#base64后的字符串为:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
-
C由A和B通过加密算法得到,用作对token进行校验,看是否有效
- 这个部分需要base64加密后的header和base64加密后的payload使用
.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
- 这个部分需要base64加密后的header和base64加密后的payload使用
#secret为:oldlu
#得到的加密字符串为:DwMTjJktoFFdClHqjJMRgYzICo6FJOUc3Jmev9EScBc
#整体的token为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.DwMTjJktoFFdClHqjJMRgYzICo6FJOUc3Jmev9EScBc
3 流程

4 示例
导入依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
编写测试用例:
package com.tanhua.sso.service;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class TestJWT {
String secret = "oldlu";
@Test
public void testCreateToken(){
Map<String, Object> header = new HashMap<String, Object>();
header.put(JwsHeader.TYPE, JwsHeader.JWT_TYPE);
header.put(JwsHeader.ALGORITHM, "HS256");
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("mobile", "1333333333");
claims.put("id", "2");
// 生成token
String jwt = Jwts.builder()
.setHeader(header) //header,可省略
.setClaims(claims) //payload,存放数据的位置,不能放置敏感数据,如:密码等
.signWith(SignatureAlgorithm.HS256, secret) //设置加密方法和加密盐
.setExpiration(new Date(System.currentTimeMillis() + 3000)) //设置过期时间,3秒后过期
.compact();
System.out.println(jwt);
}
@Test
public void testDecodeToken(){
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtb2JpbGUiOiIxMzMzMzMzMzMzIiwiaWQiOiIyIiwiZXhwIjoxNjA1NTEzMDA2fQ.1eG3LpudD4XBycUG39UQDaKVBQHgaup-E1OLWo_m8m8";
try {
// 通过token解析数据
Map<String, Object> body = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
System.out.println(body); //{mobile=1333333333, id=2, exp=1605513392}
} catch (ExpiredJwtException e) {
System.out.println("token已经过期!");
} catch (Exception e) {
System.out.println("token不合法!");
}
}
}
2、校验token
在整个系统架构中,只有SSO保存了JWT中的秘钥,所以只能通过SSO系统提供的接口服务进行对token的校验,所以在SSO系统中,需要对外开放接口,通过token进行查询用户信息,如果返回null说明用户状态已过期或者是非法的token,否则返回User对象数据。
2.1、UserController
/**
* 校验token,根据token查询用户数据
*
* @param token
* @return
*/
@GetMapping("{token}")
public User queryUserByToken(@PathVariable("token") String token) {
return this.userService.queryUserByToken(token);
}
2.2、UserService
public User queryUserByToken(String token) {
try {
// 通过token解析数据
Map<String, Object> body = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
User user = new User();
user.setId(Long.valueOf(body.get("id").toString()));
//需要返回user对象中的mobile,需要查询数据库获取到mobile数据
//如果每次都查询数据库,必然会导致性能问题,需要对用户的手机号进行缓存操作
//数据缓存时,需要设置过期时间,过期时间要与token的时间一致
//如果用户修改了手机号,需要同步修改redis中的数据
String redisKey = "TANHUA_USER_MOBILE_" + user.getId();
if(this.redisTemplate.hasKey(redisKey)){
String mobile = this.redisTemplate.opsForValue().get(redisKey);
user.setMobile(mobile);
}else {
//查询数据库
User u = this.userMapper.selectById(user.getId());
user.setMobile(u.getMobile());
//将手机号写入到redis中
//在jwt中的过期时间的单位为:秒
long timeout = Long.valueOf(body.get("exp").toString()) * 1000 - System.currentTimeMillis();
this.redisTemplate.opsForValue().set(redisKey, u.getMobile(), timeout, TimeUnit.MILLISECONDS);
}
return user;
} catch (ExpiredJwtException e) {
log.info("token已经过期! token = " + token);
} catch (Exception e) {
log.error("token不合法! token = "+ token, e);
}
return null;
}
2.3、测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xKUyAUZ8-1610109096955)(assets/image-20201123155434121.png)]
数据已经存储到redis中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uH4jeYso-1610109096958)(…/image-20201123162229283.png)]
2.3、统一处理token
在之前的开发中,我们会在每一个Service中对token做处理,相同的逻辑一定是要进行统一处理的,接下来我们将使用拦截器+ThreadLocal的方式进行解决。
2.3.1、编写UserThreadLocal
package com.tanhua.server.utils;
import com.tanhua.server.pojo.User;
public class UserThreadLocal {
//线程 变量 隔离 thread1 ThreadLocal(aa=10 ) thread2 aa=20
private static final ThreadLocal<User> LOCAL = new ThreadLocal<User>();
private UserThreadLocal() {
}
public static void set(User user) {
LOCAL.set(user);
}
public static User get() {
return LOCAL.get();
}
}
2.3.1.1 讲解ThreadLocal,线程安全的一种策略
从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
内存泄露问题:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3w4haErB-1610066910992)(img/image-20201107211712573.png)]
上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。
1、Thread中有一个map,就是ThreadLocalMap
2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。
3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收
重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
补充内容:线程安全:原子性(lock,synchronized),可见性(volatile,synchronized),顺序性(synchronized,volatile),ThreadLocal(变量隔离)
2.3.2、编写TokenInterceptor
package com.tanhua.server.interceptor;
import com.tanhua.server.pojo.User;
import com.tanhua.server.service.UserService;
import com.tanhua.server.utils.NoAuthorization;
import com.tanhua.server.utils.UserThreadLocal;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 统一完成根据token查询用User的功能
*/
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
NoAuthorization noAnnotation = handlerMethod.getMethod().getAnnotation(NoAuthorization.class);
if (noAnnotation != null) {
// 如果该方法被标记为无需验证token,直接返回即可
return true;
}
}
String token = request.getHeader("Authorization");
if (StringUtils.isNotEmpty(token)) {
User user = this.userService.queryUserByToken(token);
if (null != user) {
UserThreadLocal.set(user); //将当前对象,存储到当前的线程中
return true;
}
}
//请求头中如不存在Authorization直接返回false
response.setStatus(401); //无权限访问
return false;
}
}
2.3.3、编写注解NoAuthorization
package com.tanhua.server.utils;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented //标记注解
public @interface NoAuthorization {
}
2.3.4、注册拦截器
package com.tanhua.server.config;
import com.tanhua.server.interceptor.RedisCacheInterceptor;
import com.tanhua.server.interceptor.TokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private RedisCacheInterceptor redisCacheInterceptor;
@Autowired
private TokenInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注意拦截器的顺序
registry.addInterceptor(this.tokenInterceptor).addPathPatterns("/**");
registry.addInterceptor(this.redisCacheInterceptor).addPathPatterns("/**");
}
}
2.3.5、使用ThreadLocal
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZFCtaSVa-1610066910995)(assets/1567741529854.png)]
2.4、查询好友动态
查询好友动态其实就是查询自己的时间线表,好友在发动态时已经将动态信息写入到了自己的时间线表中。



浙公网安备 33010602011771号