- 前端访问后台
- a系统和b系统访问
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。
JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token
,并且这个JWT token
带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输
JWT是由三段信息构成的,将这三段信息文本用.
链接一起就构成了Jwt字符串
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT的构成
第一部分我们称它为头部(header),
第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),
第三部分是签证(signature).
header
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
完整的头部就像下面这样的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
playload
载荷就是存放有效信息的地方 这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明 :
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后将其进行base64加密,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.
连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRta
如何应用
一般是在请求头里加入Authorization
,并加上Bearer
标注:
fetch('api/user/1', {
headers: {
'Authorization': 'Bearer ' + token
}
})
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IB6lV4WS-1689563766871)(http://qiniu.xiaotao.cloud/1821058-2e28fe6c997a60c9.png)]
优点
- 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
- 因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
- 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
- 它不需要在服务端保存会话信息, 所以它易于应用的扩展
安全相关
- 不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
- 保护好secret私钥,该私钥非常重要。
- 如果可以,请使用https协议
使用
java 中 使用 JWT
<!--jwt 在java 中的使用-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
生成签名
/**
* 生成签名
*/
@Test
public void t1(){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,100); // 100秒后过期
String token = JWT.create()
.withClaim("userId",21)
.withClaim("userName","xiaohuihui") // playload
.withExpiresAt(instance.getTime()) // 指定令牌过期时间
.sign(Algorithm.HMAC256("kirtuicxmnhlkeqwem$#%$^%^$#$")); // signature 签名内容自定义
System.out.println(token);
}
验证签名
/**
* 验证签名
*/
@Test
public void t2(){
JWTVerifier build = JWT.require(Algorithm.HMAC256("kirtuicxmnhlkeqwem$#%$^%^$#$")).build();
DecodedJWT verify = build.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6InhpYW9odWlodWkiLCJleHAiOjE2NDEyNzYwNzYsInVzZXJJZCI6MjF9.HhPEyOyQwamGAbZV0AEMMy24Rw7zKifpQxub4WfHpAU"); // 上面生成的字符串
System.out.println(verify.getClaim("userName").asString());
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BB3mDiGA-1689563766873)(http://qiniu.xiaotao.cloud/image-20220104141233841.png)]
异常:
AlgorithmMismatchException: 算法不匹配异常
TokenExpiredException: 令牌过期时间
JWTUtil
public class JwtUtils {
public static final String SIGN = "#%f4524df$^dsf23&**&^%$5dsf%^$fd245223sf#$";
/**
* 生成· token
* @return
*/
public static String getToken(Map<String,String> map){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7); // 默认7天后过期
// 创建 jwt Builder
JWTCreator.Builder builder = JWT.create();
// playload 使用 lambda 遍历 map
map.forEach((k,v) -> builder.withClaim(k,v));
String token = builder.withExpiresAt(instance.getTime()) // 指定令牌过期时间
.sign(Algorithm.HMAC256(SIGN)); // signature 签名
return token;
}
/**
* 验证 token 只要不合法 就会抛出异常
*/
public static void verify(String token){
JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
/**
* 获取 token 信息
*/
public static DecodedJWT getDecodedJWT(String token){
DecodedJWT verify = JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
return verify;
}
}
SpringBoot 整合 JWT
controller login
/**
* 管理后台登录 JWT
* @param username
*/
@PostMapping(value = "/user/login")
@ResponseBody
public RestResponseBo doLogin(@RequestParam("loginName") String username,
@RequestParam("password") String password,
@RequestParam(required = false) String rember_me,
HttpServletRequest request,
HttpServletResponse response) {
//读取缓存信息值:用于判断用户登录失败次数
Integer error_count = cache.get("login_error_count");
try {
TUsers users = new TUsers();
users.setUsername(username);
users.setPassword(password);
TUsers user = usersService.login(users);
// JWT
Map<String,String> payload = new HashMap<>();
payload.put("userId",user.getUid()+"");
payload.put("userName",user.getUsername());
// 生成JWT 令牌
String token = JwtUtils.getToken(payload);
//将当前登录用户 存入session中
request.getSession().setAttribute(WebConst.LOGIN_SESSION_KEY, user);
if (StringUtils.isNotBlank(rember_me)) {
//存入cook中 通过工具类
TaleUtils.setCookie(response, user.getUid());
}
//登录信息 存入日志表中
logService.insertLog(LogActions.LOGIN.getAction(), null, request.getRemoteAddr(), user.getUid());
} catch (Exception e) {
error_count = null == error_count ? 1 : error_count + 1;
if (error_count > 3) {
return RestResponseBo.fail("您输入密码已经错误超过3次,请10分钟后尝试");
}
//错误后存入 缓存中
cache.set("login_error_count", error_count, 10 * 60);
String msg = "登录失败";
if (e instanceof TipException) {
msg = e.getMessage();
} else {
LOGGER.error(msg, e);
}
return RestResponseBo.fail(msg);
}
return RestResponseBo.ok();
}
后面需要保护的请求
@RequestMapping("/user/test")
public Map<String,Object> test(String token){
HashMap<String, Object> map = new HashMap<>();
try {
DecodedJWT verify = JwtUtils.getDecodedJWT(token); // 验证令牌
map.put("status",true);
map.put("msg","请求成功");
}catch (SignatureVerificationException e){
e.printStackTrace();
map.put("msg","无效签名");
}catch (TokenExpiredException e){
e.printStackTrace();
map.put("msg","token 过期");
}catch (AlgorithmMismatchException e){
e.printStackTrace();
map.put("msg","两次算法不一致");
}catch (Exception e){
map.put("msg","token 无效");
}
map.put("status",false);
return map;
}
JWTInterceptor
/**
* jwt 请求拦截处理
*/
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HashMap<String, Object> map = new HashMap<>();
// 获取请求头中的令牌
String token = request.getHeader("token");
if(token == null){
request.setAttribute("msg","你没有权限访问 请先登录 ");
request.getRequestDispatcher("/login.html").forward(request,response);
return false;
}
try {
JwtUtils.getDecodedJWT(token); // 验证令牌
return true; // 发行
}catch (SignatureVerificationException e){
e.printStackTrace();
map.put("msg","无效签名");
}catch (TokenExpiredException e){
e.printStackTrace();
map.put("msg","token 过期");
}catch (AlgorithmMismatchException e){
e.printStackTrace();
map.put("msg","两次算法不一致");
}catch (Exception e){
map.put("msg","token 无效");
}
map.put("status",false); // 设置状态
// 将 map 转为 json 响应
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(json);
return false;
}
}
将拦截器注入到SpirngMVC
/**
* 登录请求拦截转发
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/**") //拦截的请求
.excludePathPatterns("/login.html","/","/user/login","/css/**","/js/**","/img/**"); //不拦截的请求
}