mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-26 05:06:42 +08:00
357 lines
24 KiB
HTML
357 lines
24 KiB
HTML
<!DOCTYPE html>
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
<head>
|
||
<head>
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
<link rel="icon" href="/static/favicon.png">
|
||
<title>14 资源保护:如何基于 OAuth2 协议配置授权过程?.md.html</title>
|
||
<!-- Spectre.css framework -->
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
<!-- theme css & js -->
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
</head>
|
||
<body>
|
||
<div class="book-container">
|
||
<div class="book-sidebar">
|
||
<div class="book-brand">
|
||
<a href="/">
|
||
<img src="/static/favicon.png">
|
||
<span>技术文章摘抄</span>
|
||
</a>
|
||
</div>
|
||
<div class="book-menu uncollapsible">
|
||
<ul class="uncollapsible">
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li><a href="../">上一级</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/00 开篇词 Spring Security,为你的应用安全与职业之路保驾护航.md.html">00 开篇词 Spring Security,为你的应用安全与职业之路保驾护航</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/01 顶级框架:Spring Security 是一款什么样的安全性框架?.md.html">01 顶级框架:Spring Security 是一款什么样的安全性框架?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/02 用户认证:如何使用 Spring Security 构建用户认证体系?.md.html">02 用户认证:如何使用 Spring Security 构建用户认证体系?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/03 认证体系:如何深入理解 Spring Security 用户认证机制?.md.html">03 认证体系:如何深入理解 Spring Security 用户认证机制?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/04 密码安全:Spring Security 中包含哪些加解密技术?.md.html">04 密码安全:Spring Security 中包含哪些加解密技术?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/05 访问授权:如何对请求的安全访问过程进行有效配置?.md.html">05 访问授权:如何对请求的安全访问过程进行有效配置?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/06 权限管理:如何剖析 Spring Security 的授权原理?.md.html">06 权限管理:如何剖析 Spring Security 的授权原理?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/07 案例实战:使用 Spring Security 基础功能保护 Web 应用.md.html">07 案例实战:使用 Spring Security 基础功能保护 Web 应用</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/08 管道过滤:如何基于 Spring Security 过滤器扩展安全性?.md.html">08 管道过滤:如何基于 Spring Security 过滤器扩展安全性?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/09 攻击应对:如何实现 CSRF 保护和跨域 CORS?.md.html">09 攻击应对:如何实现 CSRF 保护和跨域 CORS?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/10 全局方法:如何确保方法级别的安全访问?.md.html">10 全局方法:如何确保方法级别的安全访问?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/11 案例实战:使用 Spring Security 高级主题保护 Web 应用.md.html">11 案例实战:使用 Spring Security 高级主题保护 Web 应用</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/12 开放协议:OAuth2 协议解决的是什么问题?.md.html">12 开放协议:OAuth2 协议解决的是什么问题?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/13 授权体系:如何构建 OAuth2 授权服务器?.md.html">13 授权体系:如何构建 OAuth2 授权服务器?</a>
|
||
</li>
|
||
<li>
|
||
<a class="current-tab" href="/专栏/Spring Security 详解与实操/14 资源保护:如何基于 OAuth2 协议配置授权过程?.md.html">14 资源保护:如何基于 OAuth2 协议配置授权过程?</a>
|
||
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/15 令牌扩展:如何使用 JWT 实现定制化 Token?.md.html">15 令牌扩展:如何使用 JWT 实现定制化 Token?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/16 案例实战:基于 Spring Security 和 Spring Cloud 构建微服务安全架构.md.html">16 案例实战:基于 Spring Security 和 Spring Cloud 构建微服务安全架构</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/17 案例实战:基于 Spring Security 和 OAuth2 实现单点登录.md.html">17 案例实战:基于 Spring Security 和 OAuth2 实现单点登录</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/18 技术趋势:如何为 Spring Security 添加响应式编程特性?.md.html">18 技术趋势:如何为 Spring Security 添加响应式编程特性?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/19 测试驱动:如何基于 Spring Security 测试系统安全性?.md.html">19 测试驱动:如何基于 Spring Security 测试系统安全性?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Spring Security 详解与实操/20 结束语 以终为始,Spring Security 的学习总结.md.html">20 结束语 以终为始,Spring Security 的学习总结</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
<div class="sidebar-toggle-inner"></div>
|
||
</div>
|
||
<script>
|
||
function add_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.add('show')
|
||
}
|
||
function remove_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.remove('show')
|
||
}
|
||
function sidebar_toggle() {
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let content = document.querySelector('.off-canvas-content')
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
sidebar_toggle.classList.remove('extend')
|
||
sidebar.classList.remove('hide')
|
||
content.classList.remove('extend')
|
||
} else { // hide
|
||
sidebar_toggle.classList.add('extend')
|
||
sidebar.classList.add('hide')
|
||
content.classList.add('extend')
|
||
}
|
||
}
|
||
function open_sidebar() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.add('show')
|
||
overlay.classList.add('show')
|
||
}
|
||
function hide_canvas() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.remove('show')
|
||
overlay.classList.remove('show')
|
||
}
|
||
</script>
|
||
<div class="off-canvas-content">
|
||
<div class="columns">
|
||
<div class="column col-12 col-lg-12">
|
||
<div class="book-navbar">
|
||
<!-- For Responsive Layout -->
|
||
<header class="navbar">
|
||
<section class="navbar-section">
|
||
<a onclick="open_sidebar()">
|
||
<i class="icon icon-menu"></i>
|
||
</a>
|
||
</section>
|
||
</header>
|
||
</div>
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;">
|
||
<div class="book-post">
|
||
<p id="tip" align="center"></p>
|
||
<div><h1>14 资源保护:如何基于 OAuth2 协议配置授权过程?</h1>
|
||
<p>上一讲我们学习了如何构建 OAuth2 授权服务器,并掌握了生成 Token 的系统方法。今天我们关注的重点是如何使用 Token 实现对服务访问的具体授权。在日常开发过程中,我们需要对每个服务的不同功能进行不同粒度的权限控制,并且希望这种控制方法足够灵活,能够确保不同服务根据业务场景动态调整权限控制体系。同时,在微服务架构中,我们还需要考虑如何在多个服务中对 Token 进行有效的传播,确保整个服务访问的链路都得到授权管理。借助 Spring Security 框架,实现这些需求都很简单,下面我们就来展开学习。</p>
|
||
<h3>在微服务中集成 OAuth2 授权机制</h3>
|
||
<p>我们知道在 OAuth2 协议中,单个微服务的定位就是资源服务器。Spring Security 框架为此提供了专门的 @EnableResourceServer 注解。通过<strong>在 Bootstrap 类中添加 @EnableResourceServer 注解</strong>,相当于声明该服务中的所有内容都是受保护的资源,示例代码如下所示:</p>
|
||
<pre><code>@SpringBootApplication
|
||
@EnableResourceServer
|
||
public class UserApplication {
|
||
|
||
public static void main(String[] args) {
|
||
SpringApplication.run(UserApplication.class, args);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>一旦我们在微服务中添加了 @EnableResourceServer 注解,该服务就会对所有的 HTTP 请求进行验证以确定 Header 部分中是否包含 Token 信息。如果没有 Token 信息,就会直接限制访问;如果有 Token 信息,则通过访问 OAuth2 服务器进行 Token 的验证。那么问题来了,每个微服务是如何与 OAuth2 服务器进行通信并获取传入 Token 的验证结果的呢?</p>
|
||
<p>要想回答这个问题,我们需要明确将 Token 传递给 OAuth2 授权服务器的目的是<strong>获取该 Token 中包含的用户和授权信息</strong>。这样,我们势必需要在各个微服务和 OAuth2 授权服务器之间建立起一种交互关系。我们可以在配置文件中添加如下所示的 security.oauth2.resource.userInfoUri 配置项来实现这一目标:</p>
|
||
<pre><code>security:
|
||
oauth2:
|
||
resource:
|
||
userInfoUri: http://localhost:8080/userinfo
|
||
</code></pre>
|
||
<p>这里的 <a href="http://localhost:8080/userinfo">http://localhost:8080/userinfo</a>指向 OAuth2 授权服务器中的一个自定义端点,实现方式如下所示:</p>
|
||
<pre><code>@RequestMapping(value = "/userinfo", produces = "application/json")
|
||
public Map<String, Object> user(OAuth2Authentication user) {
|
||
Map<String, Object> userInfo = new HashMap<>();
|
||
userInfo.put("user", user.getUserAuthentication().getPrincipal());
|
||
userInfo.put("authorities", AuthorityUtils.authorityListToSet(
|
||
user.getUserAuthentication().getAuthorities()));
|
||
return userInfo;
|
||
}
|
||
</code></pre>
|
||
<p>这个端点的作用就是获取可访问的那些受保护服务的用户信息。这里我们用到了 OAuth2Authentication 类,该类保存着用户的身份(Principal)和权限(Authority)信息。</p>
|
||
<p>当使用 Postman 访问 http://localhost:8080/userinfo 端点时,我们就需要传入一个有效的 Token。这里我们以上一讲中生成的 Token“0efa61be-32ab-4351-9dga-8ab668ababae”为例,在 HTTP 请求中添加一个“Authorization”请求头。请注意,因为我们使用的是 bearer 类型的 Token,所以需要<strong>在 access_token 的具体值之前加上“bearer”前缀</strong>。当然,我们也可以直接在“Authorization”页中选择协议类型为 OAuth 2.0,然后输入 Access Token,这样就相当于添加了请求头信息,如下图所示:</p>
|
||
<p><img src="assets/CioPOWDwBsmAdTfxAAF0kH4jkg4969.jpg" alt="图片.png" /></p>
|
||
<p>通过 Token 发起 HTTP 请求示意图</p>
|
||
<p>在后续的 HTTP 请求中,我们都将以这种方式发起对微服务的调用。该请求的结果如下所示:</p>
|
||
<pre><code>{
|
||
"user":{
|
||
"password":null,
|
||
"username":"spring_user",
|
||
"authorities":[
|
||
{
|
||
"autority":"ROLE_USER"
|
||
}
|
||
],
|
||
"accountNonExpired":true,
|
||
"accountNonLocker":true,
|
||
"credentialsNonExpired":true,
|
||
"enabled":true
|
||
},
|
||
"authorities":[
|
||
"ROLE_USER"
|
||
]
|
||
}
|
||
</code></pre>
|
||
<p>我们知道“0efa61be-32ab-4351-9dga-8ab668ababae”这个 Token 是由“spring_user”这个用户生成的,可以看到该结果中包含了用户的用户名、密码以及该用户名所拥有的角色,这些信息与我们在上一讲中初始化的“spring_user”用户信息保持一致。我们也可以尝试使用“spring_admin”这个用户来重复上述过程。</p>
|
||
<h3>在微服务中嵌入访问授权控制</h3>
|
||
<p>在一个微服务系统中,每个微服务作为独立的资源服务器,对自身资源的保护粒度并不是固定的,可以根据需求对访问权限进行精细化控制。在 Spring Security 中,对访问的不同控制层级进行了抽象,形成了<strong>用户、角色和请求方法</strong>这三种粒度,如下图所示:</p>
|
||
<p><img src="assets/CioPOWDwBtmAMiWZAAFQfgXxt_0021.jpg" alt="image-2.png" /></p>
|
||
<p>用户、角色和请求方法三种控制粒度示意图</p>
|
||
<p>基于上图,我们可以对这三种粒度进行排列组合,形成用户、用户+角色以及用户+角色+请求方法这三种层级,这三种层级能够访问的资源范围逐一递减。用户层级是指只要是认证用户就能访问服务内的各种资源;而用户+角色层级在用户层级的基础上,还要求用户属于某一个或多个特定角色;最后的用户+角色+请求方法层级要求最高,能够对某些 HTTP 操作进行访问限制。接下来我们针对这三个层级展开讨论。</p>
|
||
<h4>用户层级的权限访问控制</h4>
|
||
<p>通过上一讲的学习,我们已经熟悉了通过扩展各种 ConfigurerAdapter 配置适配器类来实现自定义配置信息的方法。对于资源服务器而言,也存在一个 ResourceServerConfigurerAdapter 类,而我们的做法同样是<strong>继承该类并覆写它的 configure 方法</strong>,如下所示:</p>
|
||
<pre><code>@Configuration
|
||
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
|
||
|
||
@Override
|
||
public void configure(HttpSecurity httpSecurity) throws Exception{
|
||
httpSecurity.authorizeRequests()
|
||
.anyRequest()
|
||
.authenticated();
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>我们注意到,这个方法的入参是一个 HttpSecurity 对象,而上述配置中的 anyRequest().authenticated() 方法指定了访问该服务的任何请求都需要进行验证。因此,当我们使用普通的 HTTP 请求来访问 user-service 中的任何 URL 时,将会得到一个“unauthorized”的 401 错误信息。解决办法就是<strong>在 HTTP 请求中设置“Authorization”请求头并传入一个有效的 Token 信息</strong>,你可以模仿前面的示例做一些练习。</p>
|
||
<h4>用户+角色层级的权限访问控制</h4>
|
||
<p>对于某些安全性要求比较高的资源,我们不应该开放资源访问入口给所有的认证用户,而是需要<strong>限定访问资源的角色</strong>。针对不同的业务场景,我们可以判断哪些服务涉及核心业务流程,这些服务的 HTTP 端口不应该开放给普通用户,而是限定只有角色为“ADMIN”的管理员才能访问该服务。要想达到这种效果,实现方式也比较简单,就是在HttpSecurity 中通过 antMatchers() 和 hasRole() 方法指定想要限制的资源和角色。我们可以创建一个新 ResourceServerConfiguration 类实例并覆写它的 configure 方法,如下所示:</p>
|
||
<pre><code>@Configuration
|
||
public class ResourceServerConfiguration extends
|
||
ResourceServerConfigurerAdapter{
|
||
|
||
@Override
|
||
public void configure(HttpSecurity httpSecurity) throws Exception {
|
||
|
||
httpSecurity.authorizeRequests()
|
||
.antMatchers("/order/**")
|
||
.hasRole("ADMIN")
|
||
.anyRequest()
|
||
.authenticated();
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>可以看到,这里使用了 05 讲[“访问授权:如何对请求的安全访问过程进行有效配置?”]中介绍的 Ant 匹配器实现了授权管理。现在,如果我们使用角色为“User”的 Token 访问这个服务,就会得到一个“access_denied”的错误信息。然后,我们使用上一讲中初始化的一个具有“ADMIN”角色的用户“spring_admin”来创建新的 Token,并再次访问该服务,就能得到正常的返回结果。</p>
|
||
<h4>用户+角色+操作层级的权限访问控制</h4>
|
||
<p>更进一步,我们还可以针对某个端点的某个具体 HTTP 方法进行控制。例如,如果我们认为对某个微服务中的“user”端点下的资源进行更新的风险很高,那么就可以在 HttpSecurity 的 antMatchers() 中添加 HttpMethod.PUT 限定。</p>
|
||
<pre><code>@Configuration
|
||
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
|
||
|
||
@Override
|
||
public void configure(HttpSecurity httpSecurity) throws Exception{
|
||
httpSecurity.authorizeRequests()
|
||
.antMatchers(HttpMethod.PUT, "/user/**")
|
||
.hasRole("ADMIN")
|
||
.anyRequest()
|
||
.authenticated();
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>现在,我们使用普通“USER”角色生成的 Token,并调用"/order/"端点中的 Update 操作,同样会得到“access_denied”的错误信息。而尝试使用“ADMIN”角色生成的 Token 进行访问,就可以得到正常响应。</p>
|
||
<h3>在微服务中传播 Token</h3>
|
||
<p>我们知道一个微服务系统势必涉及多个服务之间的调用,并形成一个链路。因为访问所有服务的过程都需要进行访问权限的控制,所以我们需要确保生成的 Token 能够在服务调用链路中进行传播,如下图所示:</p>
|
||
<p><img src="assets/Cgp9HWDwBumAe_JvAAEWjcq9xMI017.jpg" alt="image-3.png" /></p>
|
||
<p>微服务中 Token 传播示意图</p>
|
||
<p>那么,如何实现上图中的 Token 传播效果呢?Spring Security 基于 RestTemplate 进行了封装,专门提供了一个用在 HTTP 请求中传播 Token 的 OAuth2RestTemplate 工具类。想要在业务代码中构建一个 OAuth2RestTemplate 对象,可以使用如下所示的示例代码:</p>
|
||
<pre><code>@Bean
|
||
public OAuth2RestTemplate oauth2RestTemplate(
|
||
OAuth2ClientContext oauth2ClientContext,
|
||
OAuth2ProtectedResourceDetails details) {
|
||
|
||
return new OAuth2RestTemplate(details, oauth2ClientContext);
|
||
}
|
||
</code></pre>
|
||
<p>可以看到,通过传入 OAuth2ClientContext 和 OAuth2ProtectedResourceDetails,我们就可以创建一个 OAuth2RestTemplate 类。OAuth2RestTemplate 会把从 HTTP 请求头中获取的 Token 保存到一个 OAuth2ClientContext 上下文对象中,而OAuth2ClientContext 会把每个用户的请求信息控制在会话范围内,以确保不同用户的状态分离。另一方面,OAuth2RestTemplate 还依赖于 OAuth2ProtectedResourceDetails 类,该类封装了我们在上一讲中介绍过的clientId、客户端安全码 clientSecret、访问范围 scope 等属性。</p>
|
||
<p>一旦 OAuth2RestTemplate 创建成功,我们就可以使用它对某一个远程服务进行访问,实现代码如下所示:</p>
|
||
<pre><code>@Component
|
||
public class OrderServiceClient {
|
||
|
||
@Autowired
|
||
OAuth2RestTemplate restTemplate;
|
||
|
||
public Order getOrderById(String orderId){
|
||
ResponseEntity<Order> result =
|
||
restTemplate.exchange(
|
||
"http://orderservice/order/{orderId}",
|
||
HttpMethod.GET,
|
||
null, Order.class, orderId);
|
||
Order order = result.getBody();
|
||
return order;
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>显然,基于这种远程调用方式,我们唯一要做的就是使用 OAuth2RestTemplate 替换原有的 RestTemplate,所有关于 Token 传播的细节已经被完整地封装在每次请求中。</p>
|
||
<h3>小结与预告</h3>
|
||
<p>这一讲我们的关注点在于对服务访问进行授权。通过今天的学习,我们明确了在微服务中嵌入访问授权控制的三种粒度。同时,在微服务系统中,因为涉及多个服务之间的交互,所以需要实现 Token 在这些服务之间的有效传播,我们可以借助 Spring Security 提供的工具类轻松实现这些需求。</p>
|
||
<p>本讲内容总结如下:
|
||
<img src="assets/CioPOWDwBviAFEsOAAE_BMghR5U884.jpg" alt="资源保护:如何基于 OAuth2 协议配置授权过程?.png" /></p>
|
||
<p>最后给你留一道思考题:你能描述对服务访问进行授权的三种层级,以及每个层级对应的实现方法吗?欢迎在留言区分享你的学习收获。</p>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="float: left">
|
||
<a href="/专栏/Spring Security 详解与实操/13 授权体系:如何构建 OAuth2 授权服务器?.md.html">上一页</a>
|
||
</div>
|
||
<div style="float: right">
|
||
<a href="/专栏/Spring Security 详解与实操/15 令牌扩展:如何使用 JWT 实现定制化 Token?.md.html">下一页</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
</div>
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099757369e03d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
</body>
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
<script>
|
||
window.dataLayer = window.dataLayer || [];
|
||
function gtag() {
|
||
dataLayer.push(arguments);
|
||
}
|
||
gtag('js', new Date());
|
||
gtag('config', 'G-NPSEEVD756');
|
||
var path = window.location.pathname
|
||
var cookie = getCookie("lastPath");
|
||
console.log(path)
|
||
if (path.replace("/", "") === "") {
|
||
if (cookie.replace("/", "") !== "") {
|
||
console.log(cookie)
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
}
|
||
} else {
|
||
setCookie("lastPath", path)
|
||
}
|
||
function setCookie(cname, cvalue) {
|
||
var d = new Date();
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
var expires = "expires=" + d.toGMTString();
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
}
|
||
function getCookie(cname) {
|
||
var name = cname + "=";
|
||
var ca = document.cookie.split(';');
|
||
for (var i = 0; i < ca.length; i++) {
|
||
var c = ca[i].trim();
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
}
|
||
return "";
|
||
}
|
||
</script>
|
||
</html>
|