JWT
用于分布式系统的单点登录SSO场景,主要用来做用户身份鉴别或者资源安全性的技术。
传统的session认证:
流程:用户登录成功会保存一个会话,在cookie中维护一个jessionid,之后每次请求会携带到服务器上,如果没有过期并有状态,可以得到想要数据。一个session过期时间是30分钟,可在配置文件更改(server.servlet.session.timeout=30m)
@GetMapping
public R login(String username,String password,HttpSession session) {
//...登录成功
session.setAttribute("session",user);
}
如果用户大概有10W,日活用户3000左右,可以上面session认证可以满足。如果日活用户过多,一个用户会创建一个Session会话对象,一个用户字节是10kb,3000个用户就是20MB,JVM内存是2G,会出现页面卡顿现象。
暴露的问题:
- session会保存在服务器上,会给服务器造成压力,对于分布式服务,需要使用Redis,做存储。
- CSRF攻击,cookie在客户端,容易被截取,受到其他网站的伪请求攻击。
基于token机制:
流程:每个服务可以生成token,认证后把token传给前端,token储存给客户端进行储存,服务端没有压力。前端每次请求携带token,token通过可以得到想要的数据。
JWT构成
-
头部 header
声明加密的算法,是一个JSON,然后将JSON进行base64加密,得到一个字符串。
-
载荷 payload
存放有效信息(有3部分)
-
标准中注册的声明
iss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方 exp: jwt的过期时间,这个过期时间必须要大于签发时间 nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
-
公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息
-
私有的声明
有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息
-
-
签证
这个部分需要base64加密后的header和base64加密后的payload使用,连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT的第三部分。
官方网站:https://jwt.io/
如何使用
引入相关包:
<!-- jwt 实现接口安全性 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.7.0</version>
</dependency>
编写JWT实现类
/**
* jwt - 实现类
* @author shengren.yan
* @create 2022-10-30
*/
@Service
public class JwtService {
private static final Logger log = LoggerFactory.getLogger(JwtService.class);
// 1 定义加密的盐信息
private static final String KEY = "goodyan";
// 2 发行者
private static final String ISSUSER = "yan";
// 3 定义token的过期时间 (30天)
public static final Long TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 30L;
// 生产token
public String token(UserVo vo) {
Date now = new Date();
// 1 确定加密算法 (有很多种加密算法 - 可在官网中查看 https://jwt.io/libraries )
Algorithm algorithm = Algorithm.HMAC256(KEY);
// 2 创建token
String token = JWT.create()
.withIssuer(ISSUSER) // 签发者
.withIssuedAt(now) // token签发的时间
.withExpiresAt(new Date(now.getTime() + TOKEN_EXPIRE_TIME))
.withClaim("userid", userVo.getUserid()) // 加入要加密的值(可以多个)
.sign(algorithm);
return token;
}
// 验证token 参数 token 、userid (不要用自增主键 - 使用雪花算法)
public boolean verifyUserId(String token, String userid) {
try {
// 定义算法
Algorithm algorithm = Algorithm.HMAC256(KEY);
// 进行校验
JWTVerifier jwtVerifier = JWT.require(algorithm).withIssuer(ISSUSER)
.withClaim("userid", userid).build();
jwtVerifier.verify(token);
return true;
} catch (Exception ex) {
return false;
}
}
}
登录
/**
* token 授权 登录方式
* @author shengren.yan
* @create 2022-10-30
*/
@RestController
public class LoginAuthController {
@Autowired
private JwtService jwtService;
@Autowired(required = false)
private RedisTemplate redisTemplate;
@Autowired
private UserService userService;
// 登录
@PostMapping("/login")
public AuthResponse login(String username, String password) {
// 1 效验是否为空
// 2 根据用户查询用户是否存在
User user = userService.getByUserName(username);
if (user == null)
throw new VException(403, "用户名或密码有误!!!");
// 3 对密码进行加密加盐进行处理
password = MD5Util.md5slat(password);
// 如果用户输入的密码和数据库查询到密码不一致
if (!password.equalsIgnoreCase(user.getPassword()))
throw new ValidationException(403, "用户名或密码有误!!!");
UserVo userVo = new UserVo();
userVo.setUserid(user.getId());
userVo.setUsername(user.getUsername());
// 生成token信息
String token = jwtService.token(userVo);
userVo.setToken(token);
// 刷新token (把值存入Redis中)
userVo.setRefreshToken(UUID.randomUUID().toString());
redisTemplate.opsForValue().set(userVo.getRefreshToken(), userVo, JwtService.TOKEN_EXPIRE_TIME, TimeUnit.SECONDS);
return AuthResponse.success(userVo, ResponseCode.SUCCESS);
}
/**
* 续签的方法 -- 重新生成token
* @param refreshToken
* @return
*/
@PostMapping("/refresh/{refreshToken}")
public AuthResponse refresh(@PathVariable("refreshToken") String refreshToken) {
// 查看当前用户是不是还在有效期内 (在redis查看)
UserVo userVo = (UserVo) redisTemplate.opsForValue().get(refreshToken);
if (userVo == null)
throw new VException(403, "用户不存在!!!");
// 重新生成token
String jwt = jwtService.token(userVo);
userVo.setToken(jwt);
// 删除 redis中的key ,重新生成
redisTemplate.delete(refreshToken);
userVo.setRefreshToken(UUID.randomUUID().toString());
redisTemplate.opsForValue().set(userVo.getRefreshToken(), userVo, JwtService.TOKEN_EXPIRE_TIME, TimeUnit.SECONDS);
return AuthResponse.success(userVo, ResponseCode.SUCCESS);
}
@GetMapping("/logout")
public AuthResponse logout(String refreshToken) {
// redisTemplate.delete(refreshToken);
return AuthResponse.code(409L);
}
}
流程:当前端输入账号和密码进行登录,后端把token、该用户返回前端,之后每次请求,前端需要把token值携带传给后端,来验证是否有权限访问。
每次携带token,进行获取验证可以放在拦截器进行校验:
/**
* JWT 自定义过滤器
* 作用:每条请求都携带 token,通过自定义拦截器来获取 token值
* @author shengren.yan
* @create 2022-11-02
*/
public class AuthorizationInterceptor implements HandlerInterceptor {
@Autowired
private JwtService jwtService;
// 获取配置文件
@Value("${spring.profiles.active}")
private String profiles;
// token名,存token值
private static final String AUTH = "token";
private static final String USER = "usercode";
/**
* 校验token
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
response.setContentType("application/json;charset=utf-8");
// 1. 获取配置文件,如果是开发环境直接(放行)
if (!StringUtils.isEmpty(profiles) && profiles.equals("dev")) {
return true;
}
// 2. 从 http 请求头获取请求接口
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
String username = getParam(request, AUTH_USERNAME);
String token = getParam(request, AUTH);
if (StringUtils.isEmpty(token))
throw new ValidationException(300, "校验失败,请重新登录!!!");
if (StringUtils.isEmpty(username))
throw new ValidationException(300, "校验失败,请重新登录!!!");
// 开始对你token和你用户名进行token校验,如果正常直接返回,如果不正常抛出异常
AuthResponse authResponse = jwtService.verify(token, username);
// 如果不等于1,说明token和用户名校验失败
if (authResponse.getCode() != 1L) {
throw new ValidationException(300, "token 校验失败!");
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
// 从表头获取信息
public static String getParam(HttpServletRequest request, String filedName) {
return request.getHeader(filedName);
}
}
案例代码可参考我的gitee上的JWT。
跳转地址:https://gitee.com/yan418