关于springboot:如何设计一个安全可靠的API接口

268次阅读

共计 20985 个字符,预计需要花费 53 分钟才能阅读完成。

起源:https://www.cnblogs.com/juren…

前言

阐明:在理论的业务中,难免会跟第三方零碎进行数据的交互与传递,那么如何保证数据在传输过程中的平安呢(防窃取)?除了 https 的协定之外,能不能加上通用的一套算法以及标准来保障传输的安全性呢?

上面咱们就来探讨下罕用的一些 API 设计的平安办法,可能不肯定是最好的,有更牛逼的实现形式,然而这篇是我本人的教训分享

目录

  1. token 简介
  2. timestamp 简介
  3. sign 简介
  4. 避免反复提交
  5. 应用流程
  6. 示例代码

1. Token 简介

Token:拜访令牌 access token, 用于接口中, 用于标识接口调用者的身份、凭证,缩小用户名和明码的传输次数。个别状况下客户端 (接口调用方) 须要先向服务器端申请一个接口调用的账号,服务器会给出一个 appId 和一个 key, key 用于参数签名应用,留神 key 保留到客户端,须要做一些平安解决,避免泄露。

Token 的值个别是 UUID,服务端生成 Token 后须要将 token 做为 key,将一些和 token 关联的信息作为 value 保留到缓存服务器中(redis),当一个申请过去后,服务器就去缓存服务器中查问这个 Token 是否存在,存在则调用接口,不存在返回接口谬误,个别通过拦截器或者过滤器来实现,Token 分为两种:

  • API Token(接口令牌): 用于拜访不须要用户登录的接口,如登录、注册、一些根本数据的获取等。获取接口令牌须要拿 appId、timestamp 和 sign 来换,sign= 加密(timestamp+key)
  • USER Token(用户令牌): 用于拜访须要用户登录之后的接口,如:获取我的根本信息、保留、批改、删除等操作。获取用户令牌须要拿用户名和明码来换

对于 Token 的时效性:token 能够是一次性的、也能够在一段时间范畴内是无效的,具体应用哪种看业务须要。个别状况下接口最好应用 https 协定,如果应用 http 协定,Token 机制只是一种缩小被黑的可能性,其实只能防小人不能防君子。

个别 token、timestamp 和 sign 三个参数会在接口中会同时作为参数传递,每个参数都有各自的用处。

2. timestamp 简介

timestamp: 工夫戳,是客户端调用接口时对应的以后工夫戳,工夫戳用于避免 DoS 攻打。当黑客劫持了申请的 url 去 DoS 攻打,每次调用接口时接口都会判断服务器以后零碎工夫和接口中传的的 timestamp 的差值,如果这个差值超过某个设置的工夫(如果 5 分钟),那么这个申请将被拦挡掉,如果在设置的超时工夫范畴内,是不能阻止 DoS 攻打的。timestamp 机制只能加重 DoS 攻打的工夫,缩短攻打工夫。如果黑客批改了工夫戳的值可通过 sign 签名机制来解决。

DoS

DoS 是 Denial of Service 的简称,即拒绝服务,造成 DoS 的攻击行为被称为 DoS 攻打,其目标是使计算机或网络无奈提供失常的服务。最常见的 DoS 攻打有计算机网络带宽攻打和连通性攻打。DoS 攻打是指成心的攻打网络协议实现的缺点或间接通过横蛮伎俩仁慈地耗尽被攻打对象的资源,目标是让指标算机或网络无奈提供失常的服务或资源拜访,使指标零碎服务零碎进行响应甚至解体,而在此攻打中并不包含侵入指标服务器或指标网络设备。

这些服务资源包含网络带宽,文件系统空间容量,凋谢的过程或者容许的连贯。这种攻打会导致资源的匮乏,无论计算机的处理速度多快、内存容量多大、网络带宽的速度多快都无奈防止这种攻打带来的结果。

  • Pingflood: 该攻打在短时间外向目标主机发送大量 ping 包,造成网络梗塞或主机资源耗尽。
  • Synflood: 该攻打以多个随机的源主机地址向目标主机发送 SYN 包,而在收到目标主机的 SYN ACK 后并不回应,这样,目标主机就为这些源主机建设了大量的连贯队列,而且因为没有收到 ACK 始终保护着这些队列,造成了资源的大量耗费而不能向失常申请提供服务。
  • Smurf:该攻打向一个子网的播送地址发一个带有特定申请(如 ICMP 回应申请)的包,并且将源地址伪装成想要攻打的主机地址。子网上所有主机都回应播送包申请而向被攻打主机发包,使该主机受到攻打。
  • Land-based:攻击者将一个包的源地址和目标地址都设置为指标主机的地址,而后将该包通过 IP 坑骗的形式发送给被攻打主机,这种包能够造成被攻打主机因试图与本人建设连贯而陷入死循环,从而很大水平地升高了零碎性能。
  • Ping of Death:依据 TCP/IP 的标准,一个包的长度最大为 65536 字节。只管一个包的长度不能超过 65536 字节,然而一个包分成的多个片段的叠加却能做到。当一个主机收到了长度大于 65536 字节的包时,就是受到了 Ping of Death 攻打,该攻打会造成主机的宕机。
  • Teardrop:IP 数据包在网络传递时,数据包能够分成更小的片段。攻击者能够通过发送两段(或者更多)数据包来实现 TearDrop 攻打。第一个包的偏移量为 0,长度为 N,第二个包的偏移量小于 N。为了合并这些数据段,TCP/IP 堆栈会调配超乎寻常的微小资源,从而造成系统资源的不足甚至机器的重新启动。
  • PingSweep:应用 ICMP Echo 轮询多个主机。

3. Sign 简介

nonce:随机值,是客户端随机生成的值,作为参数传递过去,随机值的目标是减少 sign 签名的多变性。随机值个别是数字和字母的组合,6 位长度,随机值的组成和长度没有固定规定。

sign: 个别用于参数签名,避免参数被非法篡改,最常见的是批改金额等重要敏感参数,sign 的值个别是将所有非空参数依照升续排序,再 +token+key+timestamp+nonce(随机数)拼接在一起,最初应用某种加密算法进行加密,作为接口中的一个参数 sign 来传递,也能够将 sign 放到申请头中。

接口在网络传输过程中如果被黑客挟持,并批改其中的参数值,而后再持续调用接口,尽管参数的值被批改了,然而因为黑客不晓得 sign 是如何计算出来的,不晓得 sign 都有哪些值形成,不晓得以怎么的程序拼接在一起的,最重要的是不晓得签名字符串中的 key 是什么,所以黑客能够篡改参数的值,但没法批改 sign 的值,当服务器调用接口前会依照 sign 的规定从新计算出 sign 的值而后和接口传递的 sign 参数的值做比拟,如果相等示意参数值没有被篡改,如果不等,示意参数被非法篡改了,就不执行接口了。

4. 避免反复提交

对于一些重要的操作须要避免客户端反复提交的(如非幂等性重要操作),具体办法是当申请第一次提交时将 sign 作为 key 保留到 redis,并设置超时工夫,超时工夫和 Timestamp 中设置的差值雷同。当同一个申请第二次拜访时会先检测 redis 是否存在该 sign,如果存在则证实反复提交了,接口就不再持续调用了。

如果 sign 在缓存服务器中因过期工夫到了,而被删除了,此时当这个 url 再次申请服务器时,因 token 的过期工夫和 sign 的过期工夫始终,sign 过期也意味着 token 过期,那样同样的 url 再拜访服务器会因 token 谬误会被拦挡掉,这就是为什么 sign 和 token 的过期工夫要保持一致的起因。回绝反复调用机制确保 URL 被他人截获了也无奈应用(如抓取数据)。

对于哪些接口须要避免反复提交能够自定义个注解来标记。

留神:所有的安全措施都用上的话有时候不免太过简单,在理论我的项目中须要依据本身状况作出裁剪,比方能够只应用签名机制就能够保障信息不会被篡改,或者定向提供服务的时候只用 Token 机制就能够了。如何裁剪,全看我的项目理论状况和对接口安全性的要求。

5. 应用流程

  1. 接口调用方 (客户端) 向接口提供方 (服务器) 申请接口调用账号,申请胜利后,接口提供方会给接口调用方一个 appId 和一个 key 参数
  2. 客户端携带参数 appId、timestamp、sign 去调用服务器端的 API token,其中 sign= 加密(appId + timestamp+key)
  3. 客户端拿着 api\_token 去拜访不须要登录就能拜访的接口
  4. 当拜访用户须要登录的接口时,客户端跳转到登录页面,通过用户名和明码调用登录接口,登录接口会返回一个 usertoken, 客户端拿着 usertoken 去拜访须要登录能力拜访的接口

sign 的作用是避免参数被篡改,客户端调用服务端时须要传递 sign 参数,服务器响应客户端时也能够返回一个 sign 用于客户度校验返回的值是否被非法篡改了。客户端传的 sign 和服务器端响应的 sign 算法可能会不同。

6. 示例代码

1. dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

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

2. RedisConfiguration

@Configuration
public class RedisConfiguration {
    @Bean
    public JedisConnectionFactory jedisConnectionFactory(){return new JedisConnectionFactory();
    }

    /**
     * 反对存储对象
     * @return
     */
    @Bean
    public RedisTemplate<String, String> redisTemplate(){RedisTemplate<String, String> redisTemplate = new StringRedisTemplate();
        redisTemplate.setConnectionFactory(jedisConnectionFactory());
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}

3. TokenController

@Slf4j
@RestController
@RequestMapping("/api/token")
public class TokenController {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * API Token
     *
     * @param sign
     * @return
     */
    @PostMapping("/api_token")
    public ApiResponse<AccessToken> apiToken(String appId, @RequestHeader("timestamp") String timestamp, @RequestHeader("sign") String sign) {Assert.isTrue(!StringUtils.isEmpty(appId) && !StringUtils.isEmpty(timestamp) && !StringUtils.isEmpty(sign), "参数谬误");

        long reqeustInterval = System.currentTimeMillis() - Long.valueOf(timestamp);
        Assert.isTrue(reqeustInterval < 5 * 60 * 1000, "申请过期,请从新申请");

        // 1. 依据 appId 查询数据库获取 appSecret
        AppInfo appInfo = new AppInfo("1", "12345678954556");

        // 2. 校验签名
        String signString = timestamp + appId + appInfo.getKey();
        String signature = MD5Util.encode(signString);
        log.info(signature);
        Assert.isTrue(signature.equals(sign), "签名谬误");

        // 3. 如果正确生成一个 token 保留到 redis 中,如果谬误返回错误信息
        AccessToken accessToken = this.saveToken(0, appInfo, null);

        return ApiResponse.success(accessToken);
    }


    @NotRepeatSubmit(5000)
    @PostMapping("user_token")
    public ApiResponse<UserInfo> userToken(String username, String password) {// 依据用户名查问明码, 并比拟明码(明码能够 RSA 加密一下)
        UserInfo userInfo = new UserInfo(username, "81255cb0dca1a5f304328a70ac85dcbd", "111111");
        String pwd = password + userInfo.getSalt();
        String passwordMD5 = MD5Util.encode(pwd);
        Assert.isTrue(passwordMD5.equals(userInfo.getPassword()), "明码谬误");

        // 2. 保留 Token
        AppInfo appInfo = new AppInfo("1", "12345678954556");
        AccessToken accessToken = this.saveToken(1, appInfo, userInfo);
        userInfo.setAccessToken(accessToken);
        return ApiResponse.success(userInfo);
    }

    private AccessToken saveToken(int tokenType, AppInfo appInfo,  UserInfo userInfo) {String token = UUID.randomUUID().toString();

        // token 有效期为 2 小时
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        calendar.add(Calendar.SECOND, 7200);
        Date expireTime = calendar.getTime();

        // 4. 保留 token
        ValueOperations<String, TokenInfo> operations = redisTemplate.opsForValue();
        TokenInfo tokenInfo = new TokenInfo();
        tokenInfo.setTokenType(tokenType);
        tokenInfo.setAppInfo(appInfo);

        if (tokenType == 1) {tokenInfo.setUserInfo(userInfo);
        }

        operations.set(token, tokenInfo, 7200, TimeUnit.SECONDS);

        AccessToken accessToken = new AccessToken(token, expireTime);

        return accessToken;
    }

    public static void main(String[] args) {long timestamp = System.currentTimeMillis();
        System.out.println(timestamp);
        String signString = timestamp + "1" + "12345678954556";
        String sign = MD5Util.encode(signString);
        System.out.println(sign);

        System.out.println("-------------------");
        signString = "password=123456&username=1&12345678954556" + "ff03e64b-427b-45a7-b78b-47d9e8597d3b1529815393153sdfsdfsfs" + timestamp + "A1scr6";
        sign = MD5Util.encode(signString);
        System.out.println(sign);
    }
}

4. WebMvcConfiguration

@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {private static final String[] excludePathPatterns  = {"/api/token/api_token"};

    @Autowired
    private TokenInterceptor tokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {super.addInterceptors(registry);
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns(excludePathPatterns);
    }
}

5. TokenInterceptor

@Component
public class TokenInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     *
     * @param request
     * @param response
     * @param handler 拜访的指标办法
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("token");
        String timestamp = request.getHeader("timestamp");
        // 随机字符串
        String nonce = request.getHeader("nonce");
        String sign = request.getHeader("sign");
        Assert.isTrue(!StringUtils.isEmpty(token) && !StringUtils.isEmpty(timestamp) && !StringUtils.isEmpty(sign), "参数谬误");

        // 获取超时工夫
        NotRepeatSubmit notRepeatSubmit = ApiUtil.getNotRepeatSubmit(handler);
        long expireTime = notRepeatSubmit == null ? 5 * 60 * 1000 : notRepeatSubmit.value();

        // 2. 申请工夫距离
        long reqeustInterval = System.currentTimeMillis() - Long.valueOf(timestamp);
        Assert.isTrue(reqeustInterval < expireTime, "申请超时,请从新申请");

        // 3. 校验 Token 是否存在
        ValueOperations<String, TokenInfo> tokenRedis = redisTemplate.opsForValue();
        TokenInfo tokenInfo = tokenRedis.get(token);
        Assert.notNull(tokenInfo, "token 谬误");

        // 4. 校验签名(将所有的参数加进来,避免他人篡改参数) 所有参数看参数名升续排序拼接成 url
        // 申请参数 + token + timestamp + nonce
        String signString = ApiUtil.concatSignString(request) + tokenInfo.getAppInfo().getKey() + token + timestamp + nonce;
        String signature = MD5Util.encode(signString);
        boolean flag = signature.equals(sign);
        Assert.isTrue(flag, "签名谬误");

        // 5. 回绝反复调用(第一次拜访时存储,过期工夫和申请超时工夫保持一致), 只有标注不容许反复提交注解的才会校验
        if (notRepeatSubmit != null) {ValueOperations<String, Integer> signRedis = redisTemplate.opsForValue();
            boolean exists = redisTemplate.hasKey(sign);
            Assert.isTrue(!exists, "请勿反复提交");
            signRedis.set(sign, 0, expireTime, TimeUnit.MILLISECONDS);
        }

        return super.preHandle(request, response, handler);
    }
}

6. MD5Util —-MD5 工具类,加密生成数字签名

public class MD5Util {private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };

    private static String byteArrayToHexString(byte b[]) {StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();}

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    public static String encode(String origin) {return encode(origin, "UTF-8");
    }
    public static String encode(String origin, String charsetname) {
        String resultString = null;
        try {resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname))
                resultString = byteArrayToHexString(md.digest(resultString
                        .getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString
                        .getBytes(charsetname)));
        } catch (Exception exception) { }
        return resultString;
    }
}

7. @NotRepeatSubmit  —– 自定义注解,避免反复提交。

/**
 * 禁止反复提交
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotRepeatSubmit {
    /** 过期工夫,单位毫秒 **/
    long value() default 5000;}

8. AccessToken

@Data
@AllArgsConstructor
public class AccessToken {
    /** token */
    private String token;

    /** 生效工夫 */
    private Date expireTime;
}

9. AppInfo

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AppInfo {
    /** App id */
    private String appId;
    /** API 秘钥 */
    private String key;
}

10. TokenInfo

@Data
public class TokenInfo {
    /** token 类型: api:0、user:1 */
    private Integer tokenType;

    /** App 信息 */
    private AppInfo appInfo;

    /** 用户其余数据 */
    private UserInfo userInfo;
}

11. UserInfo

@Data
public class UserInfo {
    /** 用户名 */
    private String username;
    /** 手机号 */
    private String mobile;
    /** 邮箱 */
    private String email;
    /** 明码 */
    private String password;
    /** 盐 */
    private String salt;

    private AccessToken accessToken;

    public UserInfo(String username, String password, String salt) {
        this.username = username;
        this.password = password;
        this.salt = salt;
    }
}

12. ApiCodeEnum

/**
 * 错误码 code 能够应用纯数字, 应用不同区间标识一类谬误,也能够应用纯字符,也能够应用前缀 + 编号
 *
 * 错误码:ERR + 编号
 *
 * 能够应用日志级别的前缀作为谬误类型辨别 Info(I) Error(E) Warning(W)
 *
 * 或者以业务模块 + 谬误号
 *
 * TODO 错误码设计
 *
 * Alipay 用了两个 code,两个 msg(https://docs.open.alipay.com/api_1/alipay.trade.pay)
 */
public enum ApiCodeEnum {SUCCESS("10000", "success"),
    UNKNOW_ERROR("ERR0001","未知谬误"),
    PARAMETER_ERROR("ERR0002","参数谬误"),
    TOKEN_EXPIRE("ERR0003","认证过期"),
    REQUEST_TIMEOUT("ERR0004","申请超时"),
    SIGN_ERROR("ERR0005","签名谬误"),
    REPEAT_SUBMIT("ERR0006","请不要频繁操作"),
    ;

    /** 代码 */
    private String code;

    /** 后果 */
    private String msg;

    ApiCodeEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public String getCode() {return code;}

    public String getMsg() {return msg;}
}

13. ApiResult

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResult {

    /** 代码 */
    private String code;

    /** 后果 */
    private String msg;
}

14. ApiUtil ——- 这个参考支付宝加密的算法写的. 我间接 Copy 过去了。

public class ApiUtil {

    /**
     * 按参数名升续拼接参数
     * @param request
     * @return
     */
    public static String concatSignString(HttpServletRequest request) {Map<String, String> paramterMap = new HashMap<>();
        request.getParameterMap().forEach((key, value) -> paramterMap.put(key, value[0]));
        // 依照 key 升续排序,而后拼接参数
        Set<String> keySet = paramterMap.keySet();
        String[] keyArray = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArray);
        StringBuilder sb = new StringBuilder();
        for (String k : keyArray) {
            // 或略掉的字段
            if (k.equals("sign")) {continue;}
            if (paramterMap.get(k).trim().length() > 0) {
                // 参数值为空,则不参加签名
                sb.append(k).append("=").append(paramterMap.get(k).trim()).append("&");
            }
        }

        return sb.toString();}

    public static String concatSignString(Map<String, String> map) {Map<String, String> paramterMap = new HashMap<>();
        map.forEach((key, value) -> paramterMap.put(key, value));
        // 依照 key 升续排序,而后拼接参数
        Set<String> keySet = paramterMap.keySet();
        String[] keyArray = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArray);
        StringBuilder sb = new StringBuilder();
        for (String k : keyArray) {if (paramterMap.get(k).trim().length() > 0) {
                // 参数值为空,则不参加签名
                sb.append(k).append("=").append(paramterMap.get(k).trim()).append("&");
            }
        }
        return sb.toString();}

    /**
     * 获取办法上的 @NotRepeatSubmit 注解
     * @param handler
     * @return
     */
    public static NotRepeatSubmit getNotRepeatSubmit(Object handler) {if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            NotRepeatSubmit annotation = method.getAnnotation(NotRepeatSubmit.class);

            return annotation;
        }

        return null;
    }
}

15. ApiResponse

@Data
@Slf4j
public class ApiResponse<T> {
    /** 后果 */
    private ApiResult result;

    /** 数据 */
    private T data;

    /** 签名 */
    private String sign;


    public static <T> ApiResponse success(T data) {return response(ApiCodeEnum.SUCCESS.getCode(), ApiCodeEnum.SUCCESS.getMsg(), data);
    }

    public static ApiResponse error(String code, String msg) {return response(code, msg, null);
    }

    public static <T> ApiResponse response(String code, String msg, T data) {ApiResult result = new ApiResult(code, msg);
        ApiResponse response = new ApiResponse();
        response.setResult(result);
        response.setData(data);

        String sign = signData(data);
        response.setSign(sign);

        return response;
    }

    private static <T> String signData(T data) {
        // TODO 查问 key
        String key = "12345678954556";
        Map<String, String> responseMap = null;
        try {responseMap = getFields(data);
        } catch (IllegalAccessException e) {return null;}
        String urlComponent = ApiUtil.concatSignString(responseMap);
        String signature = urlComponent + "key=" + key;
        String sign = MD5Util.encode(signature);

        return sign;
    }

    /**
     * @param data 反射的对象, 获取对象的字段名和值
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     */
    public static Map<String, String> getFields(Object data) throws IllegalAccessException, IllegalArgumentException {if (data == null) return null;
        Map<String, String> map = new HashMap<>();
        Field[] fields = data.getClass().getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {Field field = fields[i];
            field.setAccessible(true);

            String name = field.getName();
            Object value = field.get(data);
            if (field.get(data) != null) {map.put(name, value.toString());
            }
        }

        return map;
    }
}

7. ThreadLocal

ThreadLocal 是线程内的全局上下文。就是在单个线程中,办法之间共享的内存,每个办法都能够从该上下文中获取值和批改值。

理论案例:

在调用 api 时都会传一个 token 参数,通常会写一个拦截器来校验 token 是否非法,咱们能够通过 token 找到对应的用户信息(User),如果 token 非法,而后将用户信息存储到 ThreadLocal 中,这样无论是在 controller、service、dao 的哪一层都能拜访到该用户的信息。作用相似于 Web 中的 request 作用域。

传统形式咱们要在办法中拜访某个变量,能够通过传参的模式往办法中传参,如果多个办法都要应用那么每个办法都要传参;如果应用 ThreadLocal 所有办法就不须要传该参数了,每个办法都能够通过 ThreadLocal 来拜访该值。

  • ThreadLocalUtil.set(“key”, value); 保留值
  • T value = ThreadLocalUtil.get(“key”); 获取值

ThreadLocalUtil

public class ThreadLocalUtil<T> {private static final ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal() {
        @Override
        protected Map<String, Object> initialValue() {return new HashMap<>(4);
        }
    };


    public static Map<String, Object> getThreadLocal(){return threadLocal.get();
    }

    public static <T> T get(String key) {Map map = (Map)threadLocal.get();
        return (T)map.get(key);
    }

    public static <T> T get(String key,T defaultValue) {Map map = (Map)threadLocal.get();
        return (T)map.get(key) == null ? defaultValue : (T)map.get(key);
    }

    public static void set(String key, Object value) {Map map = (Map)threadLocal.get();
        map.put(key, value);
    }

    public static void set(Map<String, Object> keyValueMap) {Map map = (Map)threadLocal.get();
        map.putAll(keyValueMap);
    }

    public static void remove() {threadLocal.remove();
    }

    public static <T> Map<String,T> fetchVarsByPrefix(String prefix) {Map<String,T> vars = new HashMap<>();
        if(prefix == null){return vars;}
        Map map = (Map)threadLocal.get();
        Set<Map.Entry> set = map.entrySet();

        for(Map.Entry entry : set){Object key = entry.getKey();
            if(key instanceof String){if( ((String) key).startsWith(prefix) ){vars.put((String)key,(T)entry.getValue());
                }
            }
        }
        return vars;
    }

    public static <T> T remove(String key) {Map map = (Map)threadLocal.get();
        return (T)map.remove(key);
    }

    public static void clear(String prefix) {if( prefix == null){return;}
        Map map = (Map)threadLocal.get();
        Set<Map.Entry> set = map.entrySet();
        List<String> removeKeys = new ArrayList<>();

        for(Map.Entry entry : set){Object key = entry.getKey();
            if(key instanceof String){if( ((String) key).startsWith(prefix) ){removeKeys.add((String)key);
                }
            }
        }
        for(String key : removeKeys){map.remove(key);
        }
    }
}

总结

这个是目前第三方数据接口交互过程中罕用的一些参数与应用示例,心愿对大家有点帮忙。

当然如果为了保障更加的平安,能够加上 RSA,RSA2,AES 等等加密形式,保障了数据的更加的平安,然而惟一的毛病是加密与解密比拟消耗 CPU 的资源.

正文完
 0