第22天-单点登录SSO,JWT实现无状态登录,自定义网关过滤器处理登录验证

1.单点登录(SSO)

SSO英文全称Single Sign On,单点登录。

SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

第22天-单点登录SSO,JWT实现无状态登录,自定义网关过滤器处理登录验证_第1张图片



1.1. Cookie问题

电商平台通常由多个微服务组成,每个微服务都有独立的域名,而cookie是有作用域的。

查看浏览器控制台:

第22天-单点登录SSO,JWT实现无状态登录,自定义网关过滤器处理登录验证_第2张图片

domain:作用域名

domain参 gmall.com search.gmall.com item.gmall.com
gmall.com
search.gmall.com × ×
item.gmall.com × ×

domain有两点要注意:

1. domain参数可以设置父域名以及自身,但不能设置其它域名,包括子域名,否则cookie不起作用。

2. cookie的作用域是domain本身以及domain下的所有子域名。

Cookie的路径(Path):

  • response.addCookie默认放在当前路径下,访问当前路径下的所有请求都会带
  • 设置/标识项目根路径,访问项目任何位置都会携带


1.2. 有状态登录

为了保证客户端cookie的安全性,服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。

例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。

缺点是什么?

  • 服务端保存大量数据,增加服务端压力
  • 服务端保存用户状态,无法进行水平扩展
  • 客户端请求依赖服务端,多次请求必须访问同一台服务器

即使使用redis保存用户的信息,也会损耗服务器资源。



1.3. 无状态登录

微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服务的无状态性,即:

  • 服务端不保存任何客户端请求者信息
  • 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份

带来的好处是什么呢?

  • 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
  • 服务端的集群和状态对客户端透明
  • 服务端可以任意的迁移和伸缩
  • 减小服务端存储压力


1.4. 无状态登录流程

无状态登录的流程:

  • 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
  • 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
  • 以后每次请求,客户端都携带认证的token
  • 服务端对token进行解密,判断是否有效。

流程图:

第22天-单点登录SSO,JWT实现无状态登录,自定义网关过滤器处理登录验证_第3张图片
整个登录过程中,最关键的点是什么?

token的安全性

token是识别客户端身份的唯一标示,如果加密不够严密,被人伪造那就完蛋了。

采用何种方式加密才是安全可靠的呢?

我们将采用 JWT + RSA非对称加密



2. JWT实现无状态登录

JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;官网:https://jwt.io

GitHub上jwt的java客户端:https://github.com/jwtk/jjwt



2.1. 数据格式

JWT包含三部分数据:

  • Header:头部,通常头部有两部分信息:

    • token类型:JWT
    • 加密方式:base64(HS256)
  • Payload:载荷,就是有效数据,一般包含下面信息:

    • 用户身份信息(注意,这里因为采用base64编码,可解码,因此不要存放敏感信息)
    • 注册声明:如token的签发时间,过期时间,签发人等

    这部分也会采用base64编码,得到第二部分数据
  • Signature:签名,是整个数据的认证信息。根据前两步的数据,再加上指定的密钥(secret)(不要泄漏,最好周期性更换),通过base64编码生成。用于验证整个数据完整和可靠性

第22天-单点登录SSO,JWT实现无状态登录,自定义网关过滤器处理登录验证_第4张图片



2.2. JWT交互流程

流程图:

第22天-单点登录SSO,JWT实现无状态登录,自定义网关过滤器处理登录验证_第5张图片

步骤翻译:

  1. 用户登录
  2. 服务的认证,通过后根据secret生成token
  3. 将生成的token返回给浏览器
  4. 用户每次请求携带token
  5. 服务端利用公钥解读jwt签名,判断签名有效后,从Payload中获取用户信息
  6. 处理请求,返回响应结果

因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,完全符合了Rest的无状态规范。



2.3. 非对称加密

加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:

  • 对称加密,如AES

    • 基本原理:将明文分成N个组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所有的分组密文进行合并,形成最终的密文。
    • 优势:算法公开、计算量小、加密速度快、加密效率高
    • 缺陷:双方都使用同样密钥,安全性得不到保证
  • 非对称加密,如RSA

    • 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
      • 私钥加密,持有公钥才可以解密
      • 公钥加密,持有私钥才可解密
    • 优点:安全,难以破解
    • 缺点:算法比较耗时
  • 不可逆加密,如MD5,SHA

    • 基本原理:加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,无法根据密文推算出明文。

RSA算法历史:

1977年,三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA



3.搭建授权中心


第22天-单点登录SSO,JWT实现无状态登录,自定义网关过滤器处理登录验证_第6张图片

用户鉴权:

  • 接收用户的登录请求,通过用户中心的接口进行校验,通过后生成JWT
  • 使用私钥生成JWT并返回


3.1.JWT工具类

gmall-common工程中已经封装了jwt相关的工具类:

  • JwtUtils
  • RsaUtils
  • CookUtils

并在gmall-common中的pom.xml中引入了jwt相关的依赖:

		<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.9</version>
        </dependency>

3.2. 配置公钥和私钥

我们需要在授权中心生成真正的公钥和私钥。可以把相关配置内容配置到gmall-auth工程的
application.yml 中或者配置中心:

auth:
  jwt:
	pubKeyPath: D:\\gmall\\rsa\\rsa.pub
	priKeyPath: D:\\gmall\\rsa\\rsa.pri
	secret: 30489ouerweljrLROE@#)(@$*343jlsdf
	cookieName: GMALL-TOKEN
	expire: 180
	unick: unick

然后编写属性类读取jwt配置,并从秘钥配置文件中读取出响应的公钥及私钥,

JwtProperties

package com.atguigu.gmall.auth.config;

import com.atguigu.common.utils.RsaUtils;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;

import javax.annotation.PostConstruct;
import java.io.File;
import java.security.PrivateKey;
import java.security.PublicKey;

/**
 * JWT配置 {@link JwtProperties}
 *
 * @author zhangwen
 * @email: [email protected]
 */
@ConfigurationProperties(prefix = "auth.jwt")
@Slf4j
@Data
public class JwtProperties {

    private String pubKeyPath;
    private String priKeyPath;
    private String secret;
    private String cookieName;
    private Integer expire;
    private String unick;

    private PublicKey publicKey;
    private PrivateKey privateKey;

    /**
     * 该方法在构造方法执行之后执行
     */
    @PostConstruct
    public void init(){
        try {
            File pubFile = new File(pubKeyPath);
            File priFile = new File(priKeyPath);
            // 如果公钥或者私钥不存在,重新生成公钥和私钥
            if (!pubFile.exists() || !priFile.exists()) {
                RsaUtils.generateKey(pubKeyPath, priKeyPath, secret);
            }
            this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
            this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
        } catch (Exception e) {
            log.error("生成公钥和私钥出错");
            e.printStackTrace();
        }
    }
}


4.完成登录功能



4.1.跳转登录页

参照京东,当点击登录跳转到登录页面时,如下:

第22天-单点登录SSO,JWT实现无状态登录,自定义网关过滤器处理登录验证_第7张图片

会记录跳转到登录页面前的页面地址,登录成功后要回到原来的页面。

AuthController

/**
* 登录页面-单点登录
* @param returnUrl
* @return
*/
@GetMapping("/login2.html")
public String login2Page(@RequestParam("returnUrl") String returnUrl, Model model) {
	//把登录前的页面地址,记录到登录页面,以备将来登录成功,回到登录前的页面
	model.addAttribute("returnUrl", returnUrl);
	return "login2";
}

在login.html页面会记录returnUrl地址,将来登录成功后重定向到该地址:

在这里插入图片描述

在浏览器输入:http://auth.gmall.com/login2.html?returnUrl=http://gmall.com

效果如下:



4.2. 完成登录功能

接下来,我们需要在 gmall-auth 编写登录代码。基本流程如下:

  • 客户端携带用户名和密码请求登录 ,并携带登录前页面的路径
  • 授权中心调用用户中心接口,根据用户名和密码查询用户信息
  • 用户名密码不正确,不能获取用户,登录失败
  • 如果校验成功,则生成JWT签名,jwt要防止别人盗取(安全)
  • 把jwt放入cookie
  • 为了方便页面展示登录用户昵称,向cookie中单独写入昵称(例如京东cookie中的unick)
  • 重定向回到登录前的页面

4.2.1. AuthController

编写授权接口,我们接收登录名和密码及登陆前的页面地址,登录成功后重定向到登陆前页面。

  • 请求方式:post

  • 请求路径:/ssoLogin

  • 请求参数:account和password

  • 返回结果:无

代码:

 /**
  * 单点登录
  * @param account
  * @param password
  * @param returnUrl
  * @param request
  * @param response
  * @param redirectAttributes
  * @return
  */
 @PostMapping("/ssoLogin")
 public String ssoLogin(@RequestParam("account") String account,
                        @RequestParam("password") String password,
                        @RequestParam("returnUrl") String returnUrl,
                        HttpServletRequest request,
                        HttpServletResponse response,
                        RedirectAttributes redirectAttributes) {
     UserLoginVO vo = new UserLoginVO();
     vo.setAccount(account);
     vo.setPassword(password);
     // 调用远程接口
     R r = memberFeignService.login(vo);
     if (r.getCode() != 0) {
         Map<String, String> errors = new HashMap<>();
         String msg = r.getData("msg", new TypeReference<String>() {});
         errors.put("msg", msg);
         redirectAttributes.addFlashAttribute("errors", errors);
         // 登录失败,重定向到登录页面
         return "redirect:http://auth.gmall.com/login2.html";
     }

     // 3. 把用户id及用户名放入载荷
     MemberVO memberVO = r.getData("data", new TypeReference<MemberVO>() {});
     Map<String, Object> map = new HashMap<>();
     map.put("userId", memberVO.getId());
     map.put("username", memberVO.getNickname());

     // 4. 为了防止jwt被别人盗取,载荷中加入用户ip地址
     String ipAddress = IpUtils.getIpAddress(request);
     map.put("ip", ipAddress);

     // 5. 制作jwt类型的token信息
     try {
         String token = JwtUtils.generateToken(map, jwtProperties.getPrivateKey(), jwtProperties.getExpire());

         // 6. 把jwt放入cookie中
         CookieUtils.setCookie(request, response, jwtProperties.getCookieName(), token, jwtProperties.getExpire() * 60);
         // 7.用户昵称放入cookie中,方便页面展示昵称
         CookieUtils.setCookie(request, response, jwtProperties.getUnick(), memberVO.getNickname(), jwtProperties.getExpire() * 60);
     } catch (Exception e) {
         e.printStackTrace();
     }

     return "redirect:" + returnUrl;
 }


4.3. 解决cookie写入问题

解决cookie写入问题,要注意两点:

  1. cookie中的domain域必须和地址栏(或者是父域名)一致。
  2. cors跨域满足携带cookie的生效条件
    • 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。(网关中已设置)
    • 响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名。(网关中已设置具体域名)
    • 浏览器发起ajax需要指定withCredentials 为true。(前端工程:gmall-admin-vue\src\utils\httpRequest.js文件已经设置)

4.3.1. domain地址变化原因

那么问题来了:为什么我们这里的请求serverName变成了ip地址了呢?

这是因为在地址栏输入域名时,经过了两次转发:

  • 我们使用了nginx反向代理,当监听到auth.gmall.com的时候,会自动将请求转发至代理ip地址,即gateway服务器地址。
  • 而后请求到达我们的gateway网关,gateway网关就会根据路径匹配,我们的请求是/api/auth,根据规则被转发到了auth服务地址 ,即我们的授权中心。

每次转发都会丢失域名信息。


4.3.2. nginx转发时要携带域名

首先nginx转发请求给网关时,要携带域名信息。需要在nginx配置文件中配置代理头信息:

proxy_set_header Host $host;

修改完成之后,重启nginx容器 docker restart nginx

这样就解决了nginx转发时的域名问题。

4.3.3. 网关转发时要携带域名

在网关转发请求给服务时,要携带地址信息:

spring:
  cloud:
	gateway:
	  x-forwarded:
		host-enabled: true

第22天-单点登录SSO,JWT实现无状态登录,自定义网关过滤器处理登录验证_第8张图片

4.3.4.再次登录测试



4.4.页头显示用户名

第22天-单点登录SSO,JWT实现无状态登录,自定义网关过滤器处理登录验证_第9张图片

从 cookie 中获取用户昵称

第22天-单点登录SSO,JWT实现无状态登录,自定义网关过滤器处理登录验证_第10张图片



5.网关过滤器验证登录状态

gateway网关过滤器包含两种:

  • 全局过滤器
  • 局部过滤器


5.1.自定义全局过滤器

自定义全局过滤器非常简单:实现GlobalFilter接口即可,无差别拦截所有微服务的请求

@Component
public class TestGatewayFilter implements GlobalFilter, Ordered {
	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		System.out.println("无需配置,拦截所有经过网关的请求!!");	
		//放行
		return chain.filter(exchange);
	}
	/**
	* 通过实现Orderer接口的getOrder方法控制全局过滤器的执行顺序
	* @return
	*/
	@Override
	public int getOrder() {
		return 0;
	}
}


5.2. 自定义局部过滤器

自定义局部过滤器稍微麻烦一点:

  1. 需要编写过滤器工厂类继承AbstractGatewayFilterFactory抽象类
  2. 在需要过滤的微服务路由中配置该过滤器

可以做到定点拦截。

5.2.1. 过滤器工厂AuthGatewayFilterFactory

@Component
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
	@Override
	public GatewayFilter apply(Object config) {
		//实现GatewaFilter接口
		return new GatewayFilter() {
			@Override
			public Mono<Void> filter(ServerWebExchange exchange,GatewayFilterChain chain) {
				System.out.println("自定义过滤器!");
				//放行
				return chain.filter(exchange);
			}
		};
	}
}

5.2.2. 在配置文件中使用

现在拿gmall-auth工程尝试使用

- id: gmall_auth_route
  uri: lb://gmall-auth
  predicates:
  - Host=auth.gmall.com
  filters:
  - Auth

过滤器名称就是 Auth ,即自定义过滤器工厂 AuthGatewayFilterFactory 去掉
GatewayFilterFactory

5.2.3. 读取过滤器配置内容

此时,虽然可以使用这个拦截器了,但是我们的拦截器还是光秃秃的,不能指定内容。

如果像下面一样指定 拦截路径 ,并在过滤器中获取 拦截路径 ,再去判断当前路径是否需要拦截

假设如下:

- id: gmall_auth_route
  uri: lb://gmall-auth
  predicates:
  - Host=auth.gmall.com
  filters:
  - Auth=/login2.html,/login2

改造AuthGatewayFilterFactory过滤器工厂类如下:

@Component
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.PathConfig> {
	/**
	 * 一定要重写构造方法
	 * 告诉父类,这里使用PathConfĕ g对象接收配置内容
	 */
	public AuthGatewayFilterFactory() {
		super(PathConfig.class);
	}
	
	@Override
	public GatewayFilter apply(PathConfig config) {
		//实现GatewaFilter接口
		return new GatewayFilter() {
			@Override
			public Mono<Void> filter(ServerWebExchange exchange,GatewayFilterChain chain) {
				System.out.println("自定义过滤器!" + config);
				//放行
				return chain.filter(exchange);
			}
		};
	}
	
	/**
	 * 指定字段顺序
	 * 可以通过不同的字段分别读取:/login2.html,/ssoLogin
	 * 在这里希望通过一个集合字段读取所有的路径
	 * @return
	 */
	@Override
	public List<String> shortcutFieldOrder() {
		return Arrays.asList("authPaths");
	}
	
	/**
	 * 指定读取字段的结果集类型
	 * 默认通过map的方式,把配置读取到不同字段
	 * 例如:/login2.html,/ssoLogin
	 * 由于只指定了一个字段,只能接收/login2.html
	 * @return
	 */
	@Override
	public ShortcutType shortcutType() {
		return ShortcutType.GATHER_LIST;
	}
	
	/**
	 * 读取配置的内部类
	 */
	@Data
	public static class PathConfig{
		private List<String> authPaths;
	}
}

重启网关测试,已经可以拿到配置内容!!!



5.3. 通过自定义局部过滤器完成登录验证

接下来,我们在gmall-gateway编写过滤器,对用户的token进行校验,如果发现未登录,则进行拦截。

思路:

  1. 判断请求路径在不在拦截名单中,不在直接放行
  2. 获取请求中的token。同步请求从cookie中获取,异步请求从header中获取(走cookie太重,一个网站往往有很多cookie,如果通过携带cookie的方式传递token,网络传输压力太大)
  3. 判断token是否为空。为空直接拦截
  4. 如果不为空,解析jwt获取登录信息
  5. 判断是否被盗用。通过登录信息中的ip和当前请求的ip比较
  6. 传递登录信息给后续服务。后续各服务就不用再去解析了
  7. 放行

5.3.1. 引入jwt相关配置

既然是登录拦截,一定需要公钥解析jwt,我们在 gmall-gateway 中配置

首先在pom.xml中,引入所需要的依赖:

<dependency>
	<groupId>com.atguigu.gmall</groupId>
	<artifactId>gmall-common</artifactId>
	<version>0.0.1-SNAPSHOT</version>
</dependency>

然后编写application.yml属性文件,添加如下内容:

auth:
  jwt:
	pubKeyPath: D:\\gmall\\rsa\\rsa.pub # 公钥地址
	cookieName: GMALL-TOKEN

编写属性类,读取公钥:

/**
 * JWT属性类 {@link JwtProperties}
 *
 * @author zhangwen
 * @email: [email protected]
 */
@ConfigurationProperties(prefix = "auth.jwt")
@Slf4j
@Data
public class JwtProperties {
	private String pubKeyPath;
	private PublicKey publicKey;
	private String cookieName;
	
	@PostConstruct
	public void init(){
		try {
			//获取公钥
			this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
		} catch (Exception e) {
			log.error("初始化公钥失败!", e);
			throw new RuntimeException();
		}
	}
}

5.3.2. 编写代码实现登录拦截

改造AuthGatewayFilterFactory

package com.atguigu.gmall.gateway.component;

import com.atguigu.common.utils.JwtUtils;
import com.atguigu.gmall.gateway.config.JwtProperties;
import com.atguigu.gmall.gateway.util.IpUtils;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * 自定义局部过滤器 {@link AuthGatewayFilterFactory}
 *
 * @author zhangwen
 * @email: [email protected]
 */
@EnableConfigurationProperties(JwtProperties.class)
@Component
public class AuthGatewayFilterFactory
        extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.PathConfig> {

    @Autowired
    private JwtProperties properties;

    /**
     * 一定要重写构造方法
     * 告诉父类,这里使用PathConfig对象接收配置内容
     */
    public AuthGatewayFilterFactory() {
        super(PathConfig.class);
    }

    @Override
    public GatewayFilter apply(PathConfig config) {
        // 实现GatewayFilter接口
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

                // 获取request和response,注意:不是HttpServletRequest及HttpServletResponse
                ServerHttpRequest request = exchange.getRequest();
                ServerHttpResponse response = exchange.getResponse();
                // 获取当前请求的path路径
                String path = request.getURI().getPath();

                // 1.判断请求路径在不在拦截名单中,不在直接放行
                Boolean flag = false;
                for (String authPath : config.getAuthPaths()) {
                    // 如果白名单中有一个包含当前路径
                    if (path.indexOf(authPath) != -1){
                        flag = true;
                        break;
                    }
                }
                // 不在拦截名单中,放行
                if (!flag){
                    return chain.filter(exchange);
                }

                // 2.获取请求中的token
                String token = "";
                // 异步请求,通过头信息获取token
                List<String> tokenList = request.getHeaders().get("token");
                if(!CollectionUtils.isEmpty(tokenList)) {
                    token = tokenList.get(0);
                } else {
                    // 同步请求通过cookie
                    MultiValueMap<String, HttpCookie> cookies = request.getCookies();
                    if (CollectionUtils.isEmpty(cookies) || !cookies.containsKey(properties.getCookieName())) {
                        // 拦截
                        // 重定向到登录
                        // 303状态码表示由于请求对应的资源存在着另一个URI,应使用重定向获取请求的资源
                        response.setStatusCode(HttpStatus.SEE_OTHER);
                        response.getHeaders().set(HttpHeaders.LOCATION, "http://auth.gmall.com/login2.html?returnUrl="+request.getURI());
                        // 设置响应状态码为未认证
						//response.setStatusCode(HttpStatus.UNAUTHORIZED);
                        return response.setComplete();
                    }

                    // 获取cookie中的jwt
                    HttpCookie cookie = cookies.getFirst(properties.getCookieName());
                    token = cookie.getValue();
                }

                // 3.判断token是否为空
                if (StringUtils.isEmpty(token)) {
                    // 去登录
                    response.setStatusCode(HttpStatus.SEE_OTHER);
                    response.getHeaders().set(HttpHeaders.LOCATION, "http://auth.gmall.com/login2.html?returnUrl="+request.getURI());
                    return response.setComplete();
                }

                try {
                    // 4.解析jwt,获取登录信息
                    Map<String, Object> map = JwtUtils.getInfoFromToken(token, properties.getPublicKey());

                    // 5.判断token是否被盗用
                    String ip = map.get("ip").toString();
                    // 当前请求的ip
                    String curIp = IpUtils.getIpAddress(request);
                    if (!StringUtils.pathEquals(ip, curIp)){
                        // 去登陆
                        response.setStatusCode(HttpStatus.SEE_OTHER);
                        response.getHeaders().set(HttpHeaders.LOCATION, "http://auth.gmall.com/login2.html?returnUrl="+request.getURI());
                        return response.setComplete();
                    }

                    // 6.传递登录信息给后续服务
                    String userId = map.get("userId").toString();
                    System.out.println("网关从JWT获取userId:" + userId);
                    // 将userId转变成request对象。mutate:转变的意思
                    request.mutate().header("userId", userId).build();
                    exchange.mutate().request(request).build();

                    // 放行
                    return chain.filter(exchange);
                } catch (Exception e) {
                    e.printStackTrace();
                    // 7.异常,去登录
                    response.setStatusCode(HttpStatus.SEE_OTHER);
                    response.getHeaders().set(HttpHeaders.LOCATION, "http://auth.gmall.com/login2.html?returnUrl="+request.getURI());
                    return response.setComplete();
                }
            }
        };
    }

    /**
     * 指定字段顺序
     * 可以通过不同的字段分别读取:/login2.html,/ssoLogin
     * 在这里希望通过一个集合字段读取所有的路径
     * @return
     */
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("authPaths");
    }

    /**
     * 指定读取字段的结果集类型
     * 默认通过map的方式,把配置读取到不同字段
     *  例如:/login2.html,/ssoLogin
     *      由于只指定了一个字段,只能接收/login2.html
     * @return
     */
    @Override
    public ShortcutType shortcutType() {
        return ShortcutType.GATHER_LIST;
    }

    /**
     * 读取配置的内部类
     */
    @Data
    public static class PathConfig{
        private List<String> authPaths;
    }
}

5.3.3. 配置过滤器并测试

在网关配置文件中的配置如下:

- id: gmall_auth_route
  uri: lb://gmall-auth
  predicates:
  - Host=auth.gmall.com
  filters:
  - Auth=/cart

在浏览器上访问:http://auth.gmall.com/cart

重定向到了登录页面

你可能感兴趣的:(谷粒商城,java,数据库,开发语言)