登录校验
- 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令牌由三部分组成:
- 第一部分Header(头):记录令牌类型、签名算法等。例如:{“alg”:“HS256”,“type”:“JWT”}
- 第二部分Payload(有效载荷):携带一些自定义的信息,默认信息等。例如:{“id”:“1”,“username”:“Tom”}
- 第三部分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的bodyJwtBuilder 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) {// 得到DefaultJwtParserClaims claims = Jwts.parser()// 设置签名的秘钥.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))// 设置需要解析的jwt.parseClaimsJws(token).getBody();return claims;}
}
对于一般项目而言,JWT令牌需要的签名秘钥、有效时长以及前端传递过来的令牌名称是较为固定的,可以在配置到配置文件中,以便修改。
步骤如下:
- 在配置文件中自定义配置参数,代码如下:
web:jwt:# 设置jwt签名加密时使用的秘钥secret-key: itcast# 设置jwt过期时间 单位msttl: 7200000# 设置前端传递过来的令牌名称token-name: token
- 定义一个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过滤器
过滤器能够把对资源的请求拦截下来,从而实现一些特殊的功能,例如登录校验、敏感字符处理等。
2.1 Filter过滤器的简单实现
Filter过滤器使用分两步:
- 定义Filter:定义一个类,实现Filter接口,并重写其所有方法。Filter接口是javax.servlet.*包下的Filter接口。
- 配置Filter:Filter类上加@WebFilter注解,配置拦截资源路径。引导类(启动类)上加@ServletComponentScan开启Servlet组件支持。
@WebFilter(urlPatterns = "/*") // 通过WebFilter注解表示这是一个web过滤器组件,通过urlPatterns属性配置拦截路径
public class DemoFilter implements Filter {// 初始化方法、Web服务器启动,创建Filter时调用,只调用一次。通常在此方法中完成资源和环境的准备操作@Overridepublic void init(FilterConfig filterConfig) throws ServletException {Filter.super.init(filterConfig);}// 每次拦截到请求时会调用@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {System.out.println("拦截请求...放行前逻辑");filterChain.doFilter(servletRequest, servletResponse);System.out.println("拦截请求...放行后逻辑");}// 销毁方法,Web服务器关闭时调用,只调用一次。通常完成资源的释放等操作@Overridepublic 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过滤器时需要注意几点:
- 执行doFilter()方法时首先执行其中的放行前逻辑,然后放行。
- 过滤器放行,访问完对应路径的资源之后会回到过滤器。
- 回到过滤器之后还会执行放行后逻辑。
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.获取请求的URLString 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拦截器的使用分两步:
- 定义拦截器:实现HandlerInterceptor接口,并重写其所有方法,在实现类上加上@Component注解,交给IOC容器管理;
@Component // 注册成Bean,交给IOC容器管理
public class DemoInterceptor implements HandlerInterceptor {// 目标资源方法执行前执行,返回true:放行,返回false:不放行@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("preHandle...");return true;}// 目标资源方法执行后执行@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("postHandle...");}// 视图渲染完毕后执行,最后执行@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println("afterCompletion...");}
}
- 注册配置拦截器。创建一个配置类,实现WebMvcConfigurer接口。在配置类上加@Configuration注解,代表当前类是配置类。重写WebMvcConfigurer接口的addInterceptors方法,使用该方法的形参registry调用addInterceptor方法,将创建好的拦截器注册进去。因此需要先将拦截器注入。然后接着调用addPathPatterns()和excludePathPatterns()方法配置拦截哪些路径,不拦截哪些路径。
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate DemoInterceptor demoInterceptor;@Overridepublic 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. 判断请求路径是否包含 /loginif(url.contains("login")) {return true;}// 3. 从请求头获取token,一般把JWT令牌的key名设置为tokenString 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;}