品達通用權限系統-五


19. 網關服務開發

pd-gateway作為通用權限系統的網關服務,前端的http請求首先需要經過網關服務處理,再通過網關服務的路由功能轉發到權限服務或者其他微服務進行業務處理。我們可以在網關服務進行統一的jwt令牌解析、鑒權相關操作。

19.1 配置文件

19.1.1 bootstrap.yml

由於我們當前使用的是Nacos作為整個項目的配置中心,所以Spring Boot的大部分配置文件都在Nacos中進行統一配置,我們的項目中只需要按照Spring Boot的要求在resources目錄下提供bootstrap.yml配置文件即可,文件內容如下:

pinda:
  # docker部署時,需要指定, 表示運行該服務的宿主機IP
  local-ip: ${LOCAL_IP:${spring.cloud.client.ip-address}}   
  nacos:
    ip: ${NACOS_IP:@pom.nacos.ip@}
    port: ${NACOS_PORT:@pom.nacos.port@}
    namespace: ${NACOS_ID:@pom.nacos.namespace@}

spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: @project.artifactId@ #pd-gateway
  profiles:
    active: @pom.profile.name@ #dev
  cloud:
    nacos:
      config:
        server-addr: ${pinda.nacos.ip}:${pinda.nacos.port}
        file-extension: yml
        namespace: ${pinda.nacos.namespace}
        shared-dataids: common.yml,redis.yml,mysql.yml
        refreshable-dataids: common.yml
        enabled: true
      discovery:
        server-addr: ${pinda.nacos.ip}:${pinda.nacos.port}
        namespace: ${pinda.nacos.namespace}
        metadata:
          management.context-path: ${server.servlet.context-path:}${spring.mvc.servlet.path:}${management.endpoints.web.base-path:}
          #http://localhost:8760/api/gate/actuator

19.1.2 logback-spring.xml

由於pd-gateway已經添加了pd-tools-log模塊的依賴,所以可以在項目中使用logback記錄日志信息。在resources目錄下提供logback-spring.xml配置文件,Spring Boot默認就可以加載到,文件內容如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <include resource="com/itheima/pinda/log/logback/pinda-defaults.xml"/>

    <springProfile name="test,docker,prod">
        <logger name="com.itheima.pinda.zuul" additivity="true" level="INFO">
            <appender-ref ref="ASYNC_CONTROLLER_APPENDER"/>
        </logger>
    </springProfile>

    <springProfile name="dev">
        <logger name="com.itheima.pinda.zuul" additivity="true" level="INFO">
            <appender-ref ref="CONTROLLER_APPENDER"/>
        </logger>
    </springProfile>
</configuration>

19.1.3 j2cache配置文件

在當前pd-gateway項目中會使用到j2cache來操作緩存,在Nacos配置中心的redis.yml中已經配置了j2cache的相關配置:

j2cache:
  #  config-location: /j2cache.properties
  open-spring-cache: true
  cache-clean-mode: passive
  allow-null-values: true
  redis-client: lettuce
  l2-cache-open: true
  # l2-cache-open: false     # 關閉二級緩存
  broadcast: net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
  #  broadcast: jgroups       # 關閉二級緩存
  L1:
    provider_class: caffeine
  L2:
    provider_class: net.oschina.j2cache.cache.support.redis.SpringRedisProvider
    config_section: lettuce
  sync_ttl_to_redis: true
  default_cache_null_object: false
  serialization: fst
caffeine:
  properties: /j2cache/caffeine.properties   # 這個配置文件需要放在項目中
lettuce:
  mode: single
  namespace:
  storage: generic
  channel: j2cache
  scheme: redis
  hosts: ${pinda.redis.ip}:${pinda.redis.port}
  password: ${pinda.redis.password}
  database: ${pinda.redis.database}
  sentinelMasterId:
  maxTotal: 100
  maxIdle: 10
  minIdle: 10
  timeout: 10000

通過上面的配置可以看到,還需要在項目中提供/j2cache/caffeine.properties,文件內容如下:

#########################################
# Caffeine configuration
# \u6682\u65F6\u6CA1\u7528
# [name] = size, xxxx[s|m|h|d]
#########################################
default=2000, 2h
resource=2000, 1h

19.1.4 密鑰文件

JWT簽名算法中,一般有兩個選擇:HS256和RS256。

HS256 (帶有 SHA-256 的 HMAC )是一種對稱加密算法, 雙方之間僅共享一個密鑰。由於使用相同的密鑰生成簽名和驗證簽名, 因此必須注意確保密鑰不被泄密。

RS256 (采用SHA-256 的 RSA 簽名) 是一種非對稱加密算法, 它使用公共/私鑰對: JWT的提供方采用私鑰生成簽名, JWT 的使用方獲取公鑰以驗證簽名。

本項目中使用RS256非對稱加密算法進行簽名,這就需要使用RSA生成一對公鑰和私鑰。在授課資料中已經提供了一對公鑰和私鑰,其中pub.key為公鑰,pri.key為私鑰。

前面我們已經提到,在當前網關服務中我們需要對客戶端請求中攜帶的jwt token進行解析,只需要公鑰就可以。將授課資料中的pub.key文件復制到pd-gateway項目的resources/client下。

19.2 啟動類

package com.itheima.pinda;

import com.itheima.pinda.auth.client.EnableAuthClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
 *網關啟動類
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients({"com.itheima.pinda"})
@EnableZuulProxy//開啟網關代理
@EnableAuthClient//開啟授權客戶端,開啟后就可以使用pd-tools-jwt提供的工具類進行jwt token解析了
public class ZuulServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulServerApplication.class, args);
    }
}

19.3 配置類

package com.itheima.pinda.zuul.config;

import com.itheima.pinda.common.config.BaseConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
 * 解決跨域問題
 */
@Configuration
public class ZuulConfiguration extends BaseConfig {
    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource source = 
            new UrlBasedCorsConfigurationSource();
        final org.springframework.web.cors.CorsConfiguration config = 
            new org.springframework.web.cors.CorsConfiguration();
        // 允許cookies跨域
        config.setAllowCredentials(true);
        // #允許向該服務器提交請求的URI,*表示全部允許
        config.addAllowedOrigin("*");
        // #允許訪問的頭信息,*表示全部
        config.addAllowedHeader("*");
        // 預檢請求的緩存時間(秒),即在這個時間段里,對於相同的跨域請求不會再預檢了
        config.setMaxAge(18000L);
        // 允許提交請求的方法,*表示全部允許
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("HEAD");
        // 允許Get的請求類型
        config.addAllowedMethod("GET");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

19.4 API接口和熔斷器

在網關服務中會通過Feign來調用權限服務獲取相關信息,所以需要定義API接口和對應的熔斷器類

package com.itheima.pinda.zuul.api;

import com.itheima.pinda.authority.dto.auth.ResourceQueryDTO;
import com.itheima.pinda.authority.entity.auth.Resource;
import com.itheima.pinda.base.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;

@FeignClient(name = "${pinda.feign.authority-server:pd-auth-server}", 
             fallback = ResourceApiFallback.class)
public interface ResourceApi {
    //獲取所有需要鑒權的資源
    @GetMapping("/resource/list")
    public R<List> list();

    //查詢當前登錄用戶擁有的資源權限
    @GetMapping("/resource")
    public R<List<Resource>> visible(ResourceQueryDTO resource);
}
package com.itheima.pinda.zuul.api;

import com.itheima.pinda.authority.dto.auth.ResourceQueryDTO;
import com.itheima.pinda.authority.entity.auth.Resource;
import com.itheima.pinda.base.R;
import org.springframework.stereotype.Component;
import java.util.List;
/**
 * 資源API熔斷
 */
@Component
public class ResourceApiFallback implements ResourceApi {
    public R<List> list() {
        return null;
    }

    public R<List<Resource>> visible(ResourceQueryDTO resource) {
        return null;
    }
}

19.5 過濾器

在網關服務中我們需要通過過濾器來實現jwt token解析鑒權相關處理。

19.5.1 BaseFilter

BaseFilter作為基礎過濾器,統一抽取一些公共屬性和方法。

package com.itheima.pinda.zuul.filter;
import javax.servlet.http.HttpServletRequest;
import com.itheima.pinda.base.R;
import com.itheima.pinda.common.adapter.IgnoreTokenConfig;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
/**
 * 基礎 網關過濾器
 */
@Slf4j
public abstract class BaseFilter extends ZuulFilter {
    @Value("${server.servlet.context-path}")
    protected String zuulPrefix;

    /**
     * 判斷當前請求uri是否需要忽略
     */
    protected boolean isIgnoreToken() {
        HttpServletRequest request = 
            RequestContext.getCurrentContext().getRequest();
        String uri = request.getRequestURI();
        uri = StrUtil.subSuf(uri, zuulPrefix.length());
        uri = StrUtil.subSuf(uri, uri.indexOf("/", 1));
        boolean ignoreToken = IgnoreTokenConfig.isIgnoreToken(uri);
        return ignoreToken;
    }

    /**
     * 網關拋異常
     * @param errMsg
     * @param errCode
     * @param httpStatusCode
     */
    protected void errorResponse(String errMsg, int errCode, int httpStatusCode) {
        R tokenError = R.fail(errCode, errMsg);
        RequestContext ctx = RequestContext.getCurrentContext();
        // 返回錯誤碼
        ctx.setResponseStatusCode(httpStatusCode);
        ctx.addZuulResponseHeader(
            "Content-Type", "application/json;charset=UTF-8");
        if (ctx.getResponseBody() == null) {
            // 返回錯誤內容
            ctx.setResponseBody(tokenError.toString());
            // 過濾該請求,不對其進行路由
            ctx.setSendZuulResponse(false);
        }
    }
}

19.5.2 TokenContextFilter

TokenContextFilter過濾器主要作用就是解析請求頭中的jwt token並將解析出的用戶信息放入zuul的header中供后面的程序使用。

package com.itheima.pinda.zuul.filter;
import javax.servlet.http.HttpServletRequest;
import com.itheima.pinda.auth.client.properties.AuthClientProperties;
import com.itheima.pinda.auth.client.utils.JwtTokenClientUtils;
import com.itheima.pinda.auth.utils.JwtUserInfo;
import com.itheima.pinda.base.R;
import com.itheima.pinda.context.BaseContextConstants;
import com.itheima.pinda.exception.BizException;
import com.itheima.pinda.utils.StrHelper;
import com.netflix.zuul.context.RequestContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
/**
 * 解析token中的用戶信息 過濾器
 */
@Component
public class TokenContextFilter extends BaseFilter {
    @Autowired
    private AuthClientProperties authClientProperties;
    @Autowired
    private JwtTokenClientUtils jwtTokenClientUtils;

    @Override
    public String filterType() {
        // 前置過濾器
        return PRE_TYPE;
    }

    /**
     * filterOrder:通過int值來定義過濾器的執行順序,數字越大,優先級越低
     */
    @Override
    public int filterOrder() {
        /*
         一定要在
         org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter
         過濾器之后執行,因為這個過濾器做了路由,而我們需要這個路由信息來鑒權
         這個過濾器會將我們鑒權需要的信息放置在請求上下文中
         */
        return FilterConstants.PRE_DECORATION_FILTER_ORDER + 1;
    }

    /**
     * 返回一個boolean類型來判斷該過濾器是否要執行
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        // 不進行攔截的地址
        if (isIgnoreToken()) {
            return null;
        }

        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        //獲取token, 解析,然后將信息放入 header
        //1, 獲取token
        String userToken = 
            request.getHeader(authClientProperties.getUser().getHeaderName());

        //2, 解析token
        JwtUserInfo userInfo = null;

        try {
            userInfo = jwtTokenClientUtils.getUserInfo(userToken);
        } catch (BizException e) {
            errorResponse(e.getMessage(), e.getCode(), 200);
            return null;
        } catch (Exception e) {
            errorResponse("解析token出錯", R.FAIL_CODE, 200);
            return null;
        }

        //3, 將信息放入header
        if (userInfo != null) {
            addHeader(ctx, BaseContextConstants.JWT_KEY_ACCOUNT, 
                      userInfo.getAccount());
            addHeader(ctx, BaseContextConstants.JWT_KEY_USER_ID, 
                      userInfo.getUserId());
            addHeader(ctx, BaseContextConstants.JWT_KEY_NAME, 
                      userInfo.getName());
            addHeader(ctx, BaseContextConstants.JWT_KEY_ORG_ID, 
                      userInfo.getOrgId());
            addHeader(ctx, BaseContextConstants.JWT_KEY_STATION_ID, 
                      userInfo.getStationId());
        }
        return null;
    }

    private void addHeader(RequestContext ctx, String name, Object value) {
        if (StringUtils.isEmpty(value)) {
            return;
        }
        ctx.addZuulRequestHeader(name, StrHelper.encode(value.toString()));
    }
}

19.5.3 AccessFilter

AccessFilter過濾器主要進行的是鑒權相關處理。具體的處理邏輯如下:

第1步:判斷當前請求uri是否需要忽略
第2步:獲取當前請求的請求方式和uri,拼接成GET/user/page這種形式,稱為權限標識符
第3步:從緩存中獲取所有需要進行鑒權的資源(同樣是由資源表的method字段值+url字段值拼接成),如果沒有獲取到則通過Feign調用權限服務獲取並放入緩存中
第4步:判斷這些資源是否包含當前請求的權限標識符,如果不包含當前請求的權限標識符,則返回未經授權錯誤提示
第5步:如果包含當前的權限標識符,則從zuul header中取出用戶id,根據用戶id取出緩存中的用戶擁有的權限,如果沒有取到則通過Feign調用權限服務獲取並放入緩存,判斷用戶擁有的權限是否包含當前請求的權限標識符
第6步:如果用戶擁有的權限包含當前請求的權限標識符則說明當前用戶擁有權限,直接放行
第7步:如果用戶擁有的權限不包含當前請求的權限標識符則說明當前用戶沒有權限,返回未經授權錯誤提示
package com.itheima.pinda.zuul.filter;

import cn.hutool.core.util.StrUtil;
import com.itheima.pinda.authority.dto.auth.ResourceQueryDTO;
import com.itheima.pinda.authority.entity.auth.Resource;
import com.itheima.pinda.base.R;
import com.itheima.pinda.common.constant.CacheKey;
import com.itheima.pinda.context.BaseContextConstants;
import com.itheima.pinda.exception.code.ExceptionCode;
import com.itheima.pinda.zuul.api.ResourceApi;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import net.oschina.j2cache.CacheChannel;
import net.oschina.j2cache.CacheObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
/**
 * 權限驗證過濾器
 */
@Component
@Slf4j
public class AccessFilter extends BaseFilter {
    @Autowired
    private CacheChannel cacheChannel;
    @Autowired
    private ResourceApi resourceApi;
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return FilterConstants.PRE_DECORATION_FILTER_ORDER + 10;
    }

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

    /**
     * 驗證當前用戶是否擁有某個URI的訪問權限
     */
    @Override
    public Object run() {
        // 不進行攔截的地址
        if (isIgnoreToken()) {
            return null;
        }

        RequestContext requestContext = RequestContext.getCurrentContext();
        String requestURI = requestContext.getRequest().getRequestURI();
        requestURI = StrUtil.subSuf(requestURI, zuulPrefix.length());
        requestURI = StrUtil.subSuf(requestURI, requestURI.indexOf("/", 1));
        String method = requestContext.getRequest().getMethod();
        String permission = method + requestURI;

        //從緩存中獲取所有需要進行鑒權的資源
        CacheObject resourceNeed2AuthObject = 
            cacheChannel.get(CacheKey.RESOURCE, 
                             CacheKey.RESOURCE_NEED_TO_CHECK);
        List<String> resourceNeed2Auth = 
            (List<String>) resourceNeed2AuthObject.getValue();
        if(resourceNeed2Auth == null){
            resourceNeed2Auth = resourceApi.list().getData();
            if(resourceNeed2Auth != null){
                cacheChannel.set(CacheKey.RESOURCE,
                                 CacheKey.RESOURCE_NEED_TO_CHECK,
                                 resourceNeed2Auth);
            }
        }
        if(resourceNeed2Auth != null){
            long count = resourceNeed2Auth.stream().filter((String r) -> {
                return permission.startsWith(r);
            }).count();
            if(count == 0){
                //未知請求
                errorResponse(ExceptionCode.UNAUTHORIZED.getMsg(),
                          ExceptionCode.UNAUTHORIZED.getCode(), 200);
                return null;
            }
        }

        String userId = requestContext.getZuulRequestHeaders().
            				get(BaseContextConstants.JWT_KEY_USER_ID);
        CacheObject cacheObject = cacheChannel.get(CacheKey.USER_RESOURCE, userId);
        List<String> userResource = (List<String>) cacheObject.getValue();
        // 如果從緩存獲取不到當前用戶的資源權限,需要查詢數據庫獲取,然后再放入緩存
        if(userResource == null){
            ResourceQueryDTO resourceQueryDTO = new ResourceQueryDTO();
            resourceQueryDTO.setUserId(new Long(userId));
            //通過Feign調用服務,查詢當前用戶擁有的權限
            R<List<Resource>> result = resourceApi.visible(resourceQueryDTO);
            if(result.getData() != null){
                List<Resource> userResourceList = result.getData();
                userResource = userResourceList.stream().map((Resource r) -> {
                    return r.getMethod() + r.getUrl();
                }).collect(Collectors.toList());
                cacheChannel.set(CacheKey.USER_RESOURCE,userId,userResource);
            }
        }

        long count = userResource.stream().filter((String r) -> {
            return permission.startsWith(r);
        }).count();
        
        if(count > 0){
            //有訪問權限
            return null;
        }else{
            log.warn("用戶{}沒有訪問{}資源的權限",userId,method + requestURI);
            errorResponse(ExceptionCode.UNAUTHORIZED.getMsg(),
                          ExceptionCode.UNAUTHORIZED.getCode(), 200);
        }
        return null;
    }
}

20. 通用權限系統企業應用指南

20.1 新項目開發

如果是新項目開發,可以在品達通用權限系統的基礎上進行相關的業務開發,其實就是將通用權限系統當做開發腳手架在此基礎之上快速開始業務開發。

本小節通過一個商品服務的案例來講解如何基於品達通用權限系統進行新業務的開發。

20.1.1 數據庫環境搭建

創建數據庫pd_goods並創建表pd_goods_info,可以使用資料中提供的建表腳本pd_goods_info.sql進行創建。

20.1.2 后端業務功能開發

20.1.2.1 創建工程

在品達通用權限系統基礎上創建商品服務相關模塊,如下圖:

pd-goods              #商品服務父工程
├── pd-goods-entity	  #實體
├── pd-goods-server	  #服務

20.1.2.2 pd-goods-entity開發

第一步:配置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>pd-goods</artifactId>
        <groupId>com.itheima</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>pd-goods-entity</artifactId>
    <description>接口服務實體模塊</description>
    <dependencies>
        <dependency>
            <groupId>com.itheima</groupId>
            <artifactId>pd-tools-common</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
        </dependency>
    </dependencies>
</project>

第二步:創建商品實體類

package com.itheima.pinda.goods.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import com.itheima.pinda.base.entity.Entity;
import lombok.*;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("pd_goods_info")
public class GoodsInfo extends Entity<Long> {

    private static final long serialVersionUID = 1L;

    /**
     * 商品編碼
     */
    private String code;

    /**
     * 商品名稱
     */
    private String name;

    /**
     * 國條碼
     */
    private String barCode;

    /**
     * 品牌表id
     */
    private Integer brandId;

    /**
     * 一級分類id
     */
    private Integer oneCategoryId;

    /**
     * 二級分類id
     */
    private Integer twoCategoryId;

    /**
     * 三級分類id
     */
    private Integer threeCategoryId;

    /**
     * 商品的供應商id
     */
    private Integer supplierId;

    /**
     * 商品售價價格
     */
    private BigDecimal price;

    /**
     * 商品加權平均成本
     */
    private BigDecimal averageCost;

    /**
     * 上下架狀態:0下架,1上架
     */
    private boolean publishStatus;

    /**
     * 審核狀態: 0未審核,1已審核
     */
    private boolean auditStatus;

    /**
     * 商品重量
     */
    private Float weight;

    /**
     * 商品長度
     */
    private Float length;

    /**
     * 商品重量
     */
    private Float height;

    /**
     * 商品寬度
     */
    private Float width;

    /**
     * 顏色
     */
    private String color;

    /**
     * 生產日期
     */
    private LocalDateTime productionDate;

    /**
     * 商品有效期
     */
    private Integer shelfLife;

    /**
     * 商品描述
     */
    private String descript;

}

第三步:創建商品操作對應的多個DTO類

package com.itheima.pinda.goods.dto;

import com.itheima.pinda.goods.entity.GoodsInfo;
import lombok.*;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class GoodsInfoPageDTO extends GoodsInfo {
    private LocalDateTime startCreateTime;
    private LocalDateTime endCreateTime;
}
package com.itheima.pinda.goods.dto;

import com.itheima.pinda.goods.entity.GoodsInfo;

public class GoodsInfoSaveDTO extends GoodsInfo {
}
package com.itheima.pinda.goods.dto;

import com.itheima.pinda.goods.entity.GoodsInfo;

public class GoodsInfoUpdateDTO extends GoodsInfo {
}

20.1.2.3 pd-goods-server開發

第一步:配置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>pd-goods</artifactId>
        <groupId>com.itheima</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>pd-goods-server</artifactId>
    <description>接口服務啟動模塊</description>
    <dependencies>
        <dependency>
            <groupId>com.itheima</groupId>
            <artifactId>pd-tools-log</artifactId>
        </dependency>
        <dependency>
            <groupId>com.itheima</groupId>
            <artifactId>pd-tools-swagger2</artifactId>
        </dependency>
        <dependency>
            <groupId>com.itheima</groupId>
            <artifactId>pd-tools-validator</artifactId>
        </dependency>
        <dependency>
            <groupId>com.itheima</groupId>
            <artifactId>pd-tools-xss</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>fastjson</artifactId>
                    <groupId>com.alibaba</groupId>
                </exclusion>
                <exclusion>
                    <groupId>com.google.guava</groupId>
                    <artifactId>guava</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.google.guava</groupId>
                    <artifactId>guava</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.google.guava</groupId>
                    <artifactId>guava</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>${asm.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.tomcat.embed</groupId>
                    <artifactId>tomcat-embed-websocket</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-json</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.itheima</groupId>
            <artifactId>pd-tools-databases</artifactId>
        </dependency>
        <dependency>
            <groupId>com.itheima</groupId>
            <artifactId>pd-tools-dozer</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-context</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.itheima</groupId>
            <artifactId>pd-goods-entity</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

第二步:導入資料中提供的配置文件

第三步:在配置中心Nacos中創建pd-goods-server.yml

配置文件內容如下:

# 在這里配置 權限服務 所有環境都能使用的配置
pinda:
  mysql:
    database: pd_goods
  swagger:
    enabled: true
    docket:
      core:
        title: 核心模塊
        base-package: com.itheima.pinda.goods.controller

server:
  port: 8767

第四步:編寫啟動類

package com.itheima.pinda;

import com.itheima.pinda.validator.config.EnableFormValidator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.net.InetAddress;
import java.net.UnknownHostException;

@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableFeignClients(value = {
        "com.itheima.pinda",
})
@EnableTransactionManagement
@Slf4j
@EnableFormValidator
public class GoodsServerApplication {
    public static void main(String[] args) throws UnknownHostException {
        ConfigurableApplicationContext application = SpringApplication.run(GoodsServerApplication.class, args);
        Environment env = application.getEnvironment();
        log.info("\n----------------------------------------------------------\n\t" +
                        "應用 '{}' 運行成功! 訪問連接:\n\t" +
                        "Swagger文檔: \t\thttp://{}:{}/doc.html\n\t" +
                        "----------------------------------------------------------",
                env.getProperty("spring.application.name"),
                InetAddress.getLocalHost().getHostAddress(),
                env.getProperty("server.port"));

    }
}

第五步:導入資料中提供的配置類

第六步:創建Mapper接口

package com.itheima.pinda.goods.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.pinda.goods.entity.GoodsInfo;
import org.springframework.stereotype.Repository;

/**
 * Mapper 接口
 */
@Repository
public interface GoodsInfoMapper extends BaseMapper<GoodsInfo> {
}

第七步:創建Service接口和實現類

package com.itheima.pinda.goods.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.pinda.goods.entity.GoodsInfo;

public interface GoodsInfoService extends IService<GoodsInfo> {
}
package com.itheima.pinda.goods.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.pinda.goods.dao.GoodsInfoMapper;
import com.itheima.pinda.goods.entity.GoodsInfo;
import com.itheima.pinda.goods.service.GoodsInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class GoodsInfoServiceImpl extends ServiceImpl<GoodsInfoMapper, GoodsInfo> implements GoodsInfoService {
}

第八步:創建Controller

package com.itheima.pinda.goods.controller;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.itheima.pinda.base.BaseController;
import com.itheima.pinda.base.R;
import com.itheima.pinda.base.entity.SuperEntity;
import com.itheima.pinda.database.mybatis.conditions.Wraps;
import com.itheima.pinda.database.mybatis.conditions.query.LbqWrapper;
import com.itheima.pinda.dozer.DozerUtils;
import com.itheima.pinda.goods.dto.GoodsInfoPageDTO;
import com.itheima.pinda.goods.dto.GoodsInfoSaveDTO;
import com.itheima.pinda.goods.dto.GoodsInfoUpdateDTO;
import com.itheima.pinda.goods.entity.GoodsInfo;
import com.itheima.pinda.goods.service.GoodsInfoService;
import com.itheima.pinda.log.annotation.SysLog;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@Slf4j
@Validated
@RestController
@RequestMapping("/goodsInfo")
@Api(value = "GoodsInfo", tags = "商品信息")
public class GoodsInfoController extends BaseController {
    @Autowired
    private DozerUtils dozer;
    @Autowired
    private GoodsInfoService goodsInfoService;

    /**
     * 分頁查詢商品信息
     *
     * @param data 分頁查詢對象
     * @return 查詢結果
     */
    @ApiOperation(value = "分頁查詢商品信息", notes = "分頁查詢商品信息")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "current", value = "當前頁", dataType = "long", paramType = "query", defaultValue = "1"),
            @ApiImplicitParam(name = "size", value = "每頁顯示幾條", dataType = "long", paramType = "query", defaultValue = "10"),
    })
    @GetMapping("/page")
    @SysLog("分頁查詢商品信息")
    public R<IPage<GoodsInfo>> page(GoodsInfoPageDTO data) {
        Page<GoodsInfo> page = getPage();
        LbqWrapper<GoodsInfo> wrapper = Wraps.lbQ();

        wrapper.like(GoodsInfo::getName, data.getName())
                .like(GoodsInfo::getCode, data.getCode())
                .eq(GoodsInfo::getBarCode, data.getBarCode())
                .geHeader(GoodsInfo::getCreateTime, data.getStartCreateTime())
                .leFooter(GoodsInfo::getCreateTime, data.getEndCreateTime())
                .orderByDesc(GoodsInfo::getCreateTime);

        goodsInfoService.page(page, wrapper);
        return success(page);
    }

    @ApiOperation(value = "查詢商品信息", notes = "查詢商品信息")
    @GetMapping("/list")
    @SysLog("查詢商品信息")
    public R<List<GoodsInfo>> list(GoodsInfoPageDTO data) {

        LbqWrapper<GoodsInfo> wrapper = Wraps.lbQ();

        wrapper.like(GoodsInfo::getName, data.getName())
                .like(GoodsInfo::getCode, data.getCode())
                .eq(GoodsInfo::getBarCode, data.getBarCode())
                .geHeader(GoodsInfo::getCreateTime, data.getStartCreateTime())
                .leFooter(GoodsInfo::getCreateTime, data.getEndCreateTime())
                .orderByDesc(GoodsInfo::getCreateTime);

        return success(goodsInfoService.list(wrapper));
    }

    /**
     * 查詢商品信息
     *
     * @param id 主鍵id
     * @return 查詢結果
     */
    @ApiOperation(value = "查詢商品信息", notes = "查詢商品信息")
    @GetMapping("/{id}")
    @SysLog("查詢商品信息")
    public R<GoodsInfo> get(@PathVariable Long id) {
        return success(goodsInfoService.getById(id));
    }

    /**
     * 新增商品信息
     *
     * @param data 新增對象
     * @return 新增結果
     */
    @ApiOperation(value = "新增商品信息", notes = "新增商品信息不為空的字段")
    @PostMapping
    @SysLog("新增商品信息")
    public R<GoodsInfo> save(@RequestBody @Validated GoodsInfoSaveDTO data) {
        GoodsInfo GoodsInfo = dozer.map(data, GoodsInfo.class);
        goodsInfoService.save(GoodsInfo);
        return success(GoodsInfo);
    }

    /**
     * 修改商品信息
     *
     * @param data 修改對象
     * @return 修改結果
     */
    @ApiOperation(value = "修改商品信息", notes = "修改商品信息不為空的字段")
    @PutMapping
    @SysLog("修改商品信息")
    public R<GoodsInfo> update(@RequestBody @Validated(SuperEntity.Update.class) GoodsInfoUpdateDTO data) {
        GoodsInfo GoodsInfo = dozer.map(data, GoodsInfo.class);
        goodsInfoService.updateById(GoodsInfo);
        return success(GoodsInfo);
    }

    /**
     * 刪除商品信息
     *
     * @param ids 主鍵id
     * @return 刪除結果
     */
    @ApiOperation(value = "刪除商品信息", notes = "根據id物理刪除商品信息")
    @SysLog("刪除商品信息")
    @DeleteMapping
    public R<Boolean> delete(@RequestParam("ids[]") List<Long> ids) {
        goodsInfoService.removeByIds(ids);
        return success();
    }
}

20.1.3 配置網關路由規則

在Nacos中的pd-gateway.yml中新增商品服務相關的路由配置,內容如下:

zuul:
  #  debug:
  #    request: true
  #  include-debug-header: true
  retryable: false
  servlet-path: /         # 默認是/zuul , 上傳文件需要加/zuul前綴才不會出現亂碼,這個改成/ 即可不加前綴
  ignored-services: "*"   # 忽略eureka上的所有服務
  sensitive-headers:  # 一些比較敏感的請求頭,不想通過zuul傳遞過去, 可以通過該屬性進行設置
  #  prefix: /api #為zuul設置一個公共的前綴
  #  strip-prefix: false     #對於代理前綴默認會被移除   故加入false  表示不要移除
  routes:  # 路由配置方式
    authority:  # authority是路由名稱,可以隨便定義,但是path和service-id需要一一對應
      path: /authority/**
      serviceId: pd-auth-server
    goods:
      path: /goods/**
      serviceId: pd-goods-server

20.1.4 前端開發

可以將pinda-authority-ui作為前端開發腳手架,基於此工程開發商品服務相關頁面。資料中已經提供了開發完成的前端工程,直接運行即可。

20.1.5 配置菜單和資源權限

啟動網關服務、權限服務、商品服務、前端工程,使用管理員賬號登錄,配置商品服務相關的菜單和對應的資源權限。

20.1.6 配置角色

啟動網關服務和權限服務,使用管理員賬號登錄。創建新角色並進行配置(菜單權限和資源權限)和授權(為用戶分配角色)。

20.2 已有項目集成

本小節通過一個已經完成開發的TMS(品達物流)項目來展示如何進行已有項目集成的過程。

20.2.1 TMS調整

20.2.1.1 頁面菜單

對於已經完成相關業務開發的項目,可以將其前端系統的頁面通過iframe的形式內嵌到通用權限系統的前端頁面中,這就需要對其前端系統的頁面進行相應的修改。因為原來的TMS系統前端頁面的左側菜單和導航菜單都在自己頁面中展示,現在需要將這些菜單配置到通用權限系統中,通過權限系統的前端系統來展示。

20.2.1.2 請求地址

為了能夠進行鑒權相關處理,需要將TMS前端發送的請求首先經過通用權限系統的網關進行處理:

20.2.2 網關路由配置

配置通用權限系統的網關路由規則,將針對TMS的請求轉發到TMS相關服務:

zuul:
  retryable: false
  servlet-path: /
  ignored-services: "*"   # 忽略eureka上的所有服務
  sensitive-headers:  # 一些比較敏感的請求頭,不想通過zuul傳遞過去, 可以通過該屬性進行設置
  routes:  # 路由配置方式
    authority: 
      path: /authority/**
      serviceId: pd-auth-server
    pay:
      path: /pay/**
      serviceId: pd-ofpay-server
    web-manager:
      path: /web-manager/**
      serviceId: pd-web-manager
    web-xczx:
      path: /xczx/api/**
      url: http://xc-main-java.itheima.net:7291/api/

20.2.3 通用權限系統配置

20.2.3.1 菜單配置

登錄通用權限系統,配置TMS項目相應的菜單:

20.2.3.2 資源權限配置

資源權限都是關聯到某個菜單下的,所以要配置資源權限需要先選中某個菜單,然后就可以配置相關資源權限了:

20.2.3.3 角色配置

登錄通用權限系統,在角色管理菜單中配置TMS項目中使用到的角色:

角色創建完成后可以為角色配置菜單權限和資源權限:

完成角色的菜單權限和資源權限配置后可以將角色授權給用戶:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM