springcloud-gateway整合jwt+jcasbin實現許可權控制

陳揚天發表於2023-02-06

jcasbin簡介:

jcasbin 是一個用 Java 語言打造的輕量級開源訪問控制框架https://github.com/casbin/jcasbin,是casbin的Java語言版本。目前在 GitHub 開源。jcasbin 採用了元模型的設計思想,支援多種經典的訪問控制方案,如基於角色的訪問控制 RBAC、基於屬性的訪問控制 ABAC 等。

jcasbin 的主要特性包括:

1.支援自定義請求的格式,預設的請求格式為{subject, object, action};
2.具有訪問控制模型 model 和策略 policy 兩個核心概念;
3.支援 RBAC 中的多層角色繼承,不止主體可以有角色,資源也可以具有角色;
4.支援超級使用者,如 root 或 Administrator,超級使用者可以不受授權策略的約束訪問任意資源;
5.支援多種內建的運算子,如 keyMatch,方便對路徑式的資源進行管理,如 /foo/bar 可以對映到 /foo*

jcasbin 不做的事情:

1.身份認證 authentication (即驗證使用者的使用者名稱、密碼),jcasbin 只負責訪問控制。應該有其他專門的元件負責身份認證,然後由 jcasbin 進行訪問控制,二者是相互配合的關係;
2.管理使用者列表或角色列表。jcasbin 認為由專案自身來管理使用者、角色列表更為合適,jcasbin 假設所有策略和請求中出現的使用者、角色、資源都是合法有效的。

專案架構:

基於springboot+springcloud+nacos的簡單分散式專案,專案互動採用openFeign框架,單獨提取出來成為一個獨立的model:feign

父pom檔案:

    <properties>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
<druid.version>1.2.4</druid.version>
<spring-boot.version>2.2.6.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.9.RELEASE</spring-cloud-alibaba.version>
<sql.version>8.0.29</sql.version>
<jwt.version>0.9.0</jwt.version>
<swagger2.version>2.9.2</swagger2.version>
<jcasbin.version>1.32.1</jcasbin.version>
<jdbc-adapter.version>2.3.3</jdbc-adapter.version>
</properties>
<dependencies>
<dependency>
<groupId>com.distribute</groupId>
<artifactId>commonUtil</artifactId>
<version>${version}</version>
</dependency>
<dependency>
<groupId>org.casbin</groupId>
<artifactId>jcasbin</artifactId>
<version>${jcasbin.version}</version>
</dependency>
<dependency>
<groupId>org.casbin</groupId>
<artifactId>jdbc-adapter</artifactId>
<version>${jdbc-adapter.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.distribute</groupId>
<artifactId>feign</artifactId>
<version>${version}</version>
</dependency>
<!--鑑權-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${sql.version}</version>
<scope>runtime</scope>
</dependency>

<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

gateway專案:

pom檔案:

    <dependencies>
        <dependency>
            <groupId>com.distribute</groupId>
            <artifactId>feign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
    </dependencies>

gateway相關核心程式碼:

註冊中心採用nacos,關於nacos的使用可以自行學習,不是本文關鍵。

閘道器採用gateway,核心就是gateway中的過濾器介面:GlobalFilter:


@Slf4j
@Component
@Order(value = Integer.MIN_VALUE)
public class AuthorityGlobalFilter implements GlobalFilter, Ordered {

    @Autowired
    private ConfigProperty configProperty;
    @Autowired
    private AdminUserInterfaceFeign adminUserInterfaceFeign;



    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //filter的前置處理
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().pathWithinApplication().value();
        InetSocketAddress remoteAddress = request.getRemoteAddress();
        //3 獲得請求頭 ,獲得token值
        HttpHeaders headers = request.getHeaders();
        //判斷白名單和是否有許可權
        if (validateWhiteList(path)) {
            return chain
                    //繼續呼叫filter
                    .filter(exchange)
                    //filter的後置處理
                    .then(Mono.fromRunnable(() -> {
                        ServerHttpResponse response = exchange.getResponse();
                        HttpStatus statusCode = response.getStatusCode();
                        log.info("請求路徑:{},遠端IP地址:{},響應碼:{}", path, remoteAddress, statusCode);
                    }));
        } else if(hasPower(request)){
            return chain
                    //繼續呼叫filter
                    .filter(exchange)
                    //filter的後置處理
                    .then(Mono.fromRunnable(() -> {
                        ServerHttpResponse response = exchange.getResponse();
                        HttpStatus statusCode = response.getStatusCode();
                        log.info("請求路徑:{},遠端IP地址:{},響應碼:{}", path, remoteAddress, statusCode);
                    }));
        }else {
            return noPower(exchange);
        }

    }

    @Override
    public int getOrder() {
        return 0;
    }


    /**
     * 判斷是否有許可權
     */
    private boolean hasPower( ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
       List<String> authorizationList =  headers.getOrEmpty("Authorization");
       if(authorizationList.size()==0){
           return false;
       }else{
           try {
               Claims claims = JwtUtil.parseJWT(authorizationList.get(0));
               //判斷token是否過期
               Date expireTime = claims.getExpiration();
               Date now = new Date();
               if (now.after(expireTime))
               {
                   return false;
               }

                String userName = claims.getSubject();

               String path = request.getPath().pathWithinApplication().value();
               String method = request.getMethodValue();
               Policy checkPower = new Policy(userName,path,method);
               CommonResult result = adminUserInterfaceFeign.checkPower(checkPower);
               return result.isSuccess() && (Boolean) result.getData();
           }catch (Exception e){
               return false;
           }
       }
    }

    /**
     * 閘道器拒絕,返回Result
     *
     * @param
     */
    private Mono<Void> noPower(ServerWebExchange serverWebExchange) {
        // 許可權不夠攔截
        serverWebExchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        DataBuffer buffer = serverWebExchange.getResponse().bufferFactory().wrap(JSONUtil.toJsonStr(CommonResult.error(HttpStatusCode.UNAUTHORIZED)).getBytes(StandardCharsets.UTF_8));
        ServerHttpResponse response = serverWebExchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        //指定編碼,否則在瀏覽器中會中文亂碼
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));

    }


    public boolean validateWhiteList(String requestPath) {
        for (String whiteList : configProperty.getWhiteList()) {
            if (requestPath.contains(whiteList) || requestPath.matches(whiteList)) {
                return true;
            }
        }
        return false;
    }
}

閘道器中首先校驗是否屬於白名單,白名單可以寫在application.yml中,透過實體類載入:

application.yml:

distribute:
   config:
     whiteList:
      - admin/login
      - admin/role/checkPower

ConfigProperty:

@Component
@ConfigurationProperties(prefix = "distribute.config")
@Data
public class ConfigProperty {
    List<String> whiteList;
}

訪問的資源(比如Controller路徑)如果不存在於白名單,則透過Feign呼叫admin-user專案中的鑑權方法進行鑑權,關於admin-user專案以及feign的使用,在之後會提到,GlobalFilter中涉及的jwt工具類,文末會給出。

admin-user專案:

pom檔案:

    <dependencies>
        <dependency>
            <groupId>com.distribute</groupId>
            <artifactId>feign</artifactId>
        </dependency>

        <!--資料庫驅動-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--資料庫連線池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <!--jdbc連線資料庫-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!--服務註冊與發現-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

登入控制器(測試):

/**
 * @author :fengwenzhe
 * @date :Created in 2023/2/3 11:41
 * 檔案說明: </p>
 */
@RestController
@RequestMapping("admin")
public class LoginCtrl {

    @PostMapping("login")
    public CommonResult login(@RequestBody Account account){

            String token = JwtUtil.createJWT(UUID.randomUUID().toString(), account.getUserName(), 3600L*1000);
            Map<String,Object> result = new HashMap<>();
            result.put("username",account.getUserName());
            result.put("token",token);
            return CommonResult.ok(result);
    }

}

jcasbin的整合:

jcasbin可以從檔案載入角色許可權資訊,此處已整合成從資料庫載入角色許可權資訊,為此,需要為jcasbin配置資料來源(為了方便直接使用專案中的資料庫,實際生產環境可以分開)以及模型檔案路徑:

application.yml:

org:
  jcasbin:
    model-path: jcasbin/basic_model.conf
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/jcasbin?useSSL=false
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: root

使用jcasbin首先需要配置jcasbin工廠類,初始化enforcer:


@Component
public class EnforcerFactory implements InitializingBean {

  private static Enforcer enforcer;

  @Autowired
  private EnforcerConfigProperties enforcerConfigProperties;
  @Autowired
  private DataSource dataSource;


  @Override
  public void afterPropertiesSet() throws Exception {
    //從資料庫讀取策略

    JDBCAdapter jdbcAdapter = new JDBCAdapter(dataSource);

    String path = this.getClass().getClassLoader().getResource("").getPath();
    enforcer = new Enforcer(path+enforcerConfigProperties.getModelPath(), jdbcAdapter);
    enforcer.loadPolicy();//Load the policy from DB.
  }
public static Enforcer getEnforcer(){ return enforcer; } }

@Configuration
@ConfigurationProperties(prefix = "org.jcasbin")
@Data
public class EnforcerConfigProperties {

  private String modelPath;

}

此後所有對jcasbin的操作都基於唯一例項enforcer,此時就可以進行業務上的新增許可權、角色、鑑權等的開發了。

RoleController角色控制器:

package com.distribute.admin.ctrl;

import com.distribute.admin.service.EnforcerFactory;
import com.distribute.common.CommonResult;
import com.distribute.entity.PermissionEntity;
import com.distribute.entity.Policy;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


/**
 * @author :fengwenzhe
 * @date :Created in 2023/2/3 11:41
 * 檔案說明: </p>
 */
@RestController
@RequestMapping("admin/role")
public class RoleCtrl {

    /**
     *@Description <獲取全部角色>
     *@return com.distribute.common.CommonResult
     *@date 2023/2/6 11:13
     *@auther fengwenzhee
     */
    @PostMapping("findAllRoleList")
    public CommonResult findAllRoleList(){
        return EnforcerFactory.findAllRoleList();
    }


    /**
     *@Description <批次新增 使用者/角色 的許可權>
     *@param permissionEntity
     *@return com.distribute.common.CommonResult
     *@date 2023/2/6 11:13
     *@auther fengwenzhee
     */
    @PostMapping("batchAddPermission")
    public CommonResult batchAddPermission(@RequestBody PermissionEntity permissionEntity){
        return EnforcerFactory.batchAddPermission(permissionEntity);
    }

    /**
     *@Description <批次刪除 使用者/角色 的許可權>
     *@param permissionEntity
     *@return com.distribute.common.CommonResult
     *@date 2023/2/5 17:08
     *@auther fengwenzhee
     */
    @PostMapping("batchDeletePermission")
    public CommonResult batchDeletePermission(@RequestBody PermissionEntity permissionEntity){
        return EnforcerFactory.batchDeletePermission(permissionEntity);
    }

    /**
     *@Description <批次為使用者新增角色>
     *@param permissionEntity
     *@return com.distribute.common.CommonResult
     *@date 2023/2/6 11:17
     *@auther fengwenzhee
     */
    @PostMapping("batchAddRoleForUser")
    public CommonResult batchAddRoleForUser(@RequestBody PermissionEntity permissionEntity){
        return EnforcerFactory.batchAddRoleForUser(permissionEntity);
    }

    /**
     *@Description <批次刪除使用者角色>
     *@param permissionEntity
     *@return com.distribute.common.CommonResult
     *@date 2023/2/5 17:08
     *@auther fengwenzhee
     */
    @PostMapping("batchDeleteRoleForUser")
    public CommonResult batchDeleteRoleForUser(@RequestBody PermissionEntity permissionEntity){
        return EnforcerFactory.batchDeleteRoleForUser(permissionEntity);
    }


    /**
     *@Description <批次刪除角色及其涉及到的使用者與角色關係>
     *@param permissionEntity
     *@return com.distribute.common.CommonResult
     *@date 2023/2/5 17:08
     *@auther fengwenzhee
     */
    @PostMapping("batchDeleteRole")
    public CommonResult batchDeleteRole(@RequestBody PermissionEntity permissionEntity){
        return EnforcerFactory.batchDeleteRole(permissionEntity);
    }


    @PostMapping("checkPower")
    public  CommonResult checkPower(@RequestBody Policy policy){
        if(policy.getSub().equals("admin")){ //超級管理員直接放行
            return CommonResult.ok(true);
        }
        String path = this.getClass().getClassLoader().getResource("").getPath();
        // Enforcer enforcer = new Enforcer(path+"/jcasbin/basic_model.conf", path+"/jcasbin/basic_policy.csv"); 從本地檔案載入許可權資訊

        if (EnforcerFactory.getEnforcer().enforce("user_"+policy.getSub(), policy.getObj(), policy.getAct())) {
            // permit alice to read data1

            return CommonResult.ok(true);
        } else {
            // deny the request, show an error

            return CommonResult.ok(false);
        }
    }

}

基於RBAC的模型檔案basic_model.conf:

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

RoleController角色控制器中已經寫好了一些方法,後續可以根據需要自行新增,入參實體我簡單封裝了一下,然後迴圈進行批次操作:

PermissionEntity:


@Data
public class PermissionEntity  implements Serializable {
    private Integer type; //操作物件是使用者還是角色
    private List<Policy> policyList;

}

Policy:

@Data
public class Policy implements Serializable {
    /**想要訪問資源的使用者 或者角色*/
    private String sub;

    /**將要訪問的資源,可以使用  * 作為萬用字元,例如/user/* */
    private String obj;

    /**使用者對資源執行的操作。HTTP方法,GET、POST、PUT、DELETE等,可以使用 * 作為萬用字元*/
    private String act;

    /**
     *
     * @param sub 想要訪問資源的使用者 或者角色
     * @param obj 將要訪問的資源,可以使用  * 作為萬用字元,例如/user/*
     * @param act 使用者對資源執行的操作。HTTP方法,GET、POST、PUT、DELETE等,可以使用 * 作為萬用字元
     */
    public Policy(String sub, String obj, String act) {
        super();
        this.sub = sub;
        this.obj = obj;
        this.act = act;
    }

    @Override
    public String toString() {
        return "Policy [sub=" + sub + ", obj=" + obj + ", act=" + act + "]";
    }

}

在EnforcerFactory中新增RoleController對應方法:

 public static CommonResult batchAddPermission(PermissionEntity permissionEntity) {
    if(permissionEntity.getType()==null){
      return CommonResult.error(HttpStatusCode.OPERATION_TYPE_REQUIRED);
    }
    if(permissionEntity.getType()==1){
      //操作物件為使用者
      for (Policy policy:permissionEntity.getPolicyList()){
        enforcer.addPermissionForUser("user_"+policy.getSub(),policy.getObj(),policy.getAct());
      }
    }else if(permissionEntity.getType()==2){
      //操作物件為角色
      for (Policy policy:permissionEntity.getPolicyList()){
        enforcer.addPermissionForUser("role_"+policy.getSub(),policy.getObj(),policy.getAct());
      }
    }else {
      return CommonResult.error(HttpStatusCode.OPERATION_TYPE_ERROR);
    }
    return CommonResult.ok(true);
  }

  public static CommonResult batchAddRoleForUser(PermissionEntity permissionEntity) {
    for (Policy policy:permissionEntity.getPolicyList()){
      enforcer.addRoleForUser("user_"+policy.getSub(),"role_"+policy.getObj());
    }
    return CommonResult.ok(true);
  }

  public static CommonResult batchDeleteRole(PermissionEntity permissionEntity) {

    for (Policy policy:permissionEntity.getPolicyList()){
      enforcer.deleteRole("role_"+policy.getSub());
    }
    return CommonResult.ok(true);
  }

  public static CommonResult batchDeleteRoleForUser(PermissionEntity permissionEntity) {
    for (Policy policy:permissionEntity.getPolicyList()){
      enforcer.deleteRoleForUser("user_"+policy.getSub(),"role_"+policy.getObj());
    }
    return CommonResult.ok(true);
  }

  public static CommonResult batchDeletePermission(PermissionEntity permissionEntity) {
    if(permissionEntity.getType()==null){
      return CommonResult.error(HttpStatusCode.OPERATION_TYPE_REQUIRED);
    }
    if(permissionEntity.getType()==1){
      //操作物件為使用者
      for (Policy policy:permissionEntity.getPolicyList()){
        enforcer.deletePermissionForUser("user_"+policy.getSub(),policy.getObj(),policy.getAct());
      }
    }else if(permissionEntity.getType()==2){
      //操作物件為角色
      for (Policy policy:permissionEntity.getPolicyList()){
        enforcer.deletePermissionForUser("role_"+policy.getSub(),policy.getObj(),policy.getAct());
      }
    }else {
      return CommonResult.error(HttpStatusCode.OPERATION_TYPE_ERROR);
    }
    return CommonResult.ok(true);
  }

  public static CommonResult findAllRoleList() {
    List<String> roles = new ArrayList<>();
    for (String role:enforcer.getAllRoles()){
      roles.add(role.split("role_")[1]);
    }
    return CommonResult.ok(roles);
  }

PS:jcasbin中對許可權的把控是基於subject的,所以無法區分許可權是使用者還是角色的,在這裡用字首是user_還是role_來區分,資料庫測試資料如下:

 

 意思是role_管理員角色下有兩個許可權,分別是/c/main/getUser POST,和/c/main/deleteUser DELETE,v1欄位可以視為資源,v2為請求動作,

user_fengwenzhe使用者具有role_管理員的角色,鑑權時可以如下進行:

 String path = request.getPath().pathWithinApplication().value();
               String method = request.getMethodValue();
               Policy checkPower = new Policy(userName,path,method);
               CommonResult result = adminUserInterfaceFeign.checkPower(checkPower);

比如此時我傳入userName=fengwenzhe,path=/c/main/getUser method=POST,就可以鑑權成功,因為有字首存在,程式碼中自行補足'user_':

 if (EnforcerFactory.getEnforcer().enforce("user_"+policy.getSub(), policy.getObj(), policy.getAct())) {
            // permit  to read data

            return CommonResult.ok(true);
        } else {
            // deny the request, show an error

            return CommonResult.ok(false);
        }

 

feign專案:

只定義feign相關介面與實現類:

/**
 * @author :fengwenzhe
 * @date :Created in 2023/2/2 21:48
 * 檔案說明: </p>
 */
@FeignClient(value = "platform-admin-user",fallback = AdminUserFeignImpl.class)
@Component
public interface AdminUserInterfaceFeign {

@PostMapping("admin/role/checkPower")
CommonResult checkPower(@RequestBody Policy policy);

/** *@Description <批次新增 使用者/角色 的許可權> *@param permissionEntity *@return com.distribute.common.CommonResult *@date 2023/2/6 11:13 *@auther fengwenzhee */ @PostMapping("batchAddPermission") CommonResult batchAddPermission(@RequestBody PermissionEntity permissionEntity); }
package com.distribute.impl;

import com.distribute.AdminUserInterfaceFeign;
import com.distribute.common.CommonResult;
import com.distribute.common.HttpStatusCode;
import com.distribute.entity.PermissionEntity;
import com.distribute.entity.Policy;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * @author :fengwenzhe
 * @date :Created in 2023/2/2 22:03
 * 檔案說明: </p>
 */
@Component
public class AdminUserFeignImpl implements AdminUserInterfaceFeign {

    @Override
    public CommonResult checkPower(@RequestBody Policy power) {
        return CommonResult.error(HttpStatusCode.REQUEST_TIMEOUT);
    }


    @Override
    public CommonResult batchAddPermission(PermissionEntity permissionEntity) {

        return CommonResult.error(HttpStatusCode.REQUEST_TIMEOUT);
    }


}

gateway啟動類加入feign相關注釋:

@SpringBootApplication
@ComponentScan(basePackages = {"com.distribute"})
@EnableFeignClients(basePackages = "com.distribute") //因為feign介面定義的包與專案不同級  專案預設掃描com.distribute.gateway
public class GatewayApplication {

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

}

此時啟動gateway透過feign呼叫admin-user專案中的方法依然還是報錯,需要增加如下配置類:

/**
 *@Description <手動注入Bean Spring Cloud Gateway是基於WebFlux的,是ReactiveWeb,所以HttpMessageConverters不會自動注入。如果不注入,springcloudGateway呼叫feign時會報錯
 * No qualifying bean of type 'org.springframework.boot.autoconfigure.http.HttpMessageConverters>
 */
@Configuration
public class FeignConfig {
    @Bean
    @ConditionalOnMissingBean
    public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
        return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
    }
}

最後自行設定異常資訊:

沒有許可權:

{
    "code": 401,
    "data": "",
    "message": "沒有被授權或者授權已經失效",
    "success": false
}

鑑權成功:

{
    "data": [
        "管理員"
    ],
    "success": true,
    "code": 200,
    "message": "請求已經成功處理"
}

jwt工具類:


@Component
public class JwtUtil {

    //加密 解密時的金鑰 用來生成key
    public static final String JWT_KEY = "IT1995";

    /**
     * 生成加密後的秘鑰 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }


    public static String createJWT(String id, String subject, long ttlMillis){

        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //指定簽名的時候使用的簽名演算法,也就是header那部分,jwt已經將這部分內容封裝好了。
        long nowMillis = System.currentTimeMillis();//生成JWT的時間
        Date now = new Date(nowMillis);
        SecretKey key = generalKey();//生成簽名的時候使用的秘鑰secret,這個方法本地封裝了的,一般可以從本地配置檔案中讀取,切記這個秘鑰不能外露哦。它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發jwt了。
        JwtBuilder builder = Jwts.builder() //這裡其實就是new一個JwtBuilder,設定jwt的body
//                .setClaims(claims)            //如果有私有宣告,一定要先設定這個自己建立的私有的宣告,這個是給builder的claim賦值,一旦寫在標準的宣告賦值之後,就是覆蓋了那些標準的宣告的
                .setId(id)                    //設定jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設定為一個不重複的值,主要用來作為一次性token,從而回避重放攻擊。
                .setIssuedAt(now)            //iat: jwt的簽發時間
                .setSubject(subject)        //sub(Subject):代表這個JWT的主體,即它的所有人,這個是一個json格式的字串,可以存放什麼userid,roldid之類的,作為什麼使用者的唯一標誌。
                .signWith(signatureAlgorithm, key);//設定簽名使用的簽名演算法和簽名使用的秘鑰
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);        //設定過期時間
        }
        return builder.compact();            //就開始壓縮為xxxxxxxxxxxxxx.xxxxxxxxxxxxxxx.xxxxxxxxxxxxx這樣的jwt
    }

    public static Claims parseJWT(String jwt){

        SecretKey key = generalKey();  //簽名秘鑰,和生成的簽名的秘鑰一模一樣
        Claims claims = Jwts.parser()  //得到DefaultJwtParser
                .setSigningKey(key)         //設定簽名的秘鑰
                .parseClaimsJws(jwt).getBody();//設定需要解析的jwt
        return claims;
    }

    public static void main(String[] args){

        Account account = new Account();
        account.setUserName("it1995");
        account.setPassword("123456");
        String jwt = createJWT(UUID.randomUUID().toString(), JSONUtil.toJsonStr(account), 3600 * 24);

        System.out.println("加密後:" + jwt);

        //解密
        Claims claims = parseJWT(jwt);
        System.out.println("解密後:" + claims.getSubject());
    }
}

統一結果返回類:

package com.distribute.common;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * @author :fengwenzhe
 * @date :Created in 2023/2/2 20:38
 * 檔案說明: </p>
 */
@Data
@AllArgsConstructor
public class CommonResult {
    private Object data;
    private boolean success;
    private Integer code;
    private String message;
    //私有化,防止new
    private CommonResult() {}

    //成功
    public static CommonResult ok(Object data,  HttpStatusCode statusCode) {
        return new CommonResult(data,true,statusCode.code,statusCode.zhMessage);  //code 也可以使用字典管理
    }

    //成功返回 過載 message沒有特別要求
    public static CommonResult ok(Object data) {
        return CommonResult.ok(data, HttpStatusCode.OK); //message 也可以使用字典管理
    }

    // 失敗
    public static CommonResult error( HttpStatusCode statusCode) {
        return new CommonResult("",false, statusCode.code, statusCode.zhMessage);
    }
}
package com.distribute.common;

import lombok.Data;

public enum HttpStatusCode {
    /**
     * http狀態碼列舉所有狀態碼註解
     */
    USERNAME_PASSWORD_DENY(1000, "username password deny", "使用者名稱或密碼錯誤"),
    OK(200, "OK", "請求已經成功處理"),
    OPERATION_TYPE_ERROR(512, "", "操作型別不正確");

    //錯誤碼
    public Integer code;
    //提示資訊
    public String enMessage;

    //提示資訊
    public String zhMessage;


    HttpStatusCode(int code, String enMessage, String zhMessage) {
        this.code = code;
        this.enMessage = enMessage;
        this.zhMessage = zhMessage;
    }
}

 

相關文章