接口层面的安全措施

验签机制:

是什么(what)

验签机制是一种安全校验机制,通过代码层面进行修改,提高安全性。在请求中添加一些安全校验的参数进行校验,防止参数被篡改。同时可以引入时间戳,可以控制接口的时效性。自定义的验签规则以及验签的额外参数,也可以校验请求方是否合法。

在什么场景下用 (where)

当对接口有一定的安全性要求,且非公开接口一般都可以使用该方式。比如自己app的接口,网页的接口,服务间的接口等。

什么时候用 (when)

当发起请求时,进行验签计算,将验签结果和时间戳等参数加到请求中。服务端接收到请求时,可以用拦截器统一处理。对验签进行校验,然后再进行下一步操作。

怎么用 (how)

重头戏在这,该如何使用验签

验签的机制是,将参数进行某个规则的加密,这个规则只有你的服务端和客户端知道,参数中除了正常的请求参数外,多了一个验签参数 sign。后台拿到请求参数后,可以将参数用同样的方法加密,然后与sign比对,如果一致就认为没有被篡改。

请求参数和时间戳一起加密,因为时间戳是变化的,所以说加密出来的结果也是变化的。同时请求参数中带着时间戳参数,服务端可以根据时间戳来限制单次请求的有效时间,比如说单次请求1分钟内有效。那么之后的请求,就算原封不动的拿过来,因为时间戳的限制,不是当时的请求就是无效的,获取不到数据。然而要修改参数中的时间戳,又跟加密验签sign不一致,也得不到返回。

服务端个客户端都是我们自己的程序,或者说都是我们授权的。那么我就可以在服务端设置一个参数secret。然后我们把secret告诉给客户端。当然不是走网络 请求,而是通过线下的方式,比如说发邮件啊啥的。然后客户端会把secret存在自己本地,服务器也存一个secret。加密的时候,将secret拼接在要加密的字符串上,secret本身不进行网络传输。这样,在原文中就有一个不可见的字符,别人通过网络也拿不到真正的原文。这样破解起来就不容易了

我们还可以设置加密秘钥,服务端和客户端知道,也不参与传输。这样也更安全。而且加密不用选择对称加密,采用MD5这种非对称加密就可以,因为服务器不需要解密,服务器只用通过相同的加密规则得到一个相同的结果就可以了

然后还有数字签名,其实本质上是一样的,但数字签名更加规范一些。简单来说就是通过提供 可鉴别 的 数字信息 验证 自身身份 的一种方式。一套 数字签名 通常定义两种 互补 的运算,一个用于 签名,另一个用于 验证。分别由 发送者 持有能够 代表自己身份 的 私钥 (私钥不可泄露),由 接受者 持有与私钥对应的 公钥 ,能够在 接受 到来自发送者信息时用于 验证 其身份。

附代码:spring boot 拦截器实现

拦截器配置

@Configuration
public class ApiConfig implements WebMvcConfigurer {

    @Autowired
    private ApiInterceptor apiInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册TestInterceptor拦截器
        InterceptorRegistration registration = registry.addInterceptor(apiInterceptor);
        registration.addPathPatterns("/api/**");                      //所有路径都被拦截
    }
}

拦截器实现

@Slf4j
@Component
public class ApiInterceptor implements HandlerInterceptor {

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

    /**
     * 在请求处理之前进行调用(Controller方法调用之前)
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        try {
            log.info("请求参数:" + JSON.toJSONString(request.getParameterMap()));
            // 校验请求时间,三分钟以内  获取当前时间戳
            int nowTimestamp = (int) (System.currentTimeMillis() / 1000);
            // 获取发起请求时的时间戳
            int requestTimestamp = Integer.parseInt(request.getParameter("timestamp"));
            // 如果请求时间超过3分钟则请求无效
            if (Math.abs(nowTimestamp - requestTimestamp) > 3 * 60) {
                AjaxResult ajaxResult = AjaxResult.error("请求时间无效");
                ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
                return false;
            }
            StringBuilder sb = new StringBuilder();
            sb.append(secret);

            // 将发送请求页面中form表单里所有具有name属性的表单对象获取(包括button).返回一个Enumeration类型的枚举
            Enumeration<String> params = request.getParameterNames();
            while (params.hasMoreElements()) {
                String paramName = params.nextElement();
                // 过滤掉时间戳和sign这两个属性
                if (!paramName.equals("timestamp") && !paramName.equals("sign")) {
                    sb.append(paramName);
                    sb.append("=[");
                    sb.append(request.getParameter(paramName));
                    sb.append("]");
                }
            }
            sb.append(requestTimestamp);
            log.info("验签加密前:" + sb);

            String resultString = MD5Util.md5(sb.toString());
            log.info("验签加密后:" + resultString);

            if (resultString.equals(request.getParameter("sign"))) {
                return true;
            }
        } catch (Exception e) {
            log.error(e.toString());
        }
        AjaxResult ajaxResult = AjaxResult.error("验签校验失败");
        ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
        return false;
    }

}

token机制:

是什么(what)

token机制也是常用的一种接口授权认证的机制。token在计算机身份认证中是令牌(临时)的意思,在词法分析中是标记的意思。一般作为邀请、登录系统使用。
当用户登录后,产生一个token,之后所有用户请求,都需要携带token。当token失效请求失败

在什么场景下用 (where)

一般用在有登录场景的接口中,如app的用户登录后的一系列操作。这个也没有严格的限制,只要是涉及到用户认证的,都可以采用token机制。服务对服务间的接口,也有采用token机制的。

什么时候用 (when)

当用户登录时,传参用户名密码,有时为了防止暴力破解请求,会加入验证码,或者限制ip请求次数。当用户登录请求传到后台,后台接口验证登录用户名密码,返回一个生成的token。token具有时效性,和自动延时性。后续请求都需要携带token,token验证通过则访问成功。

登录请求不是仅仅为了验证密码返回用户信息的,而是登录要管理起来后面所有跟用户权限有关的接口,不登录是不能用的。

怎么用 (how)

我们登陆接口中可以加上一个token验证机制,就是说,如果登录成功,我们随机生成一串字符,把这个字符作为key,关联存储user对象。然后我们返回这个key。之后的所有请求都需要携带这个key,后面的请求过来后,先通过key查找缓存中是否有User对象。如果有,说明已经登录过了,就有权限访问,反之则没有权限访问。

同时,由于我们使用了key关联user对象,那么我们也不需要返回user对象。后面的请求也不需要携带user对象中的信息过来。因为我们缓存中已经存储了user对象,通过key查了出来,我们直接使用查出来的user对象作为参数就可以了。这样也避免了user对象的泄露。

key也可以添加有效时间的信息,比如说我们可以记录上一次请求key的时间,然后下一次请求就有了一个现在的时间。如果说登录操作有效时间是2小时,那么就可以拿现在的时间减去上一次请求的时间,得到一个差值,然后比较这个差值是否小于2小时,如果小于的话则是在有效时间内,允许请求。同时更新一下最后一次请求时间为当前时间,下一次请求继续判断。如果是超时了,就可以将缓存的user给删掉,直接返回错误,重新登录。用户退出也是一样的,删除掉缓存的user对象,那么以后的请求就找不到user对象,就不允许请求了

上代码:spring boot + spring security 实现,仅供参考。只展示关键代码,相关内容不再具体展示

登录控制器

/**
 * 登录验证
 * 
 * @author jdac
 */
@RestController
public class SysLoginController
{
    @Autowired
    private SysLoginService loginService;


    @Autowired
    private TokenService tokenService;

    /**
     * 登录方法
     * 
     * @param loginBody 登录信息
     * @return 结果
     */
    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }

    /**
     * 获取用户信息
     * 
     * @return 用户信息
     */
    @GetMapping("getInfo")
    public AjaxResult getInfo()
    {
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        SysUser user = loginUser.getUser();
        
        AjaxResult ajax = AjaxResult.success();
        ajax.put("user", user);
        return ajax;
    }
}

登录service方法
/**
     * 登录验证
     * 
     * @param username 用户名
     * @param password 密码
     * @param code 验证码
     * @param uuid 唯一标识
     * @return 结果
     */
    public String login(String username, String password, String code, String uuid)
    {
        boolean captchaOnOff = configService.selectCaptchaOnOff();
        // 验证码开关  这里验证码根据自己需要实现,不再进行代码展示
        if (captchaOnOff)
        {
            validateCaptcha(username, code, uuid);
        }
        // 用户验证
        Authentication authentication = null;
        try
        {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
            //记录登录信息日志用,根据自身需求去实现,不再展开描述
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            }
            else
            {
             //记录登录信息日志用,根据自身需求去实现,不再展开描述
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new CustomException(e.getMessage());
            }
        }
         //记录登录信息日志用,根据自身需求去实现,不再展开描述
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();

		//记录登录信息
        recordLoginInfo(loginUser.getUser());
        // 生成token
        return tokenService.createToken(loginUser);
    }
 	/**
     * 记录登录信息
     */
    public void recordLoginInfo(SysUser user)
    {
        user.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
        user.setLoginDate(DateUtils.getNowDate());
        userService.updateUserProfile(user);
    }

token service 相关代码


/**
 * token验证处理
 *
 * @author jdac
 */
@Component
public class TokenService
{
    // 令牌自定义标识
    @Value("${token.header}")
    private String header;

    // 令牌秘钥
    @Value("${token.secret}")
    private String secret;

    // 令牌有效期(默认30分钟)
    @Value("${token.expireTime}")
    private int expireTime;

    protected static final long MILLIS_SECOND = 1000;

    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;

    private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;

    @Autowired
    private RedisCache redisCache;

    /**
     * 获取用户身份信息
     *
     * @return 用户信息
     */
    public LoginUser getLoginUser(HttpServletRequest request)
    {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtils.isNotEmpty(token))
        {
            try
            {
                Claims claims = parseToken(token);
                // 解析对应的权限以及用户信息
                String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
                String userKey = getTokenKey(uuid);
                LoginUser user = redisCache.getCacheObject(userKey);
                return user;
            }
            catch (Exception e)
            {
            }
        }
        return null;
    }

    /**
     * 设置用户身份信息
     */
    public void setLoginUser(LoginUser loginUser)
    {
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken()))
        {
            refreshToken(loginUser);
        }
    }

    /**
     * 删除用户身份信息
     */
    public void delLoginUser(String token)
    {
        if (StringUtils.isNotEmpty(token))
        {
            String userKey = getTokenKey(token);
            redisCache.deleteObject(userKey);
        }
    }

    /**
     * 创建令牌
     *
     * @param loginUser 用户信息
     * @return 令牌
     */
    public String createToken(LoginUser loginUser)
    {
        String token = IdUtils.fastUUID();
        loginUser.setToken(token);
        setUserAgent(loginUser);
        refreshToken(loginUser);

        Map<String, Object> claims = new HashMap<>();
        claims.put(Constants.LOGIN_USER_KEY, token);
        return createToken(claims);
    }

    /**
     * 验证令牌有效期,相差不足20分钟,自动刷新缓存
     *
     * @param loginUser
     * @return 令牌
     */
    public void verifyToken(LoginUser loginUser)
    {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
        {
            refreshToken(loginUser);
        }
    }

    /**
     * 刷新令牌有效期
     *
     * @param loginUser 登录信息
     */
    public void refreshToken(LoginUser loginUser)
    {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

    /**
     * 设置用户代理信息
     *
     * @param loginUser 登录信息
     */
    public void setUserAgent(LoginUser loginUser)
    {
        UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
        String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
        loginUser.setIpaddr(ip);
        loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
        loginUser.setBrowser(userAgent.getBrowser().getName());
        loginUser.setOs(userAgent.getOperatingSystem().getName());
    }

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String createToken(Map<String, Object> claims)
    {
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
        return token;
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims parseToken(String token)
    {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token)
    {
        Claims claims = parseToken(token);
        return claims.getSubject();
    }

    /**
     * 获取请求token
     *
     * @param request
     * @return token
     */
    private String getToken(HttpServletRequest request)
    {
        String token = request.getHeader(header);
        if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
        {
            token = token.replace(Constants.TOKEN_PREFIX, "");
        }
        return token;
    }

    private String getTokenKey(String uuid)
    {
        return Constants.LOGIN_TOKEN_KEY + uuid;
    }
}


token过滤器代码

/**
 * token过滤器 验证token有效性
 * 
 * @author jdac
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    @Autowired
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
        {
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}

请求加密:

是什么(what)

这个很好理解,就是对请求参数以及返回结果的加密处理。防止请求参数或者返回结果的泄露。

在什么场景下用 (where)

对于一些敏感信息,如用户注册的手机号,身份证号。返回的用户信息,一些其他的用户敏感数据等。若被拦截泄露,会造成很严重的后果。我们对于这些数据进行加密,即使拦截到请求,也解析不了。

什么时候用 (when)

当发起请求之前,先对参数进行一下加密。服务器返回结果之前,也对返回结果进行一下加密。

怎么用 (how)

这里加密方式有很多种,但一般都是采用对称可解密的方式,否则不能解密的数据无意义。常见的有SHA、AES/DES、Base64等。

其实还有一个,平时不怎么注意。那就是HTTPS。在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性 。 HTTPS 就是在HTTP的基础上加了SSL。

这个是非代码层面,但HTTPS也不是绝对安全。如果安全等级要求低,HTTPS也够用了,不需要代码上增加工作量。如果安全等级要求比较高,那么还是需要对自己的请求进行加密的。

加解密还有一个好处,那就是能在一定程度上防止SQL注入。当然,在数据处理层还是要进行处理的,接口这里只能是一种预防拦截处理。因为SQL注入最终是落到数据层面的。

这里就不再进行代码展示了

白名单:

是什么(what)

白名单,就是一种访问限制。当请求方地址在白名单中,则允许访问,否则拒绝访问。这个比较好理解

在什么场景下用 (where)

白名单这种形式,一般是用在服务对服务间的接口时进行使用的,或者是内部服务,分配使用者ip地址。只有请求方的地址是不变的,才可以添加到白名单内。
如果是面向互联网用户的,用户换个网络都会产生不同ip,而且普通用户也不会使用顶级ip,一般都是动态代理ip。这种的无法记录请求方地址。

但是还有另一种形式,那就是添加Mac白名单。这种一般也是针对内部应用,分配用户记录Mac或者唯一序列号UUID。但这种一般仅限于电脑程序和手机app中。能够有较大的权限以及封闭性,请求不易被篡改。

获取唯一UUID其实还有个场景被忽略了,那就是用户免登录。当用户首次登录后获取手机唯一序列号带到服务器中,服务器记录用户唯一序列号。当下次再打开,只要校验唯一序列号是否存在,以及上一次登录时什么时间就可以实现免登录了。当然,免登录有很多种形式,这也只是其中之一罢了。

另外要注意一点,IP地址也可以被伪造,如果是伪造的IP,那么白名单依然是有风险存在的。

什么时候用 (when)

白名单的使用场景见上

怎么用 (how)

一般,ip白名单可以在网络层进行限制,也可以在服务代码层进行限制。这都可以。但mac或者UUID一般是只能在服务代码层进行限制。

在数据库内或者在持久化文件中,记录可以访问的ip地址。当请求过来后,获取请求头中的ip,然后判断是否在自己记录的ip地址列表中,如果在则允许访问,不在则拒绝访问。

其他:

以上的方式并不一定只能选择其一使用,而是可以进行混合使用。可以同时使用多重方式,增加系统的安全性。

我们在平时使用一些第三方接口的时候,都是一个非常好的学习机会,去看别人对于接口安全是如何处理的。

一些公开的,如聚合数据接口,只加了一个key做验证。这个key是需要登录后进行申请的。而且对于key也没有其他过多的校验。好处就是请求简单方便,坏处就是通过拦截很容易能获取到别人的key,然后就可以随意使用别人的key了。

还有如腾讯,比如小程序或者公众号的接口对接,这些就是相当麻烦。因为也是要求服务对服务的,所以也有白名单的设置,也有域名的设置。另外请求上也有一系列的token secret等。这样的请求就比较麻烦,而好处就是安全。

总结

不难发现一个问题,想要更安全,就免不了麻烦。想要简单,那么在安全性上就会有一些损失。这就需要开发者进行权衡。

另外,安全也不仅仅只有上面我说的一些。信息安全其核心包括保密性、完整性、可用性、可控性和不可否认性五个安全目标。

除了考虑接口数据的安全外,还需要考虑稳定性、可靠性。这也是安全要考虑的事情。因为不是所有的黑客攻击都是冲着你的数据来的,也有就是冲着你破坏你来的。让你的服务失效,不能用。比如DDoS攻击,就是 通过大规模的访问,让你的服务拥挤不堪,最后崩溃。这种情况下就该考虑限制请求和提高服务器并发量等去解决了。

安全方面,还有很多事情呢,慢慢学吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值