学习微服务系列(九):springboot服务接口安全认证设计

5,494 阅读9分钟

现阶段我们开发的服务都是对外提供接口,包括bs结构的项目都是进行前后端分离,服务端通过接口的形式给前端提供业务逻辑和数据支持。同时现在都是多团队合作进行开发服务之间都是通过接口进行调用传输数据。那么就涉及接口调用的时候安全性的思考。这就需要API使用签名方法(Sign)对接口进行鉴权。每一次请求都需要在请求中包含签名信息, 以验证用户身份,不然任何人都可以通过脚本调用我们的接口,会导致安全隐患。

安全认证好处

  1. 验证调用方合法性
  2. 防止篡改
  3. 防止重放攻击

安全认证大致分类

基于动态签名类

使用这类安全认证的主要是如调用三方接口,比如有另外一个团队需要请求你们的接口,还有比如APP进行服务端接口的调用或者类似微信小程序调用服务端的接口都可以使用这类的加密方式。我们举个使用动态签名进行安全验证的例子:

思路:每次的请求,根据参数不同,按照一定算法规则动态生成相应的签名。

步骤:

服务调用方:

  1. 将全部请求的参数按参数名称进行字典排序
  2. 将排序好的参数对应的值,全部用字符串依次拼接起来 + 时间戳(用于限制请求频次)
  3. 在最后追加appKey(服务提供方提供)
  4. 进行md5
  5. 得到签名sign 放到请求header中进行传递给服务提供方

服务提供方:

  1. 获取到请求参数获取时间戳跟当前时间戳做差判断请求是否在单位时间差之内
  2. 按照加密规则进行加密与传递过来的sign值进行比对

核心方法获取sign:

public String getSignToken(Map<String, String> map) {
        String result = "";
        try {
            List<Map.Entry<String, String>> infoIds = new ArrayList<Map.Entry<String, String>>(map.entrySet());
            // 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序)
            Collections.sort(infoIds, new Comparator<Map.Entry<String, String>>() {

                public int compare(Map.Entry<String, String> o1, Map.Entry<String, String> o2) {
                    return (o1.getKey()).toString().compareTo(o2.getKey());
                }
            });
            // 构造签名键值对的格式
            StringBuilder sb = new StringBuilder();
            for (Map.Entry<String, String> item : infoIds) {
                if (item.getKey() != null || item.getKey() != "") {
                    String key = item.getKey();
                    String val = item.getValue();
                    if (!(val == "" || val == null)) {
                        sb.append(val);
                    }
                }
            }
            result = sb.toString()+appKey;
            //进行MD5加密
            result = Md5Util.getMD5(result);
        } catch (Exception e) {
            return null;
        }
        return result;
}

此处想起来个问题,就是好多小伙伴一直有这个疑问就是我用了https了那我还有必要进行签名延签的形式进行接口安全验证么?在这回答一下:

  • API 签名保证的是应用的数据安全和防篡改,并且可以作为业务的参数校验和处理重放攻击。
  • HTTPS 保证的是运输层的加密传输,但是无法防御重放攻击。

换句话说,HTTPS 保证通过中间人攻击抓到的报文是密文,无法或者说很难破解。但仍然可以将报文重发,形成 DDOS。同时,如果不签名,只用 HTTP 简单认证,通过抓包就可以随意发起请求了。因此最安全的方法就是结合 HTTPS 和 API 签名。

基于用户登录验证类

  • Token 验证

使用基于 Token 的身份验证方法,大概的流程是这样的:

  1. 客户端使用用户名跟密码请求登录   
  2. 服务端收到请求,去验证用户名与密码    
  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端    
  4. 客户端收到 Token 以后可以把它存储起来
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token    
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

总的来说就是客户端在首次登陆以后,服务端再次接收http请求的时候,就只认token了,请求只要每次把token带上就行了,服务器端会拦截所有的请求,然后校验token的合法性,合法就放行,不合法就返回401(鉴权失败)。 Springboot其实就是根据这个原理进行验证的。

Jwt全称是:json web token。它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证。

jwt优点:

  1. 简洁: 可以通过URL、POST参数或者在HTTP header发送,因为数据量小,传输速度也很快;
  2. 自包含:负载中可以包含用户所需要的信息,避免了多次查询数据库;
  3. 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持;
  4. 不需要在服务端保存会话信息,特别适用于分布式微服务。 jwt缺点:
  5. 无法作废已颁布的令牌;
  6. 不易应对数据过期。

注意一点不要试图用jwt去代替session。这种模式下其实传统的session+cookie机制工作的更好,jwt因为其无状态和分布式,事实上只要在有效期内,是无法作废的,用户的签退更多是一个客户端的签退,服务端token仍然有效,你只要使用这个token,仍然可以登陆系统。另外一个问题是续签问题,使用token,无疑令续签变得十分麻烦,当然你也可以通过redis去记录token状态,并在用户访问后更新这个状态,但这就是硬生生把jwt的无状态搞成有状态了,而这些在传统的session+cookie机制中都是不需要去考虑的。

OAuth2认证

举个例子说明:假如你们公司正在开发一个服务应用,该应用会需要在微信进行登录,你们的应用需要收集到用户的姓名,头像,地域等信息,那么你的应用如何才能拿到所有参与活动的微信用户的基本信息呢? 就需要到微信所在服务器去获取微信用户的信息认证。 在上面的例子中有几个角色:

  1. 资源所有者:微信用户
  2. 资源服务:微信服务器
  3. 第三方应用:你们自己开发的服务应用

所以我们可以看出OAuht2 就是使用资源服务器提供一个访问凭据给到第三方应用,让第三方应用可以在不知道资源所有者在资源服务器上的账号和密码的情况下,能获取到资源所有者在资源服务器上的资源。举个我们曾经开发一个微信小程序的例子说明一下:

  1. 微信用户1,访问我们开发的小程序,小程序即向微信授权服务器发起授权请求以获取该微信用户1在微信服务器上的姓名
  2. 微信授权服务器接收到我们的授权请求,引导用户确认授权后,返回授权许可给到我们小程序
  3. 我们拿到授权许可code后,再次向微信授权服务器发起访问令牌的请求(携带用户的app_id等)
  4. 微信授权服务器验证我们的身份以及授权许可code,验证通过后将下发访问令牌access_code
  5. 我们拿到访问令牌后向微信资源服务器发起请求资源,即请求微信用户1的姓名

基于加密解密类

  • 对称加密

双方使用的同一个密钥,既可以加密又可以解密,这种加密方法称为对称加密,也称为单密钥加密。

  1. 优点:速度快,对称性加密通常在消息发送方需要加密大量数据时使用,算法公开、计算量小、加密速度快、加密效率高。
  2. 缺点:在数据传送前,发送方和接收方必须商定好秘钥,然后 使双方都能保存好秘钥。其次如果一方的秘钥被泄露,那么加密信息也就不安全了。 常用算法: AES:密钥的长度可以为128、192和256位,也就是16个字节、24个字节和32个字节 DES:密钥的长度64位,8个字节。
  • 非对称加密 一对密钥由公钥和私钥组成。私钥解密公钥加密数据,公钥解密私钥加密数据(私钥公钥可以互相加密解密)。
  1. 缺点:速度较慢
  2. 优点:安全 常用算法: RSA 来个工具类大家一看就懂:
public class Demo {

    private static String src = "这是加密的字符串";

    private static RSAPublicKey rsaPublicKey;
    private static RSAPrivateKey rsaPrivateKey;

    static {
        // 1、初始化密钥
        KeyPairGenerator keyPairGenerator;
        try {
            keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(1024);// 64的整倍数
            KeyPair keyPair = keyPairGenerator.generateKeyPair();
            rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
            rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
            System.out.println("Public Key : " + Base64.encodeBase64String(rsaPublicKey.getEncoded()));
            System.out.println("Private Key : " + Base64.encodeBase64String(rsaPrivateKey.getEncoded()));
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }

    /**
     * 公钥加密,私钥解密
     */
    public static void pubEn2PriDe() throws Exception{
        //公钥加密
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(rsaPublicKey.getEncoded());
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] result = cipher.doFinal(src.getBytes());
        System.out.println("公钥加密,私钥解密 --加密: " + Base64.encodeBase64String(result));

        //私钥解密
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(rsaPrivateKey.getEncoded());
        keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
        cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        result = cipher.doFinal(result);
        System.out.println("公钥加密,私钥解密 --解密: " + new String(result));
    }


    /**
     * 私钥加密,公钥解密
     */
    public static void priEn2PubDe() throws Exception{

        //私钥加密
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(rsaPrivateKey.getEncoded());
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);
        byte[] result = cipher.doFinal(src.getBytes());
        System.out.println("私钥加密,公钥解密 --加密 : " + Base64.encodeBase64String(result));

        //公钥解密
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(rsaPublicKey.getEncoded());
        keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
        cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, publicKey);
        result = cipher.doFinal(result);
        System.out.println("私钥加密,公钥解密   --解密: " + new String(result));
    }

    public static void main(String[] args) {
        try {
            pubEn2PriDe();  //公钥加密,私钥解密
            priEn2PubDe();  //私钥加密,公钥解密
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

结果: