【JavaWeb后端学习笔记】登录校验(JWT令牌技术、Interceptor拦截器、Filter过滤器)

登录校验

  • 1、JWT令牌技术
    • 1.1 JWT令牌介绍
    • 1.2 Java代码生成与校验JWT令牌
  • 2、Filter过滤器
    • 2.1 Filter过滤器的简单实现
    • 2.2 配置拦截路径
    • 2.3 Filter接口中的三个方法:
    • 2.4 Filter过滤器登录校验
    • 2.5 过滤器链
  • 3、Interceptor拦截器
    • 3.1 拦截器(Interceptor)的简单实现
    • 3.2 配置拦截路径
    • 3.3 拦截器中的三个方法
    • 2.4 Interceptor拦截器登录校验

登录校验是系统开发中不可缺少的一环。

1、JWT令牌技术

1.1 JWT令牌介绍

JWT全称为 JSON Web Token(https://jwt.io/)。JWT定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
JWT令牌由三部分组成:

  1. 第一部分Header(头):记录令牌类型、签名算法等。例如:{“alg”:“HS256”,“type”:“JWT”}
  2. 第二部分Payload(有效载荷):携带一些自定义的信息,默认信息等。例如:{“id”:“1”,“username”:“Tom”}
  3. 第三部分Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

Header和Payload会通过Base64编码生成。Signature部分会通过Header指定的签名算法,以Header、Payload以及指定的秘钥作为签名算法输入计算得到。这三部分共同组成了JWT令牌。

1.2 Java代码生成与校验JWT令牌

Java代码生成与校验令牌需要引入JWT的相关依赖:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

生成JWT令牌需要准备四个数据:签名算法、秘钥、有效时间和载荷(自定义内容)
签名算法有很多种,可以在JWT官网查看。
下面通过一个简单的例子感受一下:
生成JWT令牌:

@Test
public void testJWT(){
	// 准备Map集合载荷
    Map<String, Object> claims = new HashMap<>();
    claims.put("id", 1);
    claims.put("name", "Tom");
    String jwt = Jwts.builder() // 使用Jwts中的builder()方法构造
            .signWith(SignatureAlgorithm.HS256, "wrj-web") // 指定签名算法和秘钥
            .setClaims(claims) // 加入载荷
            .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 设置有效时间:当前时间 + 有效时长,单位为ms
            .compact();
    System.out.println(jwt);
}
// 运行输出:
// eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiVG9tIiwiaWQiOjEsImV4cCI6MTczMzU0MTk2MX0.AgNXthmTMBJHKIeSaacBf-wThVNwsPi1F63sAqsuJkY

解析JWT令牌:

    @Test void testParseJWT(){
        Claims claims = Jwts.parser() // 使用Jwts中的parser()方法开始解析
                .setSigningKey("wrj-wrb") // 指定秘钥
                .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiVG9tIiwiaWQiOjEsImV4cCI6MTczMzU0MTk2MX0.AgNXthmTMBJHKIeSaacBf-wThVNwsPi1F63sAqsuJkY") // 输入JWT令牌,此处直接输入上述代码生成的JWT令牌
                .getBody(); // 获取载荷
        System.out.println(claims);
    }
// 运行输出:
// {name=Tom, id=1, exp=1733541961}

通常会将JWT生成与解析的代码封装成JWT工具类来使用,JwtUtil工具类中只有两个成员方法,一个用于生成JWT令牌,一个用于解析JWT令牌:

public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息
     * @return
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT有效时间
        long expMillis = System.currentTimeMillis() + ttlMillis;
        Date exp = new Date(expMillis);

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);

        return builder.compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return
     */
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }
}

对于一般项目而言,JWT令牌需要的签名秘钥、有效时长以及前端传递过来的令牌名称是较为固定的,可以在配置到配置文件中,以便修改。
步骤如下:

  1. 在配置文件中自定义配置参数,代码如下:
web:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    secret-key: itcast
    # 设置jwt过期时间 单位ms
    ttl: 7200000
    # 设置前端传递过来的令牌名称
    token-name: token
  1. 定义一个properties类用于接收配置信息,类名一般为JwtProperties,注意成员变量名与配置信息中设置的名称要对应。
@Component // 注册成Bean
@ConfigurationProperties(prefix = "web.jwt") // 关联到配置文件中web->jwt下的配置信息
@Data // Lombok注解,生成get/set等方法
public class JwtProperties {
	// 签名秘钥,与配置文件中secret-key对应
    private String secretKey;
    // jwt过期时间,与配置文件中ttl对应
    private long ttl;
    // 前端传递过来的令牌名称,与配置文件中token-name对应
    private String tokenName;
}

在需要使用这些信息时,只需要将JwtProperties 对象注入,然后通过get/set方法获取即可。

2、Filter过滤器

过滤器能够把对资源的请求拦截下来,从而实现一些特殊的功能,例如登录校验、敏感字符处理等。
【JavaWeb后端学习笔记】登录校验(JWT令牌技术、Interceptor拦截器、Filter过滤器)_第1张图片

2.1 Filter过滤器的简单实现

Filter过滤器使用分两步:

  1. 定义Filter:定义一个类,实现Filter接口,并重写其所有方法。Filter接口是javax.servlet.*包下的Filter接口。
  2. 配置Filter:Filter类上加@WebFilter注解,配置拦截资源路径。引导类(启动类)上加@ServletComponentScan开启Servlet组件支持。
@WebFilter(urlPatterns = "/*") // 通过WebFilter注解表示这是一个web过滤器组件,通过urlPatterns属性配置拦截路径
public class DemoFilter implements Filter {

    // 初始化方法、Web服务器启动,创建Filter时调用,只调用一次。通常在此方法中完成资源和环境的准备操作
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    // 每次拦截到请求时会调用
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("拦截请求...放行前逻辑");
        filterChain.doFilter(servletRequest, servletResponse);
        System.out.println("拦截请求...放行后逻辑");
    }

    // 销毁方法,Web服务器关闭时调用,只调用一次。通常完成资源的释放等操作
    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

2.2 配置拦截路径

Filter过滤器的拦截路径通过 @WebFilter 注解中的 urlPatterns 属性设置,有三种方式:

拦截路径 urlPatterns 含义
拦截具体路径 /login 只有访问 /login 路径时,才会被拦截
目录拦截 /emps/* 访问 /emps 下的所有资源,都会被拦截
拦截所有 /* 访问所有资源,都会被拦截

2.3 Filter接口中的三个方法:

Filter接口中的三个方法有各自的执行时机和作用。

  • init():初始化方法、Web服务器启动,创建Filter时调用,只调用一次。通常在此方法中完成资源和环境的准备操作;
  • doFilter():每次拦截到请求时会调用。其中可包含放行前逻辑和放行后逻辑;
  • destroy():销毁方法,Web服务器关闭时调用,只调用一次。通常完成资源的释放等操作

在使用Filter过滤器时需要注意几点:

  1. 执行doFilter()方法时首先执行其中的放行前逻辑,然后放行。
  2. 过滤器放行,访问完对应路径的资源之后会回到过滤器。
  3. 回到过滤器之后还会执行放行后逻辑。

2.4 Filter过滤器登录校验

下面改造doFilter()方法,实现登录校验功能。
场景:用户进行登录操作不进行拦截,其他操作进行拦截。也就是不拦截"/login"路径,拦截其他所有路径。
拦截逻辑分为以下几步:
1.获取请求路径;
2.判断路径中是否包含"login",包含则直接放行,不包含则需拦截,进行登录校验;Filter放行是通过执行filterChain调用doFilter()方法
3.进行登录校验,首先获取JWT令牌;
4.判断JWT令牌是否存在,或是否为空。不存在或为空则校验失败,不放行,直接return结束。
5.解析JWT令牌,不成功则不放行;
6.解析JWT令牌成功则放行。

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse resp = (HttpServletResponse) servletResponse;
        // 1.获取请求的URL
        String url = req.getRequestURL().toString();

        // 2.判断login是否存在,存在则放行,不存在则拦截
        if(url.contains("login")) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }

        // 3.获取JWT令牌
        String jwt = req.getHeader("token");

        // 4.判断JWT是否存在
        if(!StringUtils.hasLength(jwt)) {
            Result error = Result.error("NOT_LOGIN");
            String s = JSONObject.toJSONString(error);
            resp.getWriter().write(s);
            return;
        }

        // 5.判断JWT是否能解析成功
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {
            e.printStackTrace();
            Result error = Result.error("NOT_LOGIN");
            String s = JSONObject.toJSONString(error);
            resp.getWriter().write(s);
            return;
        }

        // 6.解析成功,放行
        filterChain.doFilter(servletRequest, servletResponse);
    }

这部分登录校验的代码是可以进行优化的。因为实际业务中是不会对 /login 登录请求进行拦截的。

2.5 过滤器链

一个Web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。
由于存在多个过滤器,过滤器的执行顺序会按照过滤器类名(字符串)的自然顺序

3、Interceptor拦截器

拦截器是一种动态拦截方法调用的机制,类似与过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行。拦截器通常用来拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。

3.1 拦截器(Interceptor)的简单实现

Interceptor拦截器的使用分两步:

  1. 定义拦截器:实现HandlerInterceptor接口,并重写其所有方法,在实现类上加上@Component注解,交给IOC容器管理;
@Component // 注册成Bean,交给IOC容器管理
public class DemoInterceptor implements HandlerInterceptor {
    // 目标资源方法执行前执行,返回true:放行,返回false:不放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle...");
        return true;
    }

    // 目标资源方法执行后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle...");
    }

    // 视图渲染完毕后执行,最后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion...");
    }
}
  1. 注册配置拦截器。创建一个配置类,实现WebMvcConfigurer接口。在配置类上加@Configuration注解,代表当前类是配置类。重写WebMvcConfigurer接口的addInterceptors方法,使用该方法的形参registry调用addInterceptor方法,将创建好的拦截器注册进去。因此需要先将拦截器注入。然后接着调用addPathPatterns()和excludePathPatterns()方法配置拦截哪些路径,不拦截哪些路径。
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private DemoInterceptor demoInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(demoInterceptor) // 注册拦截器
                .addPathPatterns("/**") // 配置拦截资源
                .excludePathPatterns("/login"); // 配置不拦截的资源               
    }
}

除了通过实现WebMvcConfigurer接口来注册配置拦截器,Spring还提供了继承WebMvcConfigurationSupport配置类来实现注册配置拦截器。定义一个类,继承WebMvcConfigurationSupport配置类,重写其中的addInterceptors()方法。

/**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
	@Autowired
    private DemoInterceptor demoInterceptor;

    protected void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(demoInterceptor) // 注册拦截器
                .addPathPatterns("/**") // 配置拦截资源
                .excludePathPatterns("/login"); // 配置不拦截的资源               
    }
}

3.2 配置拦截路径

Interceptor拦截器的拦截路径是在配置类中进行配置的。可借鉴上述配置类的代码。
下面介绍Interceptor的拦截路径:

拦截路径 含义 举例
/* 一级路径 能匹配 /depts、/emps、/login,不能匹配 /depts/1
/** 任意级路径 能匹配 /depts、/depts/1、/depts/1/2
/depts/* /depts下的一级路径 能匹配 /depts/1、不能匹配 /depts/1/2,/depts
/depts/** /depts下的任意级路径 能匹配 /depts、/depts/1、/depts/1/2、不能匹配 /emps/1

3.3 拦截器中的三个方法

Interceptor接口中的三个方法有各自的执行时机和作用。

  • preHandle():目标资源方法执行前执行,返回true:放行,返回false:不放行;
  • postHandle():目标资源方法执行后执行;
  • afterCompletion():视图渲染完毕后执行,最后执行。

2.4 Interceptor拦截器登录校验

Interceptor拦截器进行登录校验与Filter过滤器实现登录校验的逻辑相同,只是部分实现细节不同。这里提供一个简单的实现。

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		// 1. 获取请求路径
        String url = request.getRequestURL().toString();
        // 2. 判断请求路径是否包含 /login
        if(url.contains("login")) {
            return true;
        }
        // 3. 从请求头获取token,一般把JWT令牌的key名设置为token
        String jwt = request.getHeader("token");

		// 4. 判断是否获取到JWT令牌
        if(!StringUtils.hasLength(jwt)) {
            Result error = Result.error("NOT_LOGIN");
            String s = JSONObject.toJSONString(error);
            response.getWriter().write(s);
            return false;
        }

		// 5. 解析JWT令牌
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {
            e.printStackTrace();
            Result error = Result.error("NOT_LOGIN");
            String s = JSONObject.toJSONString(error);
            response.getWriter().write(s);
            return false;
        }
		
        return true;
    }

你可能感兴趣的:(JavaWeb后端学习笔记,学习,笔记,java)