数据请求校验 JWT身份认证

Author: Exchanges

Version: 9.0.2

认证模块

2.1 登录

简单来说,就是用户登录时对用户的信息进行校验

2.1.1 根据登录场景划分

1.普通登录:只校验用户名和密码,不限次数;

2.唯一登录:一个账号只能在线一个,例如:游戏账号;

3.多设备唯一登录:一个账号可以在多个设备上进行唯一登录;

4.SSO单点登录:一次登录,处处运行,例如:同一个浏览器下,一个账号可以在一个企业中的多个子系统共享登录信息;

...............

2.1.2 根据形式划分

1.账号密码登录

2.手机验证码登录

3.扫码登录

4.人脸识别登录

..................

2.1.3 根据系统划分

1.网页(PC网站,手机网站)

2.APP(IOS,安卓,鸿蒙)

3.小程序(微信,支付宝)

4.智能设备

.....................

2.2 Jwt

2.2.1介绍

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息,该信息可以被验证和信任,因为它是数字签名的,常用于单点登录。

2.2.2 使用场景

校验用户的登陆信息时用,类似于淘宝的登陆系统并不是简单的只给淘宝用的,阿里巴巴旗下的绝大部分功能都可以使用这一个帐号登陆,如果我们给每个系统都写一套登陆系统的话,代码是一样的出现功能重写,那么我们想办法只写一个登陆系统,然后进行统一的验证,只需要在任意其他系统中对登陆返回的数据进行校验即可,我们称之为单点登录。

单点登录实现的方式有很多,其本质就是数据的共享,登陆系统会返回一个验证信息给用户,用户访问其他系统时携带着该信息,这样我们在进行一次登陆后就可以访问多个系统了,其实就是登陆系统返回的数据我们保存起来,下次按照服务器的要求通过对应的方式传递过去,比如cookie, header,请求参数等方式

2.2.3 结构

JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:头部,载荷,签名

  • Header

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象,示例:{"typ":"JWT","alg":"HS256"}

在头部指明了签名算法是HS256算法。 我们可以通过BASE64进行编码/解码:https://www.matools.com/base64/

  • Payload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分。

1.标准中注册的声明(建议但不强制使用):例如:sub表示jwt所面向的用户

2.公共的声明:可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

3.私有的声明:私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

例如:定义一个payload:

{"sub":"1234567890","name":"John Doe","admin":true}

然后将其进行base64加密,得到Jwt的第二部分:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

  • Signature

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:header + payload + secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了

2.2.5 使用Jwt的好处
  • 无状态和可扩展性:Tokens存储在客户端。完全无状态,可扩展。我们的负载均衡器可以将用户传递到任意服务器,因为在任何地方都没有状态或会话信息。
  • 安全:Token不是Cookie。(The token, not a cookie.)每次请求的时候Token都会被发送。而且,由于没有Cookie被发送,还有助于防止CSRF攻击。即使在你的实现中将token存储到客户端的Cookie中,这个Cookie也只是一种存储机制,而非身份认证机制。没有基于会话的信息可以操作,因为我们没有会话!
2.2.6 服务的无状态性
  • 服务端不保存任何客户端请求者信息
  • 客户端的每次请求必须具备⾃描述信息,通过这些信息识别客户端身份带来的好处是什么呢?
  • 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同⼀台服务器
  • 服务端的集群和状态对客户端透明
  • 服务端可以任意的迁移和伸缩
  • 减⼩服务端存储压⼒
2.2.7 无状态登录的流程
  • 当客户端第⼀次请求服务时,服务端对⽤户进⾏信息认证(登录)
  • 认证通过,将⽤户信息进⾏加密形成token,返回给客户端,作为登录凭证
  • 以后每次请求,客户端都携带认证的token服务端对token进⾏解密,判断是否有效。
  • 服务端对token进⾏解密,判断是否有效。

2.2 创建 study-commons 子模块

2.2.1 导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>study-app</artifactId>
        <groupId>com.qf</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>study-commons</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

</project>
2.2.2 编写BaseResultBean
package com.qf.beans;

import lombok.Data;

@Data
public class BaseResultBean {

    private Integer code;
    private String msg;

    //成功返回
    public static BaseResultBean ok(){
        BaseResultBean baseResultBean = new BaseResultBean();
        baseResultBean.setCode(200);
        baseResultBean.setMsg("success");

        return baseResultBean;
    }

    //失败返回
    public static BaseResultBean error(){
        BaseResultBean baseResultBean = new BaseResultBean();
        baseResultBean.setCode(-1);
        baseResultBean.setMsg("fail");

        return baseResultBean;
    }

    //未认证
    public static BaseResultBean unauth(){
        BaseResultBean baseResultBean = new BaseResultBean();
        baseResultBean.setCode(401);
        baseResultBean.setMsg("认证失败");

        return baseResultBean;
    }

}
2.2.3 编写系统Redis前缀类
package com.qf.contants;

public interface RedisPrefix {

    //JWT存放到Redis中的前缀
    String JSON_WEB_TOKEN = "JSON_WEB_TOKEN:";

    //公共参数存放Redis中的KEY
    String SYSTEM_PARAMS_KEY = "SYSTEM_PARAMS_KEY";

    //幂等性存放Redis中的前缀
    String IDEMPOTENT = "IDEMPOTENT:";

}

2.3 创建 study-auth 子模块

2.3.1 导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>study-app</artifactId>
        <groupId>com.qf</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>study-auth</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.18.2</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>

        <!-- 导入其他模块 -->
        <dependency>
            <groupId>com.qf</groupId>
            <artifactId>study-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.13.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-redis</artifactId>
            <version>1.4.7.RELEASE</version>
        </dependency>

   </dependencies>

</project>
2.3.2 创建Jwt测试类进行测试
package com.qf.test;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.junit.Test;

import java.util.Calendar;
import java.util.HashMap;

public class JwtTest {

    //生成Jwt
    @Test
    public void createJwt(){

        //头部信息
        HashMap<String, Object> header = new HashMap<>();
        header.put("alg","HS256");
        header.put("typ","JWT");

        //过期时间
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND,3600);//一分钟后过期

        //创建Jwt
        String jwt = JWT.create()
                .withExpiresAt(calendar.getTime())//设置过期时间
                .withHeader(header)//设置头部信息
                .withClaim("sub", "1234567")//设置载荷信息
                .withClaim("username", "jack")//设置载荷信息
                .sign(Algorithm.HMAC256("you-secret"));

        System.out.println(jwt);
    }


    //验证
    @Test
    public void verfiyJwt(){
        //获取校验对象
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("you-secret")).build();

        //校验
        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3IiwiZXhwIjoxNjQ4MTk5ODIzLCJ1c2VybmFtZSI6ImphY2sifQ.4XUnRlFQ3tYc5jDYoRg5lFb4O5psDJYIv8Ht_SvYus";
        DecodedJWT decodedJWT = jwtVerifier.verify(token);

        System.out.println(decodedJWT);

        System.out.println("头部信息:"+decodedJWT.getHeader());
        System.out.println("载荷信息:"+decodedJWT.getPayload());
        System.out.println("签名信息:"+decodedJWT.getSignature());
        System.out.println("token:"+decodedJWT.getToken());

        //获取载荷中某一个信息
        System.out.println(decodedJWT.getClaim("username").asString());
    }
}
2.3.3 创建User
package com.qf.pojo;

import lombok.Data;

import java.io.Serializable;

@Data
public class User implements Serializable {

    private Integer id;
    private String name;
    private String password;

}
2.3.4 配置application.yml
jwt:
  # 加密秘钥
  secret: you-secret

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
2.3.5 封装JwtUtil工具类
package com.qf.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Calendar;
import java.util.Map;

@Component
public class JwtUtils {

    @Value("${jwt.secret}")
    private String signature;

    //创建Jwt
    public String createJwt(Map<String,String> map){
        System.out.println("signature:"+signature);
        //时间对象
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND,3600);//默认一小时过期
        //获取Builder对象,目的将集合中的数据,都存储到Jwt的载荷中
        JWTCreator.Builder builder = JWT.create();
        //遍历并赋值
        map.forEach( (k,v) -> { builder.withClaim(k,v);});
        //获取Jwt
        String token = builder.withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(signature));
        //返回
        return token;
    }

    //验证
    public DecodedJWT verifyJwt(String token){
        return JWT.require(Algorithm.HMAC256(signature)).build().verify(token);
    }
}
2.3.6 创建UserController
package com.qf.controller;

import com.qf.beans.BaseResultBean;
import com.qf.contants.RedisPrefix;
import com.qf.pojo.User;
import com.qf.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("user")
public class UserController {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("login")
    public BaseResultBean login(User user, HttpServletResponse response){
        //模拟数据库查询
        if("jack".equals(user.getName()) && "123".equals(user.getPassword())){
            //登录成功,颁发token
            HashMap<String, String> map = new HashMap<>();
            map.put("name",user.getName());
            //创建Jwt
            String jwt = jwtUtils.createJwt(map);
            System.out.println(jwt);

            //我们指定用户通过哪种方式携带jwt访问后台,换句话说,我们把jwt放到哪里
            //把token放到响应头中
            response.setHeader("token",jwt);

            //我们还可以把Jwt放到Redis中,每次登录成功会覆盖之前的token
            stringRedisTemplate.opsForValue()
                    //设置不同用户的key在redis中保存
                    .set(RedisPrefix.JSON_WEB_TOKEN+user.getName(),jwt,3600, TimeUnit.SECONDS);

            //登录成功
            return BaseResultBean.ok();
        }else{
            //登录失败
            return BaseResultBean.unauth();
        }
    }


    @RequestMapping("findInfo")
    public List<String> findInfo(){
        return Arrays.asList("张三","李四","王五");
    }
}
2.3.7 编写JwtFilter过滤器
package com.qf.filters;

import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qf.beans.BaseResultBean;
import com.qf.contants.RedisPrefix;
import com.qf.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

//Jwt校验
@Component
public class JwtFilter implements Filter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //访问其他接口(方法)时需要校验,访问登录则不需要校验
        HttpServletRequest request = (HttpServletRequest)servletRequest;

        //获取请求路径,判断是否是登录请求或者可以匿名访问的请求资源
        if(request.getRequestURL().toString().contains("login")){
            //放行
            filterChain.doFilter(servletRequest,servletResponse);
            return;
        }

        //访问的是其他请求,需要携带Jwt,放到请求头中
        String token = request.getHeader("token");

        //判断
        if(token != null){
            //再次判断:token是否可用
            try {
                DecodedJWT decodedJWT = jwtUtils.verifyJwt(token);
                //获取用户名
                String name = decodedJWT.getClaim("name").asString();
                //从redis中取值
                String redisToken = stringRedisTemplate.opsForValue()
                        .get(RedisPrefix.JSON_WEB_TOKEN + name);
                //判断
                if(redisToken!=null){
                    if(redisToken.trim().equals(token.trim())){
                        filterChain.doFilter(servletRequest,servletResponse);
                        return;
                    }else{
                        //错误
                        BaseResultBean baseResultBean = BaseResultBean.error();
                        servletResponse.setContentType("application/json;charset=utf-8");
                        servletResponse.getWriter().write(new ObjectMapper().writeValueAsString(baseResultBean));
                        return;
                    }
                }

            }catch (SignatureVerificationException signatureVerificationException){
                //错误
                BaseResultBean baseResultBean = BaseResultBean.error();
                servletResponse.setContentType("application/json;charset=utf-8");
                servletResponse.getWriter().write(new ObjectMapper().writeValueAsString(baseResultBean));
                return;
            }catch (TokenExpiredException tokenExpiredException){
                //token过期
                BaseResultBean baseResultBean = BaseResultBean.error();
                baseResultBean.setMsg("token已过期");
                servletResponse.setContentType("application/json;charset=utf-8");
                servletResponse.getWriter().write(new ObjectMapper().writeValueAsString(baseResultBean));
                return;
            }

            //没有出现异常则认证成功,放行
            filterChain.doFilter(servletRequest,servletResponse);
            return;
        }else{
            //未认证
            BaseResultBean baseResultBean = BaseResultBean.unauth();
            servletResponse.setContentType("application/json;charset=utf-8");
            servletResponse.getWriter().write(new ObjectMapper().writeValueAsString(baseResultBean));
        }

    }
}
2.3.8 编写FilterConfig配置类
package com.qf.config;

import com.qf.filters.JwtFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

//过滤器的配置类
@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean createJwtFilter(JwtFilter jwtFilter){
        //创建配置类对象
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        //设置过滤器
        filterRegistrationBean.setFilter(jwtFilter);
        //设置名称
        filterRegistrationBean.setName("JwtFilter");
        //设置顺序,正整数值越小,优先级越高
        filterRegistrationBean.setOrder(1);
        //设置拦截路径
        filterRegistrationBean.addUrlPatterns("/*");

        return filterRegistrationBean;
    }
}
2.3.9 启动配置类,注解添加@ServletComponentScan注解,进行测试
package com.qf;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@SpringBootApplication
@ServletComponentScan
public class AuthApp {
    public static void main(String[] args) {
        SpringApplication.run(AuthApp.class,args);
    }
}

2.4 请求参数校验

请求参数一般是指公共参数和服务参数

公共参数:指调用任何服务时,都需要传递的参数;

服务参数:只针对某个服务调用时,需要传递的参数;

用法上类似,简单来说,就是有一些服务在调用时必须传递的某些参数,比如:获取验证码时,需要传递手机号码等,这些参数一般在插入数据库时,也提前会在Redis中存储一份,我们需要做的就是对这些参数进行校验,从Redis中获取,然后判断请求是否携带这些参数,只不过有一些公共参数在Redis中存储的Key是固定的,服务参数视情况而定

2.4.1 在Redis中提前设置存储对应参数的集合
127.0.0.1:6379> sadd SYSTEM_PARAMS_KEY method charset
2.4.2 在study-common工程中创建异常参数接口
package com.qf.contants;

public interface ExceptionDict {
    
    Integer SYSTEM_PARAMS_ERROR = -20000;//公共参数错误
    Integer SYSTEM_TIME_STAMP_ERROR = -20001;//时间戳错误
    Integer SYSTEM_SIGN_ERROR = -20002;//签名错误
    Integer SYSTEM_IDEMPOTENT_ERROR = -20003;//幂等性错误
}
2.4.3 编写公共参数过滤器
package com.qf.filters;

import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qf.beans.BaseResultBean;
import com.qf.contants.ExceptionDict;
import com.qf.contants.RedisPrefix;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Set;

@Component
public class SystemParamsFilter implements Filter {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //登录接口,不需要校验
        HttpServletRequest req = (HttpServletRequest)request;
        //获取请求路径,判断是否是登录请求
        if(req.getRequestURL().toString().contains("login")){
            //放行
            chain.doFilter(request,response);
            return;
        }

        //公共参数可以存放在不同位置,我们存放在Redis中,便于后台对其管理操作
        Set<String> SystemParamsSet = stringRedisTemplate.opsForSet().members(RedisPrefix.SYSTEM_PARAMS_KEY);
        //判断
        if(!SystemParamsSet.isEmpty()){
            //遍历
            for(String param : SystemParamsSet){
                //判断参数对应的值是否为空
                String value= request.getParameter(param);
                //判空
                if(StrUtil.isEmpty(value)){
                    BaseResultBean baseResultBean = BaseResultBean.error();
                    baseResultBean.setCode(ExceptionDict.SYSTEM_PARAMS_ERROR);
                    baseResultBean.setMsg("未传递名为"+param+"参数的内容");
                    //返回数据
                    response.setContentType("application/json;charset=utf-8");
                    response.getWriter().write(new ObjectMapper().writeValueAsString(baseResultBean));
                    return;
                }else{
                    continue;
                }
            }
            //参数已传递
            chain.doFilter(request,response);
        }else{
            //没有公共参数,就不需要校验,直接放行
            chain.doFilter(request,response);
        }
    }
}
2.4.4 在FilterConfig配置类中添加配置
@Bean
public FilterRegistrationBean SystemParamsFilter(SystemParamsFilter systemParamsFilter){
    //配置类对象
    FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
    //配置对象
    filterRegistrationBean.setFilter(systemParamsFilter);
    //设置名字
    filterRegistrationBean.setName("systemParamsFilter");
    //设置顺序,正整数值越小,优先级越高
    filterRegistrationBean.setOrder(2);
    //设置拦截路径
    filterRegistrationBean.addUrlPatterns("/*");
    return filterRegistrationBean;
}

2.5 时间戳校验

为了防止请求被拦截后对数据进行修改,所以我们会要求用户传递时间戳过来,我们会对时间戳的有效期进行校验,比如我们要求有效期为1分钟,当用户传递的有效期和服务器收到的系统时间差超过1分钟的时候,我们将认为请求无效,有时候时间戳校验不能够满足需求,还可以进行进一步签名校验等...

例如:用户发起一个付款请求,请求中带着付款参数,攻击者拦截了该请求,向服务器重复的发送该请求,如果服务器端没有配置防止重放攻击策略,则可能会进行多次付款,造成用户损失

2.5.1 创建TimeStampFilter
package com.qf.filters;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.qf.beans.BaseResultBean;
import com.qf.contants.ExceptionDict;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

//时间戳校验
@Component
public class TimeStampFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //登录接口,不需要校验
        HttpServletRequest req = (HttpServletRequest)request;
        //获取请求路径,判断是否是登录请求
        if(req.getRequestURL().toString().contains("login")){
            //放行
            chain.doFilter(request,response);
            return;
        }

        //告诉用户传一个yyyy-MM-dd HH:mm:ss格式的时间戳参数
        String timestamp = req.getParameter("timestamp");
        //判断
        if(timestamp!=null){
            //格式化请求的时间
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date requestDate = null;
            try {
                 requestDate = simpleDateFormat.parse(timestamp);
            } catch (ParseException e) {
                BaseResultBean baseResultBean = new BaseResultBean();
                baseResultBean.setCode(ExceptionDict.SYSTEM_TIME_STAMP_ERROR);
                baseResultBean.setMsg("请传递一个yyyy-MM-dd HH:mm:ss格式的时间戳");
                //返回数据
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().write(new ObjectMapper().writeValueAsString(baseResultBean));
                return;
            }
            //获取系统时间
            long currentTimeMillis = System.currentTimeMillis();
            long requestDateTime = requestDate.getTime();
            //判断请求时间是否符合要求
            if(currentTimeMillis - requestDateTime > 60000 || currentTimeMillis - requestDateTime < 0){
                BaseResultBean baseResultBean = BaseResultBean.error();
                baseResultBean.setCode(ExceptionDict.SYSTEM_TIME_STAMP_ERROR);
                baseResultBean.setMsg("timestamp时间戳已失效,请重新传递");
                //返回数据
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().write(new ObjectMapper().writeValueAsString(baseResultBean));
                return;
            }else{
                chain.doFilter(request,response);
            }

        }else{
            BaseResultBean baseResultBean = BaseResultBean.error();
            baseResultBean.setCode(ExceptionDict.SYSTEM_TIME_STAMP_ERROR);
            baseResultBean.setMsg("请传递一个yyyy-MM-dd HH:mm:ss格式的timestamp时间戳");
            //返回数据
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(new ObjectMapper().writeValueAsString(baseResultBean));
            return;
        }

    }
}
2.5.2 在FilterConfig配置类中添加配置
@Bean
public FilterRegistrationBean TimeStampFilter(TimeStampFilter timeStampFilter){
    //配置类对象
    FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
    //配置对象
    filterRegistrationBean.setFilter(timeStampFilter);
    //设置名字
    filterRegistrationBean.setName("TimeStampFilter");
    //设置顺序,正整数值越小,优先级越高
    filterRegistrationBean.setOrder(3);
    //设置拦截路径
    filterRegistrationBean.addUrlPatterns("/*");
    return filterRegistrationBean;
}

2.6 签名校验

请求执行的操作可能包含敏感操作,比如牵扯到扣费等,因此我们需要对用户的请求进行验证,以防止被他人非法请求,我们使用的方式是通过签名进行校验,通过比较用户在请求时候生成传递的签名和服务器计算生成的签名进行比较,一致则通过,不一致则不通过,保证接口参数的一致性。

因为牵扯到数据的计算,所以必须存在一定的规则,并且规则要保证双方采用的一致,我们的规则如下:

  1. 将请求参数按照参数名的字典顺序进行组合 比如传递的参数是method=get&charset=utf-8

  2. 组合后的参数为charsetutf-8methodget

  3. 在上面的数据前面拼上用户的appsecret,比如用户的appsecret为qwer 则结果为:qwercharsetutf-8methodget

  4. 将上面的结果生成MD5值如:abcdefg,并将md5值以 sign为参数名添加到请求参数中

  5. 最终的请求参数为method=get&charset=utf-8&sign=abcdefg

  6. 服务端收到参数后,将除了sign外的参数按照上面的顺序再次生成sign值,并和用户传递的比较,一致则通过

2.6.1 在study-auth工程中创建Md5Util工具类
package com.qf.utils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

public class Md5Util {
    /**
     * 二行制转字符串
     */
    private static String byte2hex(byte[] b) {
        StringBuffer hs = new StringBuffer();
        String stmp = "";
        for (int n = 0; n < b.length; n++) {
            stmp = (Integer.toHexString(b[n] & 0XFF));
            if (stmp.length() == 1)
                hs.append("0").append(stmp);
            else
                hs.append(stmp);
        }
        return hs.toString().toUpperCase();
    }

    /***
     * 对请求的参数排序,生成定长的签名
     * @param  paramsMap  排序后的字符串
     * @param secret 密钥
     * */
    public static String md5Signature(Map<String, String> paramsMap, String secret) {
        String result = "";
        StringBuilder sb = new StringBuilder();
        Map<String, String> treeMap = new TreeMap<String, String>();
        treeMap.putAll(paramsMap);
        sb.append(secret);
        Iterator<String> iterator = treeMap.keySet().iterator();
        while (iterator.hasNext()) {
            String name = (String) iterator.next();
            sb.append(name).append(treeMap.get(name));
        }
        sb.append(secret);
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");             /**MD5加密,输出一个定长信息摘要*/
            result = byte2hex(md.digest(sb.toString().getBytes("utf-8")));

        } catch (Exception e) {
            throw new RuntimeException("sign error !");
        }
        return result;
    }

    /**
     * Calculates the MD5 digest and returns the value as a 16 element
     * <code>byte[]</code>.
     *
     * @param data Data to digest
     * @return MD5 digest
     */
    public static byte[] md5(String data) {
        return md5(data.getBytes());
    }

    /**
     * Calculates the MD5 digest and returns the value as a 16 element
     * <code>byte[]</code>.
     *
     * @param data Data to digest
     * @return MD5 digest
     */
    public static byte[] md5(byte[] data) {
        return getDigest().digest(data);
    }

    /**
     * Returns a MessageDigest for the given <code>algorithm</code>.
     *
     * @param
     * @return An MD5 digest instance.
     * @throws RuntimeException when a {@link NoSuchAlgorithmException} is
     *                          caught
     */

    static MessageDigest getDigest() {
        try {
            return MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}
2.6.2 编写SignFilter
package com.qf.filters;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.qf.beans.BaseResultBean;
import com.qf.contants.ExceptionDict;
import com.qf.utils.Md5Util;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;

@Component
public class SignFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //登录接口,不需要校验
        HttpServletRequest req = (HttpServletRequest)request;
        //获取请求路径,判断是否是登录请求
        if(req.getRequestURL().toString().contains("login")){
            //放行
            chain.doFilter(request,response);
            return;
        }

        //拿除了sign以外的其他参数,存入Map集合
        HashMap<String, String> map = new HashMap<>();
        //获取所有的参数名
        Enumeration<String> parameterNames = req.getParameterNames();
        //迭代
        while (parameterNames.hasMoreElements()){
            String paramName = parameterNames.nextElement();
            System.out.println(paramName);
            //判断,忽略大小写
            if(!"sign".equalsIgnoreCase(paramName)){
                String paramValue = request.getParameter(paramName);
                map.put(paramName,paramValue);
            }
        }

        System.out.println(map);

        //用户输入的密码
        String secret = "sign-password";

        //系统生成签名
        String md5Signature = Md5Util.md5Signature(map, secret).trim();
        System.out.println("系统生产的签名:"+md5Signature);

        //用户需要传递签名
        String sign = request.getParameter("sign");
        System.out.println("用户签名:"+sign);

        if(sign!=null){
            //判断
            if(md5Signature.equalsIgnoreCase(sign.trim())){
                chain.doFilter(request,response);
                return;
            }else{
                BaseResultBean baseResultBean = BaseResultBean.error();
                baseResultBean.setCode(ExceptionDict.SYSTEM_SIGN_ERROR);
                baseResultBean.setMsg("签名有误");
                //返回数据
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().write(new ObjectMapper().writeValueAsString(baseResultBean));
                return;
            }
        }else{
            BaseResultBean baseResultBean = BaseResultBean.error();
            baseResultBean.setMsg("请传递sign签名参数");
            //返回数据
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(new ObjectMapper().writeValueAsString(baseResultBean));
            return;
        }
    }
}
2.6.3 在FilterConfig中配置
@Bean
public FilterRegistrationBean SignFilter(SignFilter signFilter){
    //配置类对象
    FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
    //配置对象
    filterRegistrationBean.setFilter(signFilter);
    //设置名字
    filterRegistrationBean.setName("signFilter");
    //设置顺序,正整数值越小,优先级越高
    filterRegistrationBean.setOrder(4);
    //设置拦截路径
    filterRegistrationBean.addUrlPatterns("/*");
    return filterRegistrationBean;
}

2.7 幂等性校验

接口幂等性是指:用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生影响,例如支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条,这就没有保证接口的幂等性,等于同一个接口连续访问了两次

2.7.1 在Redis中提前设置存储对应参数的Hash
127.0.0.1:6379> hset IDEMPOTENT_apiName idempotent 1

注意:apiName 是拼接在 IDEMPOTENT_ 后面的方法名称,不同的方法设置 idempotent 的值也不同

2.7.2 编写IdempotentsFilter
package com.qf.filters;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.qf.beans.BaseResultBean;
import com.qf.contants.ExceptionDict;
import com.qf.contants.RedisPrefix;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

@Component
public class IdempotentFilter implements Filter {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //登录接口,不需要校验
        HttpServletRequest req = (HttpServletRequest)request;
        //获取请求路径,判断是否是登录请求
        if(req.getRequestURL().toString().contains("login")){
            //放行
            chain.doFilter(request,response);
            return;
        }

        //获取当前Api接口的是否需要保证幂等性
        //假设:idempotent = 1 则需要保证该接口幂等性,不能重复访问该接口
        //通过Api接口的key拿到idempotent的value
        String idempotent = stringRedisTemplate.opsForHash().
                get(RedisPrefix.IDEMPOTENT + "apiName", "idempotent").toString();
        System.out.println(idempotent);

        //判断
        if("1".equals(idempotent)){
            //通过sign来生成一个key,做为标识
            String sign = request.getParameter("sign");
            //判断
            String idempotentSign = stringRedisTemplate.opsForValue().get(RedisPrefix.IDEMPOTENT  + sign);
            if(idempotentSign==null){
                //保存idempotentSign对应的key,过期时间要和时间戳保持一致
                stringRedisTemplate.opsForValue().set(RedisPrefix.IDEMPOTENT  + sign,sign,60000, TimeUnit.MILLISECONDS);
                chain.doFilter(request,response);
                return;
            }else{
                //之前已经访问一次了,不能再访问了
                BaseResultBean baseResultBean = BaseResultBean.error();
                baseResultBean.setCode(ExceptionDict.SYSTEM_IDEMPOTENT_ERROR);
                baseResultBean.setMsg("该接口不能重复访问");
                //返回数据
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().write(new ObjectMapper().writeValueAsString(baseResultBean));
                return;
            }

        }else{
            chain.doFilter(request,response);
        }

    }
}
2.7.3 在FilterConfig中配置
@Bean
public FilterRegistrationBean IdempotentFilter(IdempotentFilter idempotentFilter){
    //配置类对象
    FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
    //配置对象
    filterRegistrationBean.setFilter(idempotentFilter);
    //设置名字
    filterRegistrationBean.setName("idempotentFilter");
    //设置顺序,正整数值越小,优先级越高
    filterRegistrationBean.setOrder(5);
    //设置拦截路径
    filterRegistrationBean.addUrlPatterns("/*");
    return filterRegistrationBean;
}
posted @ 2022-07-10 22:21  qtyanan  阅读(355)  评论(0编辑  收藏  举报