java电商项目(七)

本文涉及的产品
实时数仓Hologres,5000CU*H 100GB 3个月
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
实时计算 Flink 版,5000CU*H 3个月
简介: 微服务网关作为系统唯一对外的入口,位于客户端和服务端之间,处理非业务功能,如路由请求、鉴权、监控、缓存、限流等。它解决了客户端直接调用多个微服务带来的复杂性、跨域请求、认证复杂、难以重构等问题。常用的微服务网关技术有Nginx、Zuul和Spring Cloud Gateway。Spring Cloud Gateway因其集成断路器、路径重写和较好的性能而被广泛使用。本文介绍了如何使用Spring Cloud Gateway搭建后台网关系统,包括引入依赖、配置文件、跨域配置、路由过滤配置、负载均衡、限流等。此外,还详细讲解了RBAC权限数据管理、组织机构管理单点登录(SSO)及JWT鉴权的实现

[TOC]

1 微服务网关

1.1 微服务网关的概述

网关是系统的唯一对外的入口,介于客户端和服务端之间的中间层,所有的外部请求都会先经过 网关这一层。网关处理非业务功能提供路由请求、鉴权、监控、缓存、限流等功能(类似所有请求都会经过的过滤器,类似演唱会过安检,检票员还可以监控有多少人进场,检查进场人员是否合法)(比如下单操作需要用户登陆,在Gateway层判断用户是否登陆),这样既提高业务灵活性又不缺安全性。

不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:

  • 客户端会多次请求不同的微服务,增加了客户端的复杂性
  • 存在跨域请求,在一定场景下处理相对复杂
  • 认证复杂,每个服务都需要独立认证
  • 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施
  • 某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难

以上这些问题可以借助网关解决。

网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 网关来做,这样既提高业务灵活性又不缺安全性,典型的架构图如图所示:

1557824391432

优点如下:

  • 安全 ,只有网关系统对外进行暴露,微服务可以隐藏在内网,通过防火墙保护。
  • 易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
  • 易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
  • 减少了客户端与各个微服务之间的交互次数
  • 易于统一授权。

总结:微服务网关就是一个系统,通过暴露该微服务网关系统,方便我们进行相关的鉴权,安全控制,日志统一处理,易于监控的相关功能。

1.2 微服务网关技术

实现微服务网关的技术有很多,

  • nginx Nginx (tengine x) 是一个高性能的HTTP反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务
  • zuul ,Zuul 是 Netflix 出品的一个基于 JVM 路由和服务端的负载均衡器。
  • spring-cloud-gateway, 是spring 出品的 基于spring 的网关项目,集成断路器,路径重写,性能比Zuul好。

我们使用gateway这个网关技术,无缝衔接到基于spring cloud的微服务开发中来。

gateway官网:

https://spring.io/projects/spring-cloud-gateway

2 网关系统使用

2.1 需求分析

​ 由于我们开发的系统 有包括前台系统和后台系统,后台的系统 给管理员使用。那么也需要调用各种微服务,所以我们针对 系统管理搭建一个网关系统。分析如下:

1561959723219

2.2 搭建后台网关系统

2.2.1 搭建分析

创建网关微服务工程gateway,目录结构如下:

1561961106274

2.2.2 工程搭建

(1)引入依赖

pom.xml如下:

<?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>legou-parent</artifactId>
        <groupId>com.lxs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>gateway</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <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.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>

    </dependencies>

</project>

(2)引导类

gateway/src/main/java/com/lxs/clound/GatewayApplication.java

package com.lxs.clound;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class GatewayApplication {
   

    public static void main(String[] args) {
   
        SpringApplication.run(GatewayApplication.class, args);
    }

}

(3)配置文件

gateway/src/main/resources/bootstrap.yml:

spring:
  application:
    name: gateway

config-repo/gateway.yml

server:
  port: 8062
ribbon:
  ConnectTimeout: 250 # 连接超时时间(ms)
  ReadTimeout: 2000 # 通信超时时间(ms)
  OkToRetryOnAllOperations: true # 是否对所有操作重试
  MaxAutoRetriesNextServer: 2 # 同一服务不同实例的重试次数
  MaxAutoRetries: 1 # 同一实例的重试次数
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMillisecond: 60000 # 熔断超时时长:6000ms

2.3 跨域配置

有时候,我们需要对所有微服务跨域请求进行处理,则可以在gateway中进行跨域支持。添加如下代码:

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allow-credentials: true
            allowed-origins: "*"
            allowed-headers: "*"
            allowed-methods: "*"
            max-age: 3600

2.4 网关过滤配置

1562033084310

路由过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应。 如上图,根据请求路径路由到不同微服务去,这块可以使用Gateway的路由过滤功能实现。

内置的过滤器工厂有22个实现类,包括 头部过滤器、路径过滤器、Hystrix 过滤器 、请求URL 变更过滤器,还有参数和状态码等其他类型的过滤器。根据过滤器工厂的用途来划分,可以分为以下几种:Header、Parameter、Path、Body、Status、Session、Redirect、Retry、RateLimiter和Hystrix。

2.4.1 路径匹配过滤配置

我们还可以根据请求路径实现对应的路由过滤操作,例如请求中以/brand/路径开始的请求,都直接交给http://localhost:8062服务处理,如下配置:

1562043573738

上图配置如下:

spring:
  cloud:
    gateway:
      routes:
        - id: item-service
          uri: http://localhost:9005
          predicates:
            - Path=/api/item/**

测试请求http://localhost:8062/api/item/brand/list,效果如下:

1562043787417

2.4.2 StripPrefix 过滤配置

很多时候也会有这么一种请求,用户请求路径是/api/item/brand/list,而真实路径是/brand/list,这时候我们需要去掉/api/item才是真实路径,此时可以使用StripPrefix功能来实现路径的过滤操作,如下配置:

1562045331974

上图配置如下:

      default-filters:
        - StripPrefix=2

测试请求http://localhost:8001/api/item/brand/list,效果如下:

1562045175964

2.4.3 LoadBalancerClient 路由过滤器(客户端负载均衡)

上面的路由配置每次都会将请求给指定的URL处理,但如果在以后生产环境,并发量较大的时候,我们需要根据服务的名称判断来做负载均衡操作,可以使用LoadBalancerClientFilter来实现负载均衡调用。LoadBalancerClientFilter会作用在url以lb开头的路由,然后利用loadBalancer来获取服务实例,构造目标requestUrl,设置到GATEWAY_REQUEST_URL_ATTR属性中,供NettyRoutingFilter使用。

修改application.yml配置文件,代码如下:

1562045841241

上图配置如下:

        - id: item-service
          uri: lb://item-service
          predicates:
            - Path=/api/item/**

测试请求路径http://localhost:8001/api/item/brand/list

1562045175964

2.5 网关限流

网关可以做很多的事情,比如,限流,当我们的系统 被频繁的请求的时候,就有可能 将系统压垮,所以 为了解决这个问题,需要在每一个微服务中做限流操作,但是如果有了网关,那么就可以在网关系统做限流,因为所有的请求都需要先通过网关系统才能路由到微服务中。

2.5.1 思路分析

2.5.2 令牌桶算法

令牌桶算法是比较常见的限流算法之一,大概描述如下:
1)所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)根据限流大小,设置按照一定的速率往桶里添加令牌;
3)桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
4)请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
5)令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流

如下图:

1557910299016

这个算法的实现,有很多技术,Guaua是其中之一,redis客户端也有其实现。

2.5.3 漏桶算法

1560774438337

算法思想是:

  • 水(请求)从上方倒入水桶,从水桶下方流出(被处理);
  • 来不及流出的水存在水桶中(缓冲),以固定速率流出;
  • 水桶满后水溢出(丢弃)。
  • 这个算法的核心是:缓存请求、匀速处理、多余的请求直接丢弃。

两种算法的区别

两者主要区别在于“漏桶算法”能够强行限制数据的传输速率,而“令牌桶算法”在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,所以它适合于具有突发特性的流量。

2.5.4 网关限流

spring cloud gateway 默认使用redis的RateLimter限流算法来实现。所以我们要使用首先需要引入redis的依赖

(1)引入redis依赖

在gateway的pom.xml中引入redis的依赖

<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>

(2)定义KeyResolver

在Applicatioin引导类中添加如下代码,KeyResolver用于计算某一个类型的限流的KEY也就是说,可以通过KeyResolver来指定限流的Key。

我们可以根据IP来限流,比如每个IP每秒钟只能请求一次,在GatewayWebApplication定义key的获取,获取客户端IP,将IP作为key,如下代码:

/***
 * IP限流
 * @return
 */
@Bean(name="ipKeyResolver")
public KeyResolver userKeyResolver() {
   
    return new KeyResolver() {
   
        @Override
        public Mono<String> resolve(ServerWebExchange exchange) {
   
            //获取远程客户端IP
            String hostName = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
            System.out.println("hostName:"+hostName);
            return Mono.just(hostName);
        }
    };
}

(3)修改application.yml中配置项,指定限制流量的配置以及REDIS的配置,如图

修改如下图:

1562049737562

完成配置代码如下:

config-repo/gateway.yml

server:
  port: 8062
spring:
  redis:
    host: 192.168.220.110
    port: 6379
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allow-credentials: true
            allowed-origins: "*"
            allowed-headers: "*"
            allowed-methods: "*"
            max-age: 3600
      routes:
        - id: security-service
          uri: lb://security-service
          predicates:
            - Path=/api/security/**
        - id: admin-service
          uri: lb://admin-service
          predicates:
            - Path=/api/admin/**
        - id: route-service
          uri: lb://route-service
          predicates:
            - Path=/api/route/**
        - id: item-service
          uri: lb://item-service
          predicates:
            - Path=/api/item/**
          filters:
            - name: RequestRateLimiter
              args:
                key-resolver: "#{@ipKeyResolver}"
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 4
        - id: search-service
          uri: lb://search-service
          predicates:
            - Path=/api/search/**
      default-filters:
        - StripPrefix=2
ribbon:
  ConnectTimeout: 250 # 连接超时时间(ms)
  ReadTimeout: 2000 # 通信超时时间(ms)
  OkToRetryOnAllOperations: true # 是否对所有操作重试
  MaxAutoRetriesNextServer: 2 # 同一服务不同实例的重试次数
  MaxAutoRetries: 1 # 同一实例的重试次数
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMillisecond: 60000 # 熔断超时时长:6000ms

解释:

redis-rate-limiter.replenishRate是您希望允许用户每秒执行多少请求,而不会丢弃任何请求。这是令牌桶填充的速率

redis-rate-limiter.burstCapacity是指令牌桶的容量,允许在一秒钟内完成的最大请求数,将此值设置为零将阻止所有请求。

key-resolver: "#{@ipKeyResolver}" 用于通过SPEL表达式来指定使用哪一个KeyResolver.

如上配置:

表示 一秒内,允许 一个请求通过,令牌桶的填充速率也是一秒钟添加一个令牌。

最大突发状况 也只允许 一秒内有一次请求,可以根据业务来调整 。

多次请求会发生如下情况

1562049920902

3 RBAC权限数据管理

3.1 RBAC简介

RBAC(基于角色的权限控制 role base access control)是一种设计模式,是用来设计和管理权限相关数据的一种模型

RBAC权限数据的管理,都是重复的CRUD的操作,这里我们就不再重复的从0到1开发,我们只是把模块中需要注意的逻辑和代码讲一下,重复代码不再复述

RBAC模式:基于角色的访问控制,表结构如下:

3.2 需求分析

3.2.1 用户管理

用户列表

用户添加修改

3.2.2 角色管理

角色列表

角色修改

3.2.3 菜单管理

菜单列表

菜单修改

菜单不同于单表管理,是一个树状表结构管理,这里我们重点关注菜单(树状表结构的数据)的增删改查

3.3 导入权限数据管理工程

这里我们直接导入素材中的权限管理工程,因为业务比较复杂,不再从0到1开发,大家重点关注重点模块的业务逻辑实现即可

3.4 实体类

package com.lxs.legou.security.po;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.lxs.legou.core.po.BaseEntity;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;

import java.util.Date;

@Data
@TableName("user_")
public class User extends BaseEntity {

   @TableField("user_name_")
   private String userName; //登录名
   @TableField("real_name_")
   private String realName; //真实姓名
   @TableField("password_")
   private String password;
   @TableField("salt")
    private String salt; //加密密码的盐
   @TableField("sex_")
   private String sex;
   @TableField("tel_")
   private String tel;
   @TableField("email_")
   private String email;
   @TableField("desc_")
   private String desc;
   @TableField("lock_")
   private Boolean lock; //是否锁定
   @TableField("birthday_")
   @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
   @DateTimeFormat(pattern="yyyy-MM-dd")
   private Date birthday;
   @TableField("principal_")
   private Boolean principal; //是否为部门负责人,用于"常用语:直接上级"
   @TableField("dept_id_")
   private Long deptId; //部门
   @TableField("post_id_")
   private Long postId; //岗位

   @TableField(exist = false)
   private String deptName; //部门名称,用于列表显示

   @TableField(exist = false)
   private Long[] roleIds; //瞬时属性,用户的角色列表,如:[1,3,4,5]

   @TableField(exist = false)
   private String postName; //部门名称,用于列表显示

   public String credentialsSalt() {
      return userName + salt;
   }

}
package com.lxs.legou.security.po;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lxs.legou.core.po.BaseEntity;
import lombok.Data;


@Data
@TableName("role_")
public class Role extends BaseEntity {

   @TableField("name_")
   private String name;
   @TableField("title_")
   private String title;
   @TableField("desc_")
   private String desc;

   @TableField(exist = false)
   private Long[] userIds; //瞬时属性,角色的用户列表,如:[1,3,4,5]

}
package com.lxs.legou.security.po;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.lxs.legou.core.po.BaseTreeEntity;
import lombok.Data;


@Data
@TableName("menu_")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class Menu extends BaseTreeEntity {

   @TableField("path_")
   private String path;
   @TableField("name_")
   private String name;
   @TableField("component_")
   private String component;
   @TableField("hide_in_menu_")
   private Boolean hideInMenu = false; //设为true后在左侧菜单不会显示该页面选项
   @TableField("not_cache_")
   private Boolean notCache = false; //设为true后页面不会缓存
   @TableField("icon_")
   private String icon; //该页面在左侧菜单、面包屑和标签导航处显示的图标,如果是自定义图标,需要在图标名称前加下划线'_'

   @TableField(exist = false)
   private Long roleId; //查询条件,拥有查询角色的菜单
   @TableField(exist = false)
   private Long userId; //查询条件,拥有查询用户的菜单
   @TableField(exist = false)
   private boolean selected; //角色选择菜单,选中角色已有的菜单

}

这里需要重点注意Menu实体类,因为是树状结构的数据需要继承BaseTreeEntity,通过parent_id进行自身的关联,BaseTreeEntity代码

package com.lxs.legou.core.po;

import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;


@Data
@JsonIgnoreProperties(value = {"handler"}) //避免懒加载,json转换报错
public class BaseTreeEntity extends BaseEntity {

    @TableField("order_")
    private Integer order; //排序字段
    @TableField("parent_id_")
    private Long parentId; //父节点id
    @TableField("title_")
    private String title; //节点名称
    @TableField("expand_")
    private Boolean expand = false; //是否展开节点

}

3.5 DAO和映射文件

参考具体导入的代码,实现具体用户角色,菜单的CRUD操作,因为代码较多这里不再转帖

3.6 Service

package com.lxs.legou.security.service;

import com.lxs.legou.core.service.ICrudService;
import com.lxs.legou.security.po.Role;
import com.lxs.legou.security.po.User;

import java.util.List;


public interface IUserService extends ICrudService<User> {

    /**
     * 根据用户id查询角色
     * @param id
     * @return
     */
    public List<Role> selectRoleByUser(Long id);

    /**
     * 根据用户名,查询用户个数
     * @param userName
     * @return
     */
    public Integer findCountByUserName(String userName);

    /**
     * 根据用户名查询用户
     * @param userName
     * @return
     */
    public User getUserByUserName(String userName);

}
package com.lxs.legou.security.service;

import com.lxs.legou.core.service.ICrudService;
import com.lxs.legou.security.po.Role;
import com.lxs.legou.security.po.User;

import java.util.List;


public interface IRoleService extends ICrudService<Role> {

    /**
     * 查询角色的用户
     * @param id
     * @return
     */
    public List<User> selectUserByRole(Long id);

}
package com.lxs.legou.security.service;

import com.lxs.legou.core.service.ICrudService;
import com.lxs.legou.security.po.Menu;
import org.springframework.stereotype.Service;

import java.util.List;


@Service
public interface IMenuService extends ICrudService<Menu> {

    /**
     * 查询用户的菜单
     * @param userId
     * @return
     */
    public List<Menu> listByUser(Long userId);

    /**
     * 查询所有菜单,选中角色已有的菜单
     * @param roleId
     * @return
     */
    public List<Menu> listChecked(Long roleId) ;

    /**
     * 关联角色和菜单
     * @param roleId
     * @param menuIds
     */
    public void doAssignMenu2Role(Long roleId, Long[] menuIds);

}

具体实现类参考项目代码

3.7 Controller

package com.lxs.legou.security.controller;

import com.lxs.legou.core.controller.BaseController;
import com.lxs.legou.security.po.Role;
import com.lxs.legou.security.po.User;
import com.lxs.legou.security.service.IRoleService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;


@RestController
@RequestMapping("/role")
public class RoleController extends BaseController<IRoleService, Role> {

   @Override
   public void afterEdit(Role entity) {
      //生成用户列表, 如:1,3,4
      List<User> users = service.selectUserByRole(entity.getId());
      Long[] ids = new Long[users.size()];
      for (int i=0; i< users.size(); i++) {
         ids[i] = users.get(i).getId();
      }
      entity.setUserIds(ids);
   }

}

其他UserController和MenuController参考具体代码

3.8 网关配置

        - id: security-service
          uri: lb://security-service
          predicates:
            - Path=/api/security/**

3.8 前端代码

3.8.1 菜单管理

因为菜单管理不同于单表操作,展示的树状结构的数据显示,这里关注下具体实现

完整代码,参考具体前端代码

3.8.2 用户角色管理

用户列表

用户修改

选择角色的组件

<template>

    <Select :value="currentValue" @on-change="handleInput"  multiple >
        <Option v-for="item in roleList" :value="item.id" :key="item.id">{
  { item.name }}</Option>
    </Select>

</template>

<script>
import instance from '@/libs/api/index'

export default {
  name: 'selectRoles',
  data () {
    return {
      roleList: []
    }
  },
  computed: {
    currentValue: function () {
      return this.value
    }
  },
  props: ['value'], // 接收一个 value prop
  methods: {
    handleInput (value) {
      this.$emit('input', value) // 触发 input 事件,并传入新值
    }
  },
  mounted () {
    instance.post(`/security/role/list`).then(response => {
      this.roleList = response.data
    }).catch(error => {
      console.log(error)
    })
  }
}
</script>

具体角色管理和用户管理相似,这里不再复述

注意:前端代码比较复杂可以拷贝,做到会修改即可,有兴趣的同学可以拿出时间从0到1编码

4 组织机构管理

组织机构模块主要包括部门,职位,和字典的管理,其中部门跟菜单相似是树状结构数据,跟权限数据模块管理类似,这里时间问题,不再重复讲解,大家导入提供的组织机构管理工程即可,后续可以使用,能够修改即可

网关路由配置

        - id: admin-service
          uri: lb://admin-service
          predicates:
            - Path=/api/admin/**

4.1 表结构

4.2 功能介绍

岗位管理

部门管理

权限数据和组织机构数据管理,不涉及新的技术,但是业务逻辑比较复杂,这里我们导入即可,不再重复实现crud操作了,有时间大家根据标准代码,自己写一遍

4.3 代码实现

具体参考标准代码,因为跟之前业务逻辑基本相同,这里不再重复

5 单点登录(SSO)分析

5.1.什么是SSO

在企业发展初期,企业使用的系统很少,通常一个或者两个,每个系统都有自己的登录模块,运营人员每天用自己的账号登录,很方便。但随着企业的发展,用到的系统随之增多,运营人员在操作不同的系统时,需要多次登录,而且每个系统的账号都不一样,这对于运营人员来说,很不方便。于是,就想到是不是可以在一个系统登录,其他系统就不用登录了呢?这就是单点登录要解决的问题。单点登录英文全称Single Sign On,简称就是SSO。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。

5.2.普通认证机制

如上图所示,我们在浏览器(Browser)中访问一个应用,这个应用需要登录,我们填写完用户名和密码后,完成登录认证。这时,我们在这个用户的session中标记登录状态为yes(已登录),同时在浏览器(Browser)中写入Cookie,这个Cookie是这个用户的唯一标识。下次我们再访问这个应用的时候,请求中会带上这个Cookie,服务端会根据这个Cookie找到对应的session,通过session来判断这个用户是否登录。如果不做特殊配置,这个Cookie的名字叫做jsessionid,值在服务端(server)是唯一的。

5.3.同域名下的SSO

一个企业一般情况下只有一个域名,通过二级域名区分不同的系统。比如我们有个域名叫做:a.com,同时有两个业务系统分别为:app1.a.com和app2.a.com。我们要做单点登录(SSO),需要一个登录
系统,叫做:sso.a.com。

我们只要在sso.a.com登录,app1.a.com和app2.a.com就也登录了。通过上面的登陆认证机制,我们可以知道,在sso.a.com中登录了,其实是在sso.a.com的服务端的session中记录了登录状态,同时在浏览器端(Browser)的sso.a.com下写入了Cookie。那么我们怎么才能让app1.a.com和app2.a.com登录呢?这里有两个问题:

  • Cookie是不能跨域的,我们Cookie的domain属性是sso.a.com,在给app1.a.com和app2.a.com发送请求是带不上的。
  • sso、app1和app2是不同的应用,它们的session存在自己的应用内,是不共享的

那么我们如何解决这两个问题呢?针对第一个问题,sso登录以后,可以将Cookie的域设置为顶域,即.a.com,这样所有子域的系统都可以访问到顶域的Cookie。我们在设置Cookie时,只能设置顶域和自己的域,不能设置其他的域。比如:我们不能在自己的系统中给baidu.com的域设置Cookie

Cookie的问题解决了,我们再来看看session的问题。我们在sso系统登录了,这时再访问app1,Cookie也带到了app1的服务端(Server),app1的服务端怎么找到这个Cookie对应的Session呢?这里就要把3个系统的Session共享,如图所示。共享Session的解决方案有很多,例如:SpringSession。这样第2个问题也解决了。

5.4.基于token的认证

最近几年由于单页app、web APIs等的兴起,基于token的身份验证开始流行。当我们谈到利用token进行认证,我们一般说的就是利用JSON Web Tokens(JWTs)进行认证。虽然有不同的方式来实现token,事实上,JWTs 已成为标准。因此在本文中将互换token与JWTs。

基于token的身份验证是无状态的,服务器不需要记录哪些用户已经登录或者哪些JWTs已经处理。每个发送到服务器的请求都会带上一个token,服务器利用这个token检查确认请求的真实性。

这里可以把token理解成一张演唱会的门票。服务器(演唱会主办方)每次只需要检查你这张门票的有效性,不需要知道你这张门票是在哪里买的,从谁买的,什么时候买的等等。不同等级的门票可以坐的位置不同,同样的,权限不同的用户可以进行的操作也不同。

6 JWT讲解

6.1 需求分析

我们之前已经搭建过了网关,使用网关在网关系统中比较适合进行权限校验。

1562059385713

那么我们可以采用JWT的方式来实现鉴权校验。

6.2 什么是JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递可靠的信息。

6.3 JWT的构成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。

{
   "typ":"JWT","alg":"HS256"}

在头部指明了签名算法是HS256算法。 我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

小知识:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64EncoderBASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码

载荷(payload)

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

(1)标准中注册的声明(建议但不强制使用)

iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

(2)公共的声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

(3)私有的声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

这个指的就是自定义的claim。比如下面面结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。

定义一个payload:

{"sub":"1234567890","name":"John Doe","admin":true}

然后将其进行base64加密,得到Jwt的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

签证(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的)

payload (base64后的)

secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

6.4 JJWT的介绍和使用

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

官方文档:

https://github.com/jwtk/jjwt

6.4.1 创建TOKEN

(1)依赖引入

在gateway项目中的pom.xml中添加依赖:

<!--鉴权-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

(2)创建测试

在gateway的/test/java下创建测试类,并设置测试方法

public class TestJWT {
   

    @Test
    public void createJWT() {
   
        JwtBuilder builder = Jwts.builder()
                .setId("12356") //设置唯一编号
                .setSubject("password123")  //主题
                .setIssuedAt(new Date()) //签发时间
                .signWith(SignatureAlgorithm.HS256, "123456"); //签名,采用HS256算法加密,秘钥

        System.out.println(builder.compact());
    }
}

运行打印结果:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiLllK_kuIDnmoTmoIfor4YiLCJpc3MiOiLpooHlj5HogIUiLCJzdWIiOiLkuLvpopgiLCJleHAiOjE2MDIzMjIwMDEsIm15Y2l0eSI6ImJlaWppbmciLCJteWFkZHJlc3MiOiJjbiJ9.c7UxJaQVxulwVqvIZLP_8RxSTbKOyixcU6mgMFaYrw0

再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。

6.4.2 TOKEN解析

我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。

    @Test
    public void testParseJwt() {
   
        String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjM1NiIsInN1YiI6InBhc3N3b3JkMTIzIiwiaWF0IjoxNjA1NzY4NzQ4LCJyb2xlIjoiQURNSU4iLCJteWNpdHkiOiJoYW5nemhvdSIsIm15YWRkcmVzcyI6ImNuIn0.598OCjR-ItxRat7_VvWklUbIgGehJuEcXu21EEub8vs";
        Claims claims = Jwts.parser()
                .setSigningKey("123456")
                .parseClaimsJws(jwt)
                .getBody();
        System.out.println(claims);

        System.out.println(claims.getSubject());
    }

运行打印效果:

{id=12356, sub=password123, iat=1562062287}

试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token.

6.4.3 设置过期时间

有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间。

6.4.3.1 token过期设置
    @Test
    public void createJWT() {
        long l1 = System.currentTimeMillis(); //当前时间戳
        long l2 = l1 + 20000; //过期时间错
        JwtBuilder builder = Jwts.builder()
                .setId("12356") //设置唯一编号
                .setSubject("password123")  //主题
                .setIssuedAt(new Date()) //签发时间
                .setExpiration(new Date(l2))
                .signWith(SignatureAlgorithm.HS256, "lxs"); //签名,采用HS256算法加密,秘钥
        System.out.println(builder.compact());
    }

解释:

.setExpiration(date)//用于设置过期时间 ,参数为Date类型数据

运行,打印效果如下:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjI5MjUsImV4cCI6MTU2MjA2MjkyNX0._vs4METaPkCza52LuN0-2NGGWIIO7v51xt40DHY1U1Q
6.4.3.2 解析TOKEN
/***
 * 解析Jwt令牌数据
 */
@Test
public void testParseJwt(){
   
    String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjI5MjUsImV4cCI6MTU2MjA2MjkyNX0._vs4METaPkCza52LuN0-2NGGWIIO7v51xt40DHY1U1Q";
    Claims claims = Jwts.parser().
            setSigningKey("lxs").
            parseClaimsJws(compactJwt).
            getBody();
    System.out.println(claims);
}

打印效果:

1562063075099

当前时间超过过期时间,则会报错。

6.4.4 自定义claims

我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。

创建测试类,并设置测试方法:

创建token:

    @Test
    public void createJWT() {
        long l1 = System.currentTimeMillis(); //当前时间戳
        long l2 = l1 + 20000; //过期时间错
        JwtBuilder builder = Jwts.builder()
                .setId("12356") //设置唯一编号
                .setSubject("password123")  //主题
                .setIssuedAt(new Date()) //签发时间
//                .setExpiration(new Date(l2))
                .claim("role", "ADMIN")
                .signWith(SignatureAlgorithm.HS256, "lxs"); //签名,采用HS256算法加密,秘钥
        //自定义载荷
        Map<String, Object> map = new HashMap<>();
        map.put("myaddress", "cn");
        map.put("mycity", "hangzhou");
        builder.addClaims(map);

        System.out.println(builder.compact());
    }

运行打印效果:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiLllK_kuIDnmoTmoIfor4YiLCJpc3MiOiLpooHlj5HogIUiLCJzdWIiOiLkuLvpopgiLCJleHAiOjE2MDIzMjIwMDEsIm15Y2l0eSI6ImJlaWppbmciLCJteWFkZHJlc3MiOiJjbiJ9.c7UxJaQVxulwVqvIZLP_8RxSTbKOyixcU6mgMFaYrw0

解析TOKEN:

    @Test
    public void testParseJwt() {
        String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjM1NiIsInN1YiI6InBhc3N3b3JkMTIzIiwiaWF0IjoxNjA1NzY4NzQ4LCJyb2xlIjoiQURNSU4iLCJteWNpdHkiOiJoYW5nemhvdSIsIm15YWRkcmVzcyI6ImNuIn0.598OCjR-ItxRat7_VvWklUbIgGehJuEcXu21EEub8vs";
        Claims claims = Jwts.parser()
                .setSigningKey("lxs")
                .parseClaimsJws(jwt)
                .getBody();
        System.out.println(claims);

        System.out.println(claims.get("role"));
        System.out.println(claims.getSubject());
    }

运行效果:

1562063412350

6.5 鉴权处理

6.5.1 思路分析

1562069596308

1.用户通过访问微服务网关调用微服务,同时携带头文件信息
2.在微服务网关这里进行拦截,拦截后获取用户要访问的路径
3.识别用户访问的路径是否需要登录,如果需要,识别用户的身份是否能访问该路径[这里可以基于数据库设计一套权限]
4.如果需要权限访问,用户已经登录,则放行
5.如果需要权限访问,且用户未登录,则提示用户需要登录
6.用户通过网关访问用户微服务,进行登录验证
7.验证通过后,用户微服务会颁发一个令牌给网关,网关会将用户信息封装到头文件中,并响应用户
8.用户下次访问,携带头文件中的令牌信息即可识别是否登录

6.5.2用户登录签发TOKEN

(1)生成令牌工具类

在gateway中创建类JwtUtil,主要辅助生成Jwt令牌信息,代码如下:

gateway/src/main/java/com/lxs/clound/util/JwtUtil.java

package com.lxs.clound.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;


public class JwtUtil {
   
    //有效期为
    public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000  一个小时

    //Jwt令牌信息
    public static final String JWT_KEY = "lxs";

    /**
     * 生成令牌
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
   
        //指定算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        //当前系统时间
        long nowMillis = System.currentTimeMillis();
        //令牌签发时间
        Date now = new Date(nowMillis);

        //如果令牌有效期为null,则默认设置有效期1小时
        if (ttlMillis == null) {
   
            ttlMillis = JwtUtil.JWT_TTL;
        }

        //令牌过期时间设置
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);

        //生成秘钥
        SecretKey secretKey = generalKey();

        //封装Jwt令牌信息
        JwtBuilder builder = Jwts.builder()
                .setId(id)                    //唯一的ID
                .setSubject(subject)          // 主题  可以是JSON数据
                .setIssuer("admin")          // 签发者
                .setIssuedAt(now)             // 签发时间
                .signWith(signatureAlgorithm, secretKey) // 签名算法以及密匙
                .setExpiration(expDate);      // 设置过期时间
        return builder.compact();
    }

    /**
     * 生成加密 secretKey
     *
     * @return
     */
    public static SecretKey generalKey() {
   
        byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes());
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }


    /**
     * 解析令牌数据
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
   
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }

    public static void main(String[] args) {
   
        String jwt = JwtUtil.createJWT("weiyibiaoshi", "aaaaaa", null);
        System.out.println(jwt);
        try {
   
            Claims claims = JwtUtil.parseJWT(jwt);
            System.out.println(claims);
        } catch (Exception e) {
   
            e.printStackTrace();
        }


    }
}

gateway/src/main/java/com/lxs/clound/util/BCrypt.java

用于BCrypt加密解密的工具类(拷贝即可)

(2) 通过Feign调用用户微服务,实现具体登录

gateway/src/main/java/com/lxs/clound/client/UserClient.java

package com.lxs.clound.client;

import com.lxs.legou.security.api.UserApi;
import com.lxs.legou.security.po.Role;
import com.lxs.legou.security.po.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@FeignClient(name = "security-service", fallback = UserClient.UserClientFallback.class)
public interface UserClient extends UserApi {

    @Component
    @RequestMapping("/fallback") //这个可以避免容器中requestMapping重复
    class UserClientFallback implements UserClient {

        private static final Logger LOGGER = LoggerFactory.getLogger(UserClientFallback.class);

        @Override
        public User getByUserName(String userName) {
            LOGGER.info("异常发生,进入fallback方法");
            return null;
        }

        @Override
        public List<Role> selectRolesByUserId(Long id) {
            LOGGER.info("异常发生,进入fallback方法");
            return null;
        }
    }

}

引入legou-security-instance依赖

        <dependency>
            <groupId>com.lxs</groupId>
            <artifactId>legou-security-instance</artifactId>
            <version>${project.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-jdbc</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

(3) 用户登录成功 则 签发TOKEN,修改登录的方法:

1562070521132

代码如下:

package com.lxs.clound.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.lxs.clound.client.UserClient;
import com.lxs.clound.util.BCrypt;
import com.lxs.clound.util.JwtUtil;
import com.lxs.legou.security.po.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.util.*;

@RequestMapping("/user")
@RestController
@CrossOrigin
public class UserController {
   

    @Autowired
    private UserClient userClient;

    @Autowired
    private ObjectMapper om;

    @RequestMapping("/login")
    public ResponseEntity login(String username,String password) throws Exception {
   
        //1.从数据库中查询用户名对应的用户的对象
        User user = userClient.getByUserName(username);
        if (user == null) {
   
            //2.判断用户是否为空 为空返回数据
            return new ResponseEntity("用户名密码错误", HttpStatus.UNAUTHORIZED);
        }

        //3如果不为空 判断 密码是否正确 正确则登录成功
        if(BCrypt.checkpw(password, user.getPassword())){
   
            //成功
            Map<String,Object> info = new HashMap<String,Object>();
            info.put("role","USER");
            info.put("success","SUCCESS");
            info.put("username",username);

            //1.生成令牌
            String jwt = JwtUtil.createJWT(UUID.randomUUID().toString(), om.writeValueAsString(info), null);

            return ResponseEntity.ok(jwt);
        }else{
   
            //失败
            return new ResponseEntity("用户名密码错误", HttpStatus.UNAUTHORIZED);
        }

    }
}

执行测试后的效果

6.5.3 自定义全局过滤器

创建 过滤器类,如图所示:

1562116467343

AuthorizeFilter代码如下:

@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
   

    private static final String AUTHORIZE_TOKEN = "Authorization";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
   
        //1: 如果访问登录等放行
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        if (request.getURI().getPath().startsWith("/user/login")) {
   
            return chain.filter(exchange);
        }

        //2: 获得令牌
        String token = request.getHeaders().getFirst(AUTHORIZE_TOKEN);
        if (StringUtils.isEmpty(token)) {
   
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //3:校验令牌
        try {
   
            Claims claims = JwtUtil.parseJWT(token);

            //对解析的用户的角色判断,不同角色访问相应角色的微服务
        } catch (Exception e) {
   
            e.printStackTrace();
            //解析失败
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            response.setComplete();
        }

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
   
        //返回值越小,在过滤器链中越有限执行
        return 0;
    }
}

测试访问http://localhost:8062/user/login?username=admin&password=admin,效果如下:

1562117166529

测试访问http://localhost:8062/api/item/brand/list,效果如下:

1562117230885

参考官方手册:

https://cloud.spring.io/spring-cloud-gateway/spring-cloud-gateway.html#_stripprefix_gatewayfilter_factory

目录
相关文章
|
17天前
|
安全 NoSQL Java
java电商项目(十)
本文介绍了电商系统中订单结算和下单流程的实现。主要包括: 1. **订单结页**: - **收件地址分析**:用户从购物车页面跳转到订单结算页,加载用户收件地址。地址信息存储在 `address_` 表中。 - **实现用户收件地址查询**:通过用户登录名查询收件地址,涉及实体类、DAO、Service 和 Controller 的实现。 2. **下单**: - **业务分析**:下单时创建订单数据,包括订单表 `order_` 和订单明细表 `order_item_`,同时修改商品库存、增加用户积分并删除购物车数据。
29 3
|
17天前
|
消息中间件 安全 Java
java电商项目(十一)
本文接续前几个文章的项目进行讲解!
26 1
|
17天前
|
缓存 NoSQL Java
java电商项目(十二)
本文接续前几个文章的项目进行讲解
84 1
|
17天前
|
存储 NoSQL Java
java电商项目(九)
本文介绍了购物车功能的实现过程,包括用户登录后将商品添加至购物车、购物车列表展示及微服务之间的认证机制。具体步骤如下: 1. **购物车功能**: - 用户选择商品并点击“加入购物车”,系统将商品信息存储到Redis中。 2. **微服务之间认证**: - **传递管理员令牌**:在授权中心微服务调用用户微服务时,生成管理员令牌并通过Header传递。 - **传递当前用户令牌**:用户登录后,通过Feign拦截器将用户令牌传递到其他微服务。 - **获取用户数据**:通过`SecurityContextHolder`获取用户信息,并使用公钥解密令牌以验证用户
25 1
|
17天前
|
canal 监控 JavaScript
java电商项目(六)
Thymeleaf 是一个类似于 FreeMarker 的模板引擎,能够完全替代 JSP。它支持动静结合,无网络时显示静态内容,有网络时用后台数据替换静态内容,并且与 Spring Boot 完美整合。本文介绍了如何使用 Thymeleaf 生成商品详情页的静态页面。具体步骤包括创建商品静态化微服务、配置项目依赖、创建 Controller 和 Service、生成静态页面、模板填充、静态资源过滤以及启动测试。此外,还介绍了如何通过 Canal 监听商品数据变化,自动触发静态页面的生成或删除。
26 1
|
17天前
|
SQL 自然语言处理 Java
java电商项目(五)
本文介绍了如何构建一个基于Elasticsearch的商品搜索微服务,主要包括以下几个部分: 1. **数据导入ES**: - 搭建搜索工程,创建`legou-search`项目,提供搜索服务和索引数据更新操作。 - 配置`pom.xml`文件,引入必要的依赖。 - 创建启动器和配置文件,配置Elasticsearch连接信息。 - 分析索引库数据格式,确定需要存储的数据字段。 - 实现商品微服务接口,调用其他微服务获取所需数据。 - 创建索引并导入数据,将SPU和SKU数据转换为索引库中的Goods对象。 2. **商品搜索*
24 1
|
17天前
|
canal NoSQL 关系型数据库
java电商项目(四)
本章介绍了如何通过Lua、OpenResty、Nginx限流及Canal的使用,实现电商门户首页的高并发解决方案。主要内容包括: 1. **商城门户搭建**:使用Vue和iView构建前端门户项目,介绍如何展示商品分类和广告数据,并通过Redis缓存提升访问速度。 2. **Lua基础**:介绍Lua的基本概念、特性、应用场景及安装步骤,并通过示例展示了Lua的基本语法和常用功能。 3. **OpenResty介绍**:详细说明OpenResty的特性和优势,包括如何安装OpenResty和配置Nginx,以及如何使用Lua脚本操作Nginx缓存和数据库。
19 1
|
18天前
|
存储 前端开发 JavaScript
java电商项目(二)
本文档详细介绍了商品分类和规格参数的实现过程。商品分类分为三级管理,主要用于首页商品导航和后台商品管理,采用树状结构存储。规格参数则用于描述商品的具体属性,包括SPU和SKU的定义,规格参数与分类绑定,支持搜索过滤。文档涵盖了表结构设计、实体类、持久层、业务层、控制层的实现,并提供了前端组件的示例代码,确保前后端无缝对接。
30 1
|
17天前
|
存储 安全 Java
java电商项目(八)
OAuth 2.0 是一种开放标准,允许用户授权第三方应用访问其在某一网站上的私密资源,而无需提供用户名和密码。它通过提供一个令牌(token)来实现这一功能。OAuth 2.0 主要包括四种授权模式:授权码模式、简化模式、密码模式和客户端模式。授权码模式是最常用的一种,适用于第三方平台登录功能。Spring Security OAuth 2.0 提供了强大的工具来实现授权服务器和资源服务器的集成,支持多种授权模式和令牌存储方式,如内存、数据库、JWT 和
39 0
|
17天前
|
前端开发 算法 JavaScript
java电商项目(三)
本文介绍了乐购商城的商品数据分析和管理功能。首先解释了SPU(标准产品单位)和SKU(库存量单位)的概念,以及它们在商品管理和销售中的作用。接着详细分析了SPU、SPU详情和SKU三个表的结构及其关系。文章还介绍了商品管理的需求分析、实现思路和后台代码,包括实体类、持久层、业务层和控制层的实现。最后,文章讲解了前端组件的设计和实现,包括列表组件、添加修改组件、商品描述、通用规格、SKU特有规格和SKU列表的处理。通过这些内容,读者可以全面了解乐购商城的商品管理和数据分析系统。
29 0
下一篇
无影云桌面