分布式环境集成JWT(Java Web Token)

目录

一,说明:

  • Token的引入:客户端向服务端请求数据时一般都会加入验证信息,比如客户端在请求的信息中携带用户名、密码,服务端会校验用户名和密码是否正确,校验通过响应该客户端请求。但是每次都携带用户名和密码无疑有些繁琐,而且也不安全,在这种背景下,Token便应运而生。Token在计算机身份认证中是令牌的意思。
  • Token的定义:Token是服务端生成的一串字符串,用来作为客户端请求的一个令牌。Token是客户端第一次登录时,由服务端生成并将其返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需带上用户名和密码。

二,Token、Session和Cookie比较

  • Session和Cookie区别:
    • 数据存放位置不同:Session数据是存在服务器中的,Cookie数据存放在浏览器当中;
    • 安全程度不同:Session存储在服务器中,比Cookie存储在浏览器中,安全程度要高;
    • 性能使用程度不同:Session存储在服务器上,数量过多会影响服务器性能;
    • 数据存储大小不同:单个Cookie保存的数据不能超过4K,Session存储在服务端,根据服务器大小决定。
  • Token和Session区别:
    • Token是开发定义的,Seesion是http协议规定的;
    • Token一般不存储,Session存储在服务器中,且在分布式环境中,Session会失效;

三,Spring Boot项目集成JWT

  • 我们实现如下功能:客户端登录获取Token,请求时携带Token,服务端对携带的Token进行验证,若是有效Token则放行,非法或是过期Token拦截,给出说明信息后直接返回。
  • 代码中抛出的异常,以及返回的实体类信息,是自定义异常,详细可参考该博客自定义异常

1,引入依赖

xml 复制代码
    <!--hutool-->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
    </dependency>
    <!---jwt(java web token)-->
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
    </dependency>

2,Token工具类

通过该工具类来生成Token,以及从请求头中获取token来获取当前用户的信息。

java 复制代码
package com.tick.tack.utils;

import cn.hutool.core.date.DateUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.tick.tack.manager.entity.User;
import com.tick.tack.manager.service.IUserService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;

@Component
public class TokenUtils {
    // 日志类
    private static final Logger log= LoggerFactory.getLogger(TokenUtils.class);

    private static IUserService staticUserService;
    
    @Resource
    private IUserService userService;

    @PostConstruct
    public void setUserService() {
        //必须加@Component注解后才会执行该段代码,在spring容器中初始化
        staticUserService = userService;
    }

    public static String getToken(String userId, String password) {
        return JWT.create().withAudience(userId) //将userId保存到token里面,作为载荷
                .withExpiresAt(DateUtil.offsetHour(new Date(), 2))//2小时候过期
                .sign(Algorithm.HMAC256(password));//以password作为token的密钥
    }

    /**
     * 获取当前登录的用户信息
     */
    public static User getCurrentUser() {
        try {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            // 从请求头中获取token信息
            String token = request.getHeader("token");
            if (StringUtils.isNotBlank(token)) {
                String userAccount = JWT.decode(token).getAudience().get(0);
                return staticUserService.queryUserByAccount(userAccount);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

3,定义拦截器

AuthAccess是一个自定义的注解,在拦截器中判断如果方法上有加入该注解,则放行,不校验token

java 复制代码
package com.tick.tack.common.interceptor;

import cn.hutool.jwt.JWTException;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.tick.tack.common.Constants;
import com.tick.tack.config.AuthAccess;
import com.tick.tack.exception.ServiceException;
import com.tick.tack.manager.entity.User;
import com.tick.tack.manager.service.IUserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class JWTInterceptor implements HandlerInterceptor {

    @Autowired
    private IUserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("token");
        //如果不是映射到方法直接通过
        if (!(handler instanceof HandlerMethod)) {
            return true;
        } else {
            // 判断是否为自定义注解AuthAccess,如果是,就不校验了,直接放行
            HandlerMethod h = (HandlerMethod) handler;
            AuthAccess authAccess = h.getMethodAnnotation(AuthAccess.class);
            if (authAccess != null) {
                return true;
            }
        }
        //执行认证
        if (StringUtils.isBlank(token)) {
            throw new ServiceException(Constants.CODE_401, "无token,请重新登录");
        }
        
        //获取token中的user id,验证是否合法
        String userAccount;
        try {
            userAccount = JWT.decode(token).getAudience().get(0);
        } catch (JWTException jwt) {
            throw new ServiceException(Constants.CODE_401, "token验证失败");
        }

        //根据token中的用户账号查询数据库信息
        User user = userService.queryUserByAccount(userAccount);
        if (user == null) {
            throw new ServiceException(Constants.CODE_401, "用户不存在,请重新登录");
        }

        //用户密码加签验证token
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
        try {
            //通过 verifier.verify() 方法检验 token,如果token不符合则抛出异常
            jwtVerifier.verify(token);
        } catch (Exception e) {
            throw new ServiceException(Constants.CODE_401, e.getMessage());
        }
        return true;
    }
}

自定义注解:

java 复制代码
package com.tick.tack.config;

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthAccess {
}

4,注册拦截器

将拦截器注册到SpringMVC中

java 复制代码
package com.tick.tack.config;

import com.tick.tack.common.interceptor.JWTInterceptor;
import org.springframework.context.annotation.Bean;
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 InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor())
                //拦截的路径
                .addPathPatterns("/**") //拦截所有请求,通过判断token是否合法来决定是否需要登录
                //排除不校验的接口
                .excludePathPatterns(
                        "/loginUser", //排除路径的时候不用考虑全局上下文context-path
                        "/register",
                        //Swagger页面拦截取消
                        "/swagger-resources/**", "/webjars/**", "/v3/**", "/swagger-ui.html/**", "doc.html", "/error");
    }

    //考虑到UserService,此处需要注入一下
    @Bean
    public JWTInterceptor jwtInterceptor() {
        return new JWTInterceptor();
    }
}

5,编写登录代码

  • 1,登录实体类
java 复制代码
@Data
public class LoginUser {
    // 登入用户名
    private String userAccount;
    // 登录密码
    private String password;
}
  • 2,token实体类
java 复制代码
@Data
public class TickToken {
    // 用户名
    private String userAccount;
    // 密码
    private String password;
    // token
    private String token;
    // 到期时间
    private Date expireTime;
}
  • 3,系统登录控制类
java 复制代码
@RestController
public class LoginController {
    @Autowired //按照类型注入
    @Qualifier(value = "loginServiceImpl")
    private ILoginService ILoginService;
    
    //登录系统
    @PostMapping("/loginUser")
    public Result loginSystem(@RequestBody LoginUser user) {
        if (StringUtils.isBlank(user.getUserAccount()) || StringUtils.isBlank(user.getPassword())) {
            return Result.error(Constants.CODE_400, "参数错误");
        }
        TickToken tickToken = ILoginService.loginSystem(user);

        return Result.success(tickToken);
    }
}
  • 4,业务逻辑实现类
java 复制代码
public TickToken loginSystem(LoginUser user) {
        User one = userService.queryUserByAccount(user.getUserAccount());
        if (one != null && one.getPassword().equals(user.getPassword())) {
            TickToken tickToken = new TickToken();
            //生成token信息并返回
            String token = TokenUtils.getToken(one.getUserAccount(), one.getPassword());
            tickToken.setToken(token);
            // 设置过期时间:当前时间两小时以后
            tickToken.setExpireTime(DateUtil.offsetHour(new Date(),2));

            // 处理用户的菜单信息,在登录的时候返回给用户
            List<Menu> roleMenus = getRoleMenus(one);
            //tickToken.setMenus(roleMenus);
            return tickToken;
        } else {
            throw new ServiceException(Constants.CODE_600, "用户名或密码错误");
        }
    }

6,测试

  • 1,测试拦截
    当前未登录,测试拦截是否生效
java 复制代码
@RestController
@RequestMapping("/demo")
public class DemoController {
	@GetMapping("/{id}")
    //@AuthAccess
    public Result getUser(@PathVariable("id") Integer id) {
        User user = new User(1, "zhangSan");
        return Result.success(user);
    }
}

未登录,也没有加相应注解,会提示没有token信息。

  • 2,登录获取token
    * 3,携带token请求

    如部分接口不希望被拦截,则为该接口方法加上@AuthAccess注解即可。

四,说明

该token是基于账户和密码来生成的一串字符串,并指定了过期时间,假如登录请求是在A机器实现,下一次请求在经过负载均衡后负载到B机器,B机器也可对其验证,因为token已经包括了全部的验证信息,服务器不保存相关信息,这样在分布式环境下也可正常使用。

相关推荐
Asthenia04121 小时前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz9651 小时前
ovs patch port 对比 veth pair
后端
Asthenia04121 小时前
Java受检异常与非受检异常分析
后端
uhakadotcom2 小时前
快速开始使用 n8n
后端·面试·github
JavaGuide2 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9652 小时前
qemu 网络使用基础
后端
Asthenia04122 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04122 小时前
Spring 启动流程:比喻表达
后端
Asthenia04123 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua3 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫