乐优商城(十一)授权中心

1. 无状态登录原理

1.1 有状态登录

有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理

例如:用户登陆后,将用户的信息保存一份在服务端的 Session 中,然后响应给客户端一个 Cookie,记录对应的 Session。用户再次发起请求时,我们根据其携带的 Cookie,就能识别到对应的 Session,从而识别用户身份。

在这里插入图片描述

有状态登陆的缺点:

  • 服务端保存大量数据,增加服务端存储压力
  • 如果有多台服务器的话,还需要设置共享 Session。如果不设置共享 Session,多次请求可能访问到不同的服务器,而其他服务器没有保存对应 Session,从而无法识别用户身份。

1.2 无状态登录

无状态登陆,即服务端不保存任何客户端信息,客户端的每次请求必须具备自描述信息,通过这些信息来识别客户端身份

无状态登陆流程:

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

在这里插入图片描述

token 的安全性:

token 是识别客户端身份的唯一标示,肯定要进行严格的加密,以防被人伪造,我们将采用 JWT + RSA 非对称加密。

无状态登陆的优点:

  • 减小服务端存储压力
  • 客户端请求不依赖服务端的信息,多次请求可以访问不同服务器

1.3 JWT

1.3.1 JWT 简介

JWT( JSON Web Token),是 JSON 风格轻量级的授权和身份认证规范,可实现无状态、分布式的 Web 应用授权

1.3.2 JWT 的数据格式

JWT 的数据格式包含三部分:

  • Header,头部,由两部分组成:

    • token 类型,比如 JWT
    • 加密算法名称,比如 RSA
    {
        "alg":"RS256",
        "typ":"JWT"
    }
    

    然后,这部分 JSON 被 Base64 编码,得到第一部分数据。

  • Payload,载荷,用来存放有效数据。一般包含以下信息:

    • 用户身份信息(注意:这里因为采用 Base64 编码,可解码,因此不要存放敏感信息)
    • 注册声明,如 token 的签发时间,过期时间,签发人等
    {
        "sub": '1234567890',
        "name": 'Rick',
        "admin":true
    }
    

    然后,这部分 JSON 被 Base64 编码,得到第二部分数据。

  • Signature,签名,是整个数据的认证信息。由三部分组成:

    • Header(Base64 编码后的)
    • Payload(Base64 编码后的)
    • 服务的密钥 secret

    然后,这部分通过 Header 中声明的加密方式进行加盐 secret 组合加密,得到第三部分数据。

最终生成的 JWT:

在这里插入图片描述

1.3.3 JWT 交互流程

流程图:

在这里插入图片描述

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

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

1.3.4 加密算法

加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密)。

加密技术的要点是加密算法,加密算法可以分为三类:

  • 对称加密,如 AES
    • 基本原理:将明文分成 N 个组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所有的分组密文进行合并,形成最终的密文。
    • 优点:算法公开、计算量小、加密速度快、加密效率高
    • 缺点:双方都使用同样密钥,安全性得不到保证
  • 非对称加密,如 RSA
    • 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
      • 私钥加密,持有私钥或公钥才可以解密
      • 公钥加密,持有私钥才可解密
    • 优点:安全,难以破解
    • 缺点:算法比较耗时
  • 不可逆加密,如 MD5,SHA
    • 基本原理:加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,无法根据密文推算出明文。

1.4 JWT 结合 Zuul 鉴权

1.4.1 不使用 RSA 加密

在这里插入图片描述

  1. 用户请求登录
  2. Zuul 网关将请求转发到授权中心,请求授权
  3. 授权中心校验完成,颁发 JWT 凭证
  4. 客户端请求其它功能,携带 JWT
  5. Zuul 将 JWT 交给授权中心校验,通过后放行
  6. 用户请求到达微服务
  7. 微服务将 JWT 交给鉴权中心,鉴权同时解析用户信息
  8. 鉴权中心返回用户数据给微服务
  9. 微服务处理请求,返回响应

缺点:每次鉴权都需要访问鉴权中心,鉴权中心的访问压力非常大,这种方法显然是不推荐的。

1.4.2 使用 RSA 加密

在这里插入图片描述

  1. 首先利用 RSA 生成公钥和私钥。私钥保存在授权中心,公钥保存在 Zuul 和其他信任的微服务
  2. 用户请求登录
  3. 授权中心校验,通过后用私钥对 JWT 进行签名加密
  4. 返回 JWT 给用户
  5. 用户携带 JWT 访问
  6. Zuul 直接通过公钥解密 JWT,进行验证,验证通过则放行
  7. 请求到达微服务,微服务直接用公钥解析 JWT,获取用户信息
  8. 微服务处理请求,返回响应

2. 授权中心

2.1 搭建授权中心微服务

授权中心的功能:

  • 用户鉴权:接收用户的登录请求,通过用户中心的接口进行校验,通过后使用私钥生成 JWT 并返回
  • 服务鉴权:微服务间的调用不经过 Zuul 网关,会有风险,需要鉴权中心进行认证

由于生成 JWT,解析 JWT 这样的方法在其它微服务中也会用到,因此我们会将它们抽取成工具类。我们会把鉴权中心进行聚合,一个工具的 module,一个提供服务的 module。

2.1.1 创建 leyou-auth

  1. 右键 leyou 项目 --> New Module --> Maven --> Next

    在这里插入图片描述

  2. 填写项目信息 --> Next

    在这里插入图片描述

  3. 填写保存的位置 --> Finish

    在这里插入图片描述

  4. 删除 src 目录

2.1.2 创建 leyou-auth-common

  1. 右键 leyou-auth 项目 --> New Module --> Maven --> Next

    在这里插入图片描述

  2. 填写项目信息 --> Next

    在这里插入图片描述

  3. 填写保存的位置 --> Finish

    在这里插入图片描述

2.1.3 创建 leyou-auth-service

  1. 右键 leyou-auth 项目 --> New Module --> Maven --> Next

    在这里插入图片描述

  2. 填写项目信息 --> Next

    在这里插入图片描述

  3. 填写保存的位置 --> Finish

    在这里插入图片描述

  4. 最终目录结构

    在这里插入图片描述

  5. 添加依赖

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>leyou-auth</artifactId>
            <groupId>com.leyou.auth</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.leyou.auth</groupId>
        <artifactId>leyou-auth-service</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>com.leyou.user</groupId>
                <artifactId>leyou-user-interface</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
            </dependency>
            <dependency>
                <groupId>com.leyou.auth</groupId>
                <artifactId>leyou-auth-common</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>com.leyou.common</groupId>
                <artifactId>leyou-common</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
        </dependencies>
        
    </project>
    
  6. 创建引导类

    @EnableDiscoveryClient
    @EnableFeignClients
    @SpringBootApplication
    public class LeyouAuthApplication {
        public static void main(String[] args) {
            SpringApplication.run(LeyouAuthApplication.class, args);
        }
    }
    
  7. 编写配置文件

    server:
      port: 8087
    spring:
      application:
        name: auth-service
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:10086/eureka
        registry-fetch-interval-seconds: 10
      instance:
        lease-renewal-interval-in-seconds: 5 
        lease-expiration-duration-in-seconds: 10
    
  8. 目录结构

    在这里插入图片描述

  9. 我们修改 leyou-gateway,添加路由规则,对 leyou-auth-service 进行路由。

    zuul:
      prefix: /api
      routes:
        item-service: /item/**
        search-service: /search/**
        user-service: /user/**
        auth-service: /auth/**
    

2.2 JWT 工具类

2.2.1 创建工具类

  1. 在 leyou-auth-common 中添加依赖

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>leyou-auth</artifactId>
            <groupId>com.leyou.auth</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.leyou.auth</groupId>
        <artifactId>leyou-auth-common</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <dependencies>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.0</version>
            </dependency>
            <dependency>
                <groupId>joda-time</groupId>
                <artifactId>joda-time</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
            </dependency>
        </dependencies>
    
    </project>
    
  2. 在 leyou-auth-common 中导入 JWT 相关工具类,和载荷对象 UserInfo

    在这里插入图片描述

2.2.2 测试工具类

  1. 我们在 leyou-auth-common 中编写测试类

    public class JwtTest {
    
        private static final String pubKeyPath = "C:\\Users\\admin\\Desktop\\tmp\\rsa\\rsa.pub";
    
        private static final String priKeyPath = "C:\\Users\\admin\\Desktop\\tmp\\rsa\\rsa.pri";
    
        private PublicKey publicKey;
    
        private PrivateKey privateKey;
    
        /**
         * 生成公钥和私钥
         * @throws Exception
         */
        @Test
        public void testRsa() throws Exception {
            RsaUtils.generateKey(pubKeyPath, priKeyPath, "234");
        }
    
        /**
         * 初始化公钥和私钥
         * @throws Exception
         */
        // @Before
        public void testGetRsa() throws Exception {
            this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
            this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
        }
    
        /**
         * 私钥加密生成 token
         * @throws Exception
         */
        @Test
        public void testGenerateToken() throws Exception {
            // 生成 token
            String token = JwtUtils.generateToken(new UserInfo(20L, "jack"), privateKey, 5);
            System.out.println("token = " + token);
        }
    
        /**
         * 获取 token 中的用户信息
         * @throws Exception
         */
        @Test
        public void testParseToken() throws Exception {
            String token = "eyJhbGciOiJSUzI1NiJ9.eyJpZCI6MjAsInVzZXJuYW1lIjoiamFjayIsImV4cCI6MTUzMzI4MjQ3N30.EPo35Vyg1IwZAtXvAx2TCWuOPnRwPclRNAM4ody5CHk8RF55wdfKKJxjeGh4H3zgruRed9mEOQzWy79iF1nGAnvbkraGlD6iM-9zDW8M1G9if4MX579Mv1x57lFewzEo-zKnPdFJgGlAPtNWDPv4iKvbKOk1-U7NUtRmMsF1Wcg";
    
            // 解析token
            UserInfo user = JwtUtils.getInfoFromToken(token, publicKey);
            System.out.println("id: " + user.getId());
            System.out.println("userName: " + user.getUsername());
        }
    }
    
  2. 运行 testRsa 方法,生成公钥和私钥

    在这里插入图片描述

  3. 取消 @Before 的注释,运行 testGenerateToken 方法,私钥加密生成 token

    在这里插入图片描述

  4. 使用上面生成的 token,运行 testParseToken 方法,获取 token 中的用户信息

    在这里插入图片描述
    在这里插入图片描述

2.3 编写登录授权接口

接下来,我们需要在 leyou-auth-servcice 编写一个接口,对外提供登录授权服务。基本流程如下:

  1. 客户端携带用户名和密码请求登录

  2. 授权中心调用用户中心接口,根据用户名和密码查询用户信息

  3. 如果用户名密码正确,则能获取用户,否则登录失败

  4. 如果校验成功,则生成 JWT 并返回

2.3.1 生成公钥和私钥

  1. 我们需要在授权中心生成真正的公钥和私钥,添加相关配置到 application.yaml

    leyou:
      jwt:
        secret: vj6ad02cja3ud # 登录校验的密钥
        pubKeyPath: D:\tmp\rsa\\rsa.pub # 公钥地址
        priKeyPath: D:\tmp\rsa\\rsa.pri # 私钥地址
        expire: 30 # 过期时间,单位分钟
        cookieName: LY_TOKEN # cookie 名称
    
  2. 编写属性读取类

    @ConfigurationProperties(prefix = "leyou.jwt")
    public class JwtProperties {
    
        private String secret; // 密钥
    
        private String pubKeyPath;// 公钥
    
        private String priKeyPath;// 私钥
    
        private int expire;// token 过期时间
    
        private PublicKey publicKey; // 公钥
    
        private PrivateKey privateKey; // 私钥
    
        private String cookieName; // cookie 名称
    
        private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);
    
        /**
         * @PostContruct:在构造方法执行之后执行该方法
         */
        @PostConstruct
        public void init(){
            try {
                File pubKey = new File(pubKeyPath);
                File priKey = new File(priKeyPath);
                if (!pubKey.exists() || !priKey.exists()) {
                    // 生成公钥和私钥
                    RsaUtils.generateKey(pubKeyPath, priKeyPath, secret);
                }
                // 获取公钥和私钥
                this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
                this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
            } catch (Exception e) {
                logger.error("初始化公钥和私钥失败!", e);
                throw new RuntimeException();
            }
        }
    	
        // 省略 getter、setter 方法
    }
    

2.3.2 Controller

编写授权接口,我们接收用户名和密码,校验成功后,写入 cookie 中。

  • 请求方式:POST
  • 请求路径:/accredit
  • 请求参数:username 和 password
  • 返回结果:无
@RestController
@EnableConfigurationProperties(JwtProperties.class)
public class AuthController {

    @Autowired
    private AuthService authService;

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 登录授权
     *
     * @param username
     * @param password
     * @return
     */
    @PostMapping("/accredit")
    public ResponseEntity<Void> accredit(
            @RequestParam("username") String username,
            @RequestParam("password") String password,
            HttpServletRequest request,
            HttpServletResponse response) {
        // 登录校验
        String token = this.authService.accredit(username, password);
        // 用户校验失败
        if (StringUtils.isBlank(token)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        // 将 token 写入 cookie,并指定 httpOnly 为 true,防止通过 JS 获取和修改
        CookieUtils.setCookie(request, response, jwtProperties.getCookieName(), token, jwtProperties.getExpire() * 60, null, true);
        // 用户校验成功
        return ResponseEntity.ok().build();
    }
}

2.3.3 FeignClient

接下来我们要对用户密码进行校验,所以我们需要通过 FeignClient 去访问 user-service 微服务。

  1. 在 leyou-user-interface 工程中添加 API 接口

    public interface UserApi {
        /**
         * 根据用户名和密码查询用户
         *
         * @param username
         * @param password
         * @return
         */
        @GetMapping("query")
        public User queryUser(@RequestParam("username") String username, @RequestParam("password") String password);
    }
    
  2. 在 leyou-auth 中引入 user-service-interface 依赖

    <dependency>
        <groupId>com.leyou.user</groupId>
        <artifactId>leyou-user-interface</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    
  3. 在 leyou-auth-service 中编写 FeignClient

    @FeignClient(value = "user-service")
    public interface UserClient extends UserApi {
    }
    

2.3.4 Service

在 leyou-auth-service 中编写 AuthService

@Service
@EnableConfigurationProperties(JwtProperties.class)
public class AuthService {
    @Autowired
    private UserClient userClient;

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验用户名和密码,校验成功则返回 token
     * @param username
     * @param password
     * @return
     */
    public String accredit(String username, String password) {
        // 根据用户名和密码查询用户
        User user = userClient.queryUser(username, password);
        // 如果 user 为空,则返回 null
        if(user == null) {
            return null;
        }
        // 生成并返回 token
        try {
            UserInfo userInfo = new UserInfo();
            userInfo.setId(user.getId());
            userInfo.setUsername(user.getUsername());
            String token = JwtUtils.generateToken(userInfo, jwtProperties.getPrivateKey(), jwtProperties.getExpire());
            return token;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

2.3.5 项目完整目录

在这里插入图片描述

2.3.6 测试

  1. 启动 leyou-gateway、leyou-register、leyou-user、leyou-auth 四个微服务

  2. 打开 Postman 发送一条 POST 登录请求,响应 200,成功得到 Cookie

    在这里插入图片描述

2.4 页面登录

  1. 进入登录页面,填写用户名和密码,点击登录

    在这里插入图片描述

  2. 成功跳转到首页

    在这里插入图片描述

  3. 但打开查看 Cookie,却什么都没有

    在这里插入图片描述

2.5 解决 Cookie 写入问题

2.5.1 跟踪 Cookie

  1. 在 CookieUtils.setCookie 这一行打上断点

    在这里插入图片描述

  2. Debug 运行 leyou-auth,在页面登录

    在这里插入图片描述

  3. 我们单步调试,找到 response 中 cookies 的 domain。

    在这里插入图片描述

    分析:这里我们发现 domain 与我们的域名不匹配。我们的域名为 www.leyou.com,要想访问到 cookie,那么 domain 应该为 .leyou.com。既然发现是 domain 的值有问题,我们就再来看看 domain 的值是如何设置的。

  4. 再次 Debug 进入 CookieUtils.setCookie,我们找到 getDomainName 方法,发现 domainName 是根据 serverName 处理后得到的,而我们的 serverName 为 192.168.41.1:8087

    在这里插入图片描述

  5. 所以最后 domainName 为 168.41.1

    在这里插入图片描述

2.5.2 分析 serverName 问题

在这里插入图片描述
我们发起请求时,serverName 为 api.leyou.com,怎么最后就变成了 192.168.41.1:8087 呢?

这里的 serverName 其实就是"请求的时的主机名:Host",之所以改变有两个原因:

  1. 我们使用了 nginx 反向代理,当监听到 api.leyou.com 的时候,会自动将请求转发至 127.0.0.1:10010

    在这里插入图片描述

  2. 而后请求到达我们的网关 Zuul,Zuul 就会根据路径匹配,我们的请求是 /api/auth,根据规则被转发到了 localhost:8087,即我们的授权中心,所以最后 serverName 就变成了 192.168.41.1:8087。

    在这里插入图片描述

2.5.3 解决问题

  1. 我们首先去更改 nginx 配置,让它不要修改我们的 host,并重启 nginx。

    在这里插入图片描述

  2. 修改网关配置

    在这里插入图片描述

2.5.4 再次测试

  1. 重启 leyou-gateway、leyou-register、leyou-user、leyou-auth 四个微服务

  2. 登录后,成功获得 Cookie

    在这里插入图片描述

3. 首页显示登录用户

虽然 Cookie 已经成功写入,但是我们首页的顶部还是没有显示登录用户信息

在这里插入图片描述

这里需要向后台发起请求,根据 cookie 获取当前用户的信息

在这里插入图片描述

3.1 接口说明

我们在 leyou-auth-service 中定义校验用户登录状态接口,通过 cookie 获取 token,然后校验通过返回用户信息。

  • 请求方式:GET
  • 请求路径:/verify
  • 请求参数:无,不过我们需要从 cookie 中获取 token 信息
  • 返回结果:UserInfo,校验成功返回用户信息;校验失败,则返回 401

3.2 实现验证用户信息

在 AuthController 中添加 verifyUser 方法

/**
 * 校验用户登录状态
 *
 * @param token
 * @return
 */
@GetMapping("verify")
public ResponseEntity<UserInfo> verifyUser(@CookieValue("LY_TOKEN") String token) {
    UserInfo userInfo = null;
    // 从 token 中解析 token 信息
    try {
        userInfo = JwtUtils.getInfoFromToken(token, this.jwtProperties.getPublicKey());
        // 解析不成功,返回未授权
        if(userInfo == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        // 解析成功返回用户信息
        return ResponseEntity.ok(userInfo);
    } catch (Exception e) {
        e.printStackTrace();
    }
    // 出现异常,返回未授权
    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

3.3 测试

  1. 重启 leyou-auth 微服务

  2. 再次登录,首页成功显示用户信息

    在这里插入图片描述

3.4 刷新 token 过期时间

每当用户在页面进行新的操作,都应该刷新 token 的过期时间,否则 30 分钟后用户的登录信息就无效了。而刷新其实就是重新生成一份 token,然后写入 cookie 即可。

那么问题来了:我们怎么知道用户有操作呢?

事实上,每当用户来查询其个人信息,就证明他正在浏览网页,此时刷新 cookie 是比较合适的时机。因此我们可以对刚刚的校验用户登录状态的接口进行改进,加入刷新 token 的逻辑

/**
 * 校验用户登录状态
 *
 * @param token
 * @return
 */
@GetMapping("verify")
public ResponseEntity<UserInfo> verifyUser(@CookieValue("LY_TOKEN") String token, HttpServletRequest request, HttpServletResponse response) {
    UserInfo userInfo = null;
    // 从 token 中解析 token 信息
    try {
        userInfo = JwtUtils.getInfoFromToken(token, this.jwtProperties.getPublicKey());
        // 解析不成功,返回未授权
        if(userInfo == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        // 解析成功要重新刷新 token
        token = JwtUtils.generateToken(userInfo, this.jwtProperties.getPrivateKey(), this.jwtProperties.getExpire());
        // 更新 cookie 中的 token
        CookieUtils.setCookie(request, response, this.jwtProperties.getCookieName(), token, this.jwtProperties.getExpire()*60,null,true);

        // 解析成功返回用户信息
        return ResponseEntity.ok(userInfo);
    } catch (Exception e) {
        e.printStackTrace();
    }
    // 出现异常,返回未授权
    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

4. 网关登录拦截器

接下来,我们在 Zuul 编写拦截器,并设置访问路径的白名单。对访问路径不在白名单中的用户进行登录校验,如果发现未登录,则进行拦截。

4.1 JWT 相关配置

如果要进行登录校验,那肯定要先进行 JWT 相关配置。

  1. 在 leyou-gateway 中添加依赖

    <dependency>
        <groupId>com.leyou.auth</groupId>
        <artifactId>leyou-auth-common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.leyou.common</groupId>
        <artifactId>leyou-common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    
  2. 编写 application.yaml 配置文件

    leyou:
      jwt:
        pubKeyPath: D:\tmp\rsa\\rsa.pub # 公钥地址
        cookieName: LY_TOKEN # cookie的名称
    
  3. 编写属性读取类,读取 jwt 的配置

    @ConfigurationProperties(prefix = "leyou.jwt")
    public class JwtProperties {
    
        private String pubKeyPath;// 公钥
    
        private PublicKey publicKey; // 公钥
    
        private String cookieName;
    
        private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);
    
        @PostConstruct
        public void init(){
            try {
                // 获取公钥和私钥
                this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
            } catch (Exception e) {
                logger.error("初始化公钥失败!", e);
                throw new RuntimeException();
            }
        }
    
        // 省略 getter、setter 方法
    }
    

4.2 编写过滤器

在这里插入图片描述

过滤器基本流程:

  1. 获取 cookie 中的 token

  2. 通过 JWT 对 token 进行校验

    1. 通过:放行
    2. 不通过:返回 401
@Component
@EnableConfigurationProperties(JwtProperties.class)
public class LoginFilter extends ZuulFilter {

    @Autowired
    private JwtProperties jwtProperties;

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 10;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        // 获取上下文
        RequestContext context = RequestContext.getCurrentContext();
        // 获取 request
        HttpServletRequest request = context.getRequest();
        // 获取 token
        String token = CookieUtils.getCookieValue(request, this.jwtProperties.getCookieName());
        // 校验
        try {
            // 校验通过什么都不做,即放行
            JwtUtils.getInfoFromToken(token, this.jwtProperties.getPublicKey());
        } catch (Exception e) {
            // 校验出现异常,返回 401
            context.setSendZuulResponse(false);
            context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        }
        return null;
    }
}

4.3 测试

  1. 重启 leyou-gateway 微服务

  2. 再次访问首页,发现校验用户登录状态的请求也被拦截了,这显然是不对的,我们对一些路径是不需要进行拦截的

    在这里插入图片描述

4.4 设置白名单

  1. 在 application.yaml 中添加白名单访问路径

    leyou:
      filter:
        allowPaths:
          - /api/auth
          - /api/search
          - /api/user/register
          - /api/user/check
          - /api/user/code
          - /api/item
    
  2. 编写属性读取类,读取 filter 的配置

    @ConfigurationProperties(prefix = "leyou.filter")
    public class FilterProperties {
    
        private List<String> allowPaths;
    
        public List<String> getAllowPaths() {
            return allowPaths;
        }
    
        public void setAllowPaths(List<String> allowPaths) {
            this.allowPaths = allowPaths;
        }
    }
    
  3. 在过滤器 LoginFilter 中的 shouldFilter 方法中添加判断逻辑,如果请求路径是白名单中的路径,过滤器就不生效。

    @Component
    @EnableConfigurationProperties({JwtProperties.class, FilterProperties.class})
    public class LoginFilter extends ZuulFilter {
    
        @Autowired
        private JwtProperties jwtProperties;
    
        @Autowired
        private FilterProperties filterProperties;
    
        @Override
        public String filterType() {
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            return 10;
        }
    
        @Override
        public boolean shouldFilter() {
            // 获取上下文
            RequestContext requestContext = RequestContext.getCurrentContext();
            // 获取 request
            HttpServletRequest request = requestContext.getRequest();
            // 获取请求路径
            String requestURI = request.getRequestURI();
            // 判断是否在白名单中
            for (String path : this.filterProperties.getAllowPaths()) {
                // 然后判断是否是符合
                if (StringUtils.contains(requestURI, path)) {
                    return false;
                }
            }
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
            // 获取上下文
            RequestContext context = RequestContext.getCurrentContext();
            // 获取 request
            HttpServletRequest request = context.getRequest();
            // 获取 token
            String token = CookieUtils.getCookieValue(request, this.jwtProperties.getCookieName());
            // 校验
            try {
                // 校验通过什么都不做,即放行
                JwtUtils.getInfoFromToken(token, this.jwtProperties.getPublicKey());
            } catch (Exception e) {
                // 校验出现异常,返回 401
                context.setSendZuulResponse(false);
                context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
            }
            return null;
        }
    }
    

4.5 再次测试

  1. 重启 leyou-gateway 微服务

  2. 在白名单中的请求,没有被过滤

    在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bm1998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值