GDPR、Cookie 同意横幅 Flutter web

Vin*_*nzo 6 cookies flutter-web

我正在使用 Flutter 构建我的网站,但网络编程对我来说非常陌生,我不太确定我到底理解 Cookie 是如何工作的。我仍然需要了解哪些 cookie 将写入何处,以及我从哪里获取这些 cookie。构建横幅来管理应该很容易,如果我没记错的话,它应该是主页中弹出的第一件事。例如,中横幅只是一个可关闭的横幅,摆动消息 To make Medium work, we log user data. By using Medium, you agree to our Privacy Policy. 带有隐私政策链接的消息,但它没有任何选择加入,因此它看起来不符合 GDPR。

这里https://medium.com/@mayur_solanki/flutter-web-forming-hummingbird-ba52f5135fb0展示了如何在flutter web中写入和读取cookie

html.window.localStorage['key']
html.window.sessionStorage['key']
html.window.document.cookie

html.window.localStorage['local_value'] = localStorage;
html.window.sessionStorage['session_value'] = sessionStorage;
html.window.document.cookie= "username=${cookies}; expires=Thu, 18 Dec 2020 12:00:00 UTC";
Run Code Online (Sandbox Code Playgroud)

据我了解,cookies 就是这些类型。

第一方:为了跟踪用户行为(页面访问、用户数量等),当我使用谷歌分析时,我确实需要征求这些行为的同意。这里展示了Google Analytics、Flutter、cookie 和通用数据保护条例 (GDPR)如何激活/停用它,所以如果我没有错的话,我不应该自己存储任何内容。

第三方:例如,这些内容可能来自我主页上的 YouTube 链接视频,因此我也需要征求这些内容的同意。还没有检查过,但我想它应该类似于 Google Analytics

会话cookie:这些应该用于记住购物篮中的物品。我不应该需要这些..

持久cookie:这些应该用于保持用户登录。其中一个网页是Retailer access,它是零售商的应用程序(市场的供应方)。我正在使用 Google 签名来登录用户,所以我应该需要这些,因为Retailer access即使在用户登录后导航时,它也始终会向用户呈现登录表单。

安全 cookie:这些 cookie 仅适用于 https,用于结帐/付款页面。在我的网络中,零售商的应用程序仅创建产品、管理研讨会预订以及处理与客户的沟通。移动应用程序(市场的需求方)是使用 Stripe 进行所有付款的地方,因此我不必存储网上有什么东西..对吧?

抱歉问了这么长的问题,我希望它足够清楚。感谢您的帮助。

hnn*_*lch 3

我基本上遇到了这个问题,因为我还使用第三方脚本(firebase、stripe...),并且在运行任何这些脚本之前我需要用户的同意。

我围绕 Yett ( https://github.com/elbywan/yett )构建解决方案,它会阻止属于先前定义的黑名单的脚本。你甚至可以自己实现这个功能,作者写了一篇有趣的媒体文章

就我而言,我只有“基本”脚本,因此我构建了一个解决方案,其中仅当用户同意所有必要的脚本时,flutter 应用程序才会加载。但如果需要对用户的 cookie 设置进行更细粒度的控制,调整此解决方案应该不会太困难,并且我添加了第二个“分析”条目作为可能的起点。

我将用户的设置存储在 localStorage 中,并直接在应用程序启动时检索它们以创建黑名单并决定是否应显示 cookie 横幅。

这是我的index.html

它引用以下脚本:get_consent.jsset_consent.js和(有关它们init_firebase.jsload_app.js更多信息如下)。

<!DOCTYPE html>
<html>
<head>
  <!--
    If you are serving your web app in a path other than the root, change the
    href value below to reflect the base path you are serving from.

    The path provided below has to start and end with a slash "/" in order for
    it to work correctly.

    For more details:
    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
  -->
  <base href="/">

  <meta charset="UTF-8">
  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
  <meta name="description" content="A new Flutter project.">

  <!-- iOS meta tags & icons -->
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="flutter_utils">
  <link rel="apple-touch-icon" href="icons/Icon-192.png">
  <!-- Assigns blacklist of urls based on localStorage (must be placed before yett script) -->
  <script src="get_consent.js"></script>
  <!-- Yett is used to block all third-party scripts that are part of the blacklist (must be placed before all other (third-party) scripts) -->
  <script src="https://unpkg.com/yett"></script>
  <script src="https://js.stripe.com/v3/"></script>

  <title>flutter_utils</title>
  <link rel="manifest" href="manifest.json">
  <link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
  <!-- The standard consent popup (hidden by default) -->
  <div id="consent-popup" class="hidden consent-div">
    <h2>We use cookies and other technologies</h2>
    <p>This website uses cookies and similar functions to process end device information and personal data. The processing serves the integration of content, external services and elements of third parties, statistical analysis/measurement, personalized advertising and the integration of social media. Depending on the function, data may be passed on to third parties within the EU in the process. Your consent is always voluntary, not required for the use of our website and can be rejected or revoked at any time via the icon at the bottom right.
    </p>
    <div>
      <button id="accept-btn" class="btn inline">Accept</button>
      <button id="reject-btn" class="btn inline">Reject</button>
      <button id="info-btn" class="btn inline">More info</button>    
    </div>
  </div>
  <!-- Detailed consent popup allows the user to control scripts by their category -->
  <div id="consent-popup-details" class="hidden consent-div">
    <h2>Choose what to accept</h2>
    <div>
      <div class="row-div">
        <h3>Essential</h3>
        <label class="switch">
          <!-- Essentials must always be checked -->
          <input id="essential-cb" type="checkbox" checked disabled=true>
          <span class="slider round"></span>
        </label>    
      </div>
      <p>
      Here you can find all technically necessary scripts, cookies and other elements that are necessary for the operation of the website.
      </p>
    </div>
        <div>
      <div class="row-div">
        <h3>Analytics</h3>
        <label class="switch">
          <input id ="analytics-cb" type="checkbox">
          <span class="slider round"></span>
        </label>    
      </div>
      <p>
      For the site, visitors, web page views and diveerse other data are stored anonymously.
      </p>
    </div>
    <div>
      <button id="save-btn" class="btn inline">Save</button>
      <button id="cancel-btn" class="btn inline">Cancel</button>   
    </div>
  </div>
  <!-- Updates localStorage with user's cookie settings -->
  <script src="set_consent.js"></script>
  <script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-auth.js"></script>
  <script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-firestore.js"></script>
  <script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-storage.js"></script>
  <!-- Initializes firebase (if user gave consent) -->
  <script src="init_firebase.js"></script>
  <!-- Loads flutter app (if user gave consent) -->
  <script src="load_app.js"></script>     
</body>
</html>
Run Code Online (Sandbox Code Playgroud)

get_consent.js是第一个脚本,从 localStorage 检索用户的设置并定义 Yett 黑名单:

const essentialCookies = ["js.stripe.com", "www.gstatic.com"];
const analyticsCookies = ["www.google-analytics.com"];
const allCookies = [...essentialCookies, ...analyticsCookies];
const consentPropertyName = "cookie_consent";

const retrieveConsentSettings = () => {
  const consentJsonString = localStorage.getItem(consentPropertyName);
  return JSON.parse(consentJsonString);
};

const checkConsentIsMissing = () => {
  const consentObj = retrieveConsentSettings();
  if (!consentObj || consentObj.length == 0) {
    return true;
  }
  return false;
};

const consentIsMissing = checkConsentIsMissing();

var blacklist;
if (consentIsMissing) {
  blacklist = allCookies;
} else {
  const acceptedCookies = retrieveConsentSettings();
  // Remove all script urls from blacklist that the user accepts (if all are accepted the blacklist will be empty)
  blacklist = allCookies.filter( ( el ) => !acceptedCookies.includes( el ) );
}

// Yett blacklist expects list of RegExp objects
var blacklistRegEx = [];
for (let index = 0; index < blacklist.length; index++) {
  const regExp = new RegExp(blacklist[index]);
  blacklistRegEx.push(regExp);
}

YETT_BLACKLIST = blacklistRegEx;
Run Code Online (Sandbox Code Playgroud)

set_consent.js负责使用用户的设置更新 localStorage 并隐藏/显示相应的 div 以获取 cookie 同意。通常,人们可以简单地调用window.yett.unblock()来解锁脚本,但由于它们的顺序很重要,我决定在 localStorage 更新后简单地重新加载窗口:

const saveToStorage = (acceptedCookies) => {
  const jsonString = JSON.stringify(acceptedCookies);
  localStorage.setItem(consentPropertyName, jsonString);
};

window.onload = () => {
  const consentPopup = document.getElementById("consent-popup");
  const consentPopupDetails = document.getElementById("consent-popup-details");
  const acceptBtn = document.getElementById("accept-btn");
  const moreInfoBtn = document.getElementById("info-btn");
  const saveBtn = document.getElementById("save-btn");
  const cancelBtn = document.getElementById("cancel-btn");
  const rejectBtn = document.getElementById("reject-btn");

  const acceptFn = (event) => {
    const cookiesTmp = [...essentialCookies, ...analyticsCookies];
    saveToStorage(cookiesTmp);
    // Reload window after localStorage was updated.
    // The blacklist will then only contain items the user has not yet consented to.
    window.location.reload();
  };

  const cancelFn = (event) => {
    consentPopup.classList.remove("hidden");
    consentPopupDetails.classList.add("hidden");
  };

  const rejectFn = (event) => {
    console.log("Rejected!");
    // Possible To-Do: Show placeholder content if even essential scripts are rejected.
  };

  const showDetailsFn = () => {
    consentPopup.classList.add("hidden");
    consentPopupDetails.classList.remove("hidden");
  };

  const saveFn = (event) => {
    const analyticsChecked = document.getElementById("analytics-cb").checked;
    var cookiesTmp = [...essentialCookies];
    if (analyticsChecked) {
      cookiesTmp.push(...analyticsCookies);
    }
    saveToStorage(cookiesTmp);
    // Reload window after localStorage was updated.
    // The blacklist will then only contain items the user has not yet consented to.
    window.location.reload();
  };

  acceptBtn.addEventListener("click", acceptFn);
  moreInfoBtn.addEventListener("click", showDetailsFn);
  saveBtn.addEventListener("click", saveFn);
  cancelBtn.addEventListener("click", cancelFn);
  rejectBtn.addEventListener("click", rejectFn);

  if (consentIsMissing) {
    consentPopup.classList.remove("hidden");
  }
};

Run Code Online (Sandbox Code Playgroud)

init_firebase.js是用于初始化服务的常用脚本,但我仅在获得同意的情况下进行初始化:

var firebaseConfig = {
  // your standard config
};

// Initialize Firebase only if user consented
if (!consentIsMissing) {
  firebase.initializeApp(firebaseConfig);
}
Run Code Online (Sandbox Code Playgroud)

同样的逻辑也适用于脚本load_app.js。仅当用户同意时才会加载 Flutter 应用程序。

因此,人们可能会添加一些后备内容,index.html如果用户拒绝必要的脚本,就会显示这些内容。根据您的使用情况,也可能是无论如何加载应用程序的选项,然后通过从 localStorage 访问用户的设置来在应用程序内进行区分。

var serviceWorkerVersion = null;
var scriptLoaded = false;
function loadMainDartJs() {
  if (scriptLoaded) {
    return;
  }
  scriptLoaded = true;
  var scriptTag = document.createElement("script");
  scriptTag.src = "main.dart.js";
  scriptTag.type = "application/javascript";
  document.body.append(scriptTag);
}

// Load app only if user consented
if (!consentIsMissing) {
  if ("serviceWorker" in navigator) {
    // Service workers are supported. Use them.
    window.addEventListener("load", function () {
      // Wait for registration to finish before dropping the <script> tag.
      // Otherwise, the browser will load the script multiple times,
      // potentially different versions.
      var serviceWorkerUrl =
        "flutter_service_worker.js?v=" + serviceWorkerVersion;
      navigator.serviceWorker.register(serviceWorkerUrl).then((reg) => {
        function waitForActivation(serviceWorker) {
          serviceWorker.addEventListener("statechange", () => {
            if (serviceWorker.state == "activated") {
              console.log("Installed new service worker.");
              loadMainDartJs();
            }
          });
        }
        if (!reg.active && (reg.installing || reg.waiting)) {
          // No active web worker and we have installed or are installing
          // one for the first time. Simply wait for it to activate.
          waitForActivation(reg.installing ?? reg.waiting);
        } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
          // When the app updates the serviceWorkerVersion changes, so we
          // need to ask the service worker to update.
          console.log("New service worker available.");
          reg.update();
          waitForActivation(reg.installing);
        } else {
          // Existing service worker is still good.
          console.log("Loading app from service worker.");
          loadMainDartJs();
        }
      });

      // If service worker doesn't succeed in a reasonable amount of time,
      // fallback to plaint <script> tag.
      setTimeout(() => {
        if (!scriptLoaded) {
          console.warn(
            "Failed to load app from service worker. Falling back to plain <script> tag."
          );
          loadMainDartJs();
        }
      }, 4000);
    });
  } else {
    // Service workers not supported. Just drop the <script> tag.
    loadMainDartJs();
  }
}
Run Code Online (Sandbox Code Playgroud)

这是我的style.css

html,
body {
  height: 100%;
  width: 100%;
  background-color: #2d2d2d;
  font-family: Arial, Helvetica, sans-serif;
}

.hidden {
  display: none;
  visibility: hidden;
}

.consent-div {
  position: fixed;
  bottom: 40px;
  left: 10%;
  right: 10%;
  width: 80%;
  padding: 14px 14px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  background-color: #eee;
  border-radius: 5px;
  box-shadow: 0 0 5px 5px rgba(0, 0, 0, 0.2);
}

.row-div {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

#accept-btn,
#save-btn {
  background-color: #103900;
}

#reject-btn,
#cancel-btn {
  background-color: #ff0000;
}

.btn {
  height: 25px;
  width: 140px;
  background-color: #777;
  border: none;
  color: white;
  border-radius: 5px;
  cursor: pointer;
}

.inline {
  display: inline-block;
  margin-right: 5px;
}

h2 {
  margin-block-start: 0.5em;
  margin-block-end: 0em;
}

h3 {
  margin-block-start: 0.5em;
  margin-block-end: 0em;
}

/* The switch - the box around the slider */
.switch {
  position: relative;
  display: inline-block;
  width: 50px;
  height: 25px;
}

/* Hide default HTML checkbox */
.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

/* The slider */
.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #ccc;
}

.slider:before {
  position: absolute;
  content: "";
  height: 18px;
  width: 18px;
  left: 4px;
  bottom: 4px;
  background-color: white;
}

input:checked + .slider {
  background-color: #2196f3;
}

input:focus + .slider {
  box-shadow: 0 0 1px #2196f3;
}

input:checked + .slider:before {
  -webkit-transform: translateX(24px);
  -ms-transform: translateX(24px);
  transform: translateX(24px);
}

/* Rounded sliders */
.slider.round {
  border-radius: 34px;
}

.slider.round:before {
  border-radius: 50%;
}
Run Code Online (Sandbox Code Playgroud)