SpringBoot 基于 OAuth2 统一身份认证流程详解

Posted 地表最强菜鸡

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot 基于 OAuth2 统一身份认证流程详解相关的知识,希望对你有一定的参考价值。

1. 目标

  • 了解OAUTH2统一认证基本概念
  • 了解OAUTH2协议流程
  • 了解OAUTH2各种模式类型
  • 了解Spring Security OAuth设计

2. 分析

  • 传统登陆认证介绍

  • 单点登陆认证介绍

  • OAuth2简介

  • OAuth2角色

  • OAuth2协议流程介绍

  • OAuth2授权类型

  • OAuth2授权码模式流程

  • OAuth2简化模式

  • OAuth2密码模式

  • OAuth2客户端模式

  • Spring Security OAuth设计

3. 讲解

3.1 传统登陆认证

传统登陆方式是在每个服务进行登陆认证, 每个服务保存自己的用户数据, 并独立实现登陆认证逻辑。

随着服务的不断扩展, 用户数据很难集中统一,开发成本不断增加, 用户交互也极为不便 。

3.2 单点登陆认证

单点登陆是通过统一认证授权服务, 完成所有服务节点的登陆授权工作。

只需一台认证服务器,统一用户数据库, 完成用户认证授权, 控制资源访问, 支持其他服务或第三方应用接入, 扩展性强, 开发和运维成本降低。

3.3 OAuth2简介

OAuth 2.0 是一个行业的标准授权协议。OAuth 2.0 专注于简化客户端开发人员,同时为 Web 应用程序,桌面应用程序,手机等各种设备接入提供特定的授权流程。

OAuth2 实质是为第三方应用颁发一个具有时效性的Token令牌,使其他服务或第三方应用能够通过令牌获取相关资源。 常见的场景: 比如进入某个网站没有账号信息, 但可以通过QQ、微信、支付宝等账号进行登陆, 在这个登陆过程中采用的就是Oauth2协议; 对外API服务接口, 也一般采用OAUTH2授权, 比如微信API、新浪API等。

参考官方文档: https://oauth.net/2/

3.4 OAuth2角色

  • resource owner : 资源所有者,具备访问该资源的实体, 如果是某个人, 被称为end-user。
  • resources server: 资源服务器,受保护的资源服务器, 具备提供资源能力, 如订单服务, 商品服务等。
  • client: 客户端,这并不是指用户, 而是对资源服务器发起请求的应用程序,比如前后分离项目, 前端服务访问管理接口, 访问后台业务功能接口。
  • authorization server: 授权服务器, 能够给客户端颁发令牌, 这个就是我们上面所讲的统一认证授权服务器。
  • user-agent: 用户代理, 作为资源所有者与客户端沟通的工具, 比如APP, 浏览器等。

3.5 OAuth2 协议流程

  1. Resource Owner 与 Client 之间 , 资源所有者向Client发起认证请求, Client再返回认证授权信息。

  2. Client 收到 Resource Owner 的认证请求后, 会去Authorization Server 申请访问令牌, Authorization Server会让Client 进行认证, 通过之后会返回Access Token。

  3. Client 拿到 Authorization Server 的 Acceess Token , 访问Resource Server,Resource Server 验证之后, 返回被保护的资源信息。

  4. Resource Server 可以通过JWT在本地进行验证, 也可以访问 Authorization Server, 对Client 的请求的合法性进行验证。

3.6 授权类型

OAuth2 分为四种授权类型, 分别为:

  • Authorization Code(授权码模式):授权码模式, 先通过认证获取授权码, 然后申请获取token,进行资源访问。
  • Implicit(简化/隐式模式):用于简单应用,比如问卷调查等,用户认证通过之后, 认证服务器直接向应用服务返回token,这种模式比授权码模式少了授权码code获取环节, 简化交互, 但存在token过期与暴露问题(因为不能获取refresh_token)。
  • Resource Owner Password Credentials(密码模式):资源所有者和客户端之间具有高度信任时(例如,客户端是设备的操作系统的一部分,或者是一个高度特权应用程序, 比如APP, 自研终端等),因为client可能存储用户密码。
  • Client Credentials(客户端模式):该模式直接根据client端的id和密钥即可获取token, 不需要用户参与, 适合内部的API应用服务使用。

3.7 授权码模式流程

  1. 资源拥有者(用户)通过代理(WEB浏览器)访问客户端程序,发起授权码模式认证。

  2. 客户端(Client,比如CSDN论坛)向认证服务器(Auth Server,QQ账号认证服务)发起请求, 此时客户端携带了客户端标识(client_id, 标识来源是CSDN)和重定向地址(redirect_uri, 一般是CSDN的地址)。

  3. 用户确认授权,客户端(Client)接收到code。

  4. 在重定向的过程中,客户端拿到 code 与 client_idclient_secret 去授权服务器请求令牌,整个过程,用户代理是不会拿到令牌 token 的。

  5. 客户端(Client)拿到令牌 token 后就可以向第三方的资源服务器请求资源了, 比如获取QQ基本资料, 头像等信息。

授权请求:

response_type=code           // 必选项
&client_id=客户端的ID       // 必选项 
&redirect_uri=重定向URI    // 可选项 
&scope=申请的权限范围        // 可选项
&state=任意值              // 可选项

授权响应参数:

code=授权码          // 必填
&state=任意文字       // 如果授权请求中包含 state的话那就是必填

令牌请求:

grant_type=authorization_code      // 必填
&code=授权码                     // 必填 必须是认证服务器响应给的授权码
&redirect_uri=重定向URI          // 如果授权请求中包含 redirect_uri 那就是必填
&code_verifier=验证码            // 如果授权请求中包含 code_challenge 那就是必填

令牌响应:

"access_token":"访问令牌",      // 必填
 "token_type":"令牌类型",      // 必填
 "expires_in":过期时间,        // 任意
 "refresh_token":"刷新令牌",   // 任意
 "scope":"授权范围"            // 如果请求和响应的授权范围不一致就必填

3.8 简化模式


     +----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier     +---------------+
     |         -+----(A)-- & Redirection URI --->|               |
     |  User-   |                                | Authorization |
     |  Agent  -|----(B)-- User authenticates -->|     Server    |
     |          |                                |               |
     |          |<---(C)--- Redirection URI ----<|               |
     |          |          with Access Token     +---------------+
     |          |            in Fragment
     |          |                                +---------------+
     |          |----(D)--- Redirection URI ---->|   Web-Hosted  |
     |          |          without Fragment      |     Client    |
     |          |                                |    Resource   |
     |     (F)  |<---(E)------- Script ---------<|               |
     |          |                                +---------------+
     +-|--------+
       |    |
      (A)  (G) Access Token
       |    |
       ^    v
     +---------+
     |         |
     |  Client |
     |         |
     +---------+

  1. 资源拥有者(用户)通过代理(WEB浏览器)访问客户端程序,发起简化模式认证。
  2. 客户端(Client)向认证服务器(Auth Server)发起请求, 此时客户端携带了客户端标识(client_id)和重定向地址(redirect_uri)。
  3. 客户端(Client)拿到令牌 token 后就可以向第三方的资源服务器请求资源了。

授权请求:

response_type=token           // 必选项
&client_id=客户端的ID       // 必选项 
&redirect_uri=重定向URI    // 可选项 
&scope=申请的权限范围        // 可选项
&state=任意值              // 可选项

授权响应参数:

&access_token=令牌信息     // 必填
&expires_in=过期时间       // 任意
&state=任意文字            // 如果授权请求中包含 state 那就是必填
&scope=授权范围            // 如果请求和响应的授权范围不一致就必填

问题:为什么要有授权码和简化模式?看完这两种模式, 可能会有些疑问, 为什么要这么麻烦, 直接一次请求返回TOKEN不就可以吗?

我们可以看出, 两者主要差别, 是少了code验证环节, 直接返回token了, code验证是客户端与认证服务器在后台进行请求获取, 代理是获取不到TOKEN的, 如果缺少这个环节, 直接返回TOKEN, 相当于直接暴露给所有参与者, 存在安全隐患, 所以简化模式,一般用于信赖度较高的环境中使用。

3.9 密码模式

     +----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          v
          |    Resource Owner
         (A) Password Credentials
          |
          v
     +---------+                                  +---------------+
     |         |>--(B)---- Resource Owner ------->|               |
     |         |         Password Credentials     | Authorization |
     | Client  |                                  |     Server    |
     |         |<--(C)---- Access Token ---------<|               |
     |         |    (w/ Optional Refresh Token)   |               |
     +---------+                                  +---------------+

  1. 资源拥有者直接通过客户端发起认证请求。
  2. 客户端提供用户名和密码, 向认证服务器发起请求认证。
  3. 认证服务器通过之后, 客户端(Client)拿到令牌 token 后就可以向第三方的资源服务器请求资源了。

令牌请求:

grant_type=password       // 必填
&username=用户ID    // 必填
&password=密码    // 必填
&scope=授权范围       // 任意

令牌响应:

"access_token":"访问令牌",   // 必填
"token_type":"令牌类型",      // 必填
"expires_in":"过期时间",        // 任意
"refresh_token":"刷新令牌", // 任意
"scope":"授权范围"              // 如果请求和响应的授权范围不一致就必填

此模式简化相关步骤, 直接通过用户和密码等隐私信息进行请求认证, 认证服务器直接返回token, 这需要整个环境具有较高的安全性。

3.10 客户端模式

     +---------+                                  +---------------+
     |         |                                  |               |
     |         |>--(A)- Client Authentication --->| Authorization |
     | Client  |                                  |     Server    |
     |         |<--(B)---- Access Token ---------<|               |
     |         |                                  |               |
     +---------+                                  +---------------+
  1. 此模式最为简单直接, 由客户端直接发起请求。
  2. 客户端与服务器信赖度较高, 服务端根据请求直接认证返回token信息。
  3. 客户端(Client)拿到令牌 token 后就可以向第三方的资源服务器请求资源了。

这种模式一般在内部服务之间应用, 授权一次, 长期可用, 不用刷新token。

令牌请求:

grant_type=client_credentials     // 必填
client_id=客户端的ID          // 必填
client_secret=客户端的密钥    // 必填
&scope=授权范围               // 任意

令牌响应:

"access_token":"访问令牌",   // 必填
"token_type":"令牌类型",      // 必填
"expires_in":"过期时间",        // 任意
"scope":"授权范围"              // 如果请求和响应的授权范围不一致就必填

3.11 Spring Security OAuth设计

Spring Security OAuth2 的整体设计, 我们会在项目中集成Spring Security 组件实现OAuth2统一授权认证。

参考:

理解OAuth2: https://www.kancloud.cn/kancloud/oauth_2_0/63331

图解授权模式: https://learnku.com/articles/20082

4. 总结

  • 了解OAUTH2统一认证基本概念, 各种角色与协议流程, 了解OAUTH2支持的四种模式, 以及各种模式的不同应用场景。

第2章 OAUTH2生产实践

1. 目标

  • 完成认证服务搭建配置, 功能验证。

2. 步骤

  • 服务功能设计
  • 组件与环境准备
  • 公用组件实现
  • 认证服务实现
  • 认证服务启动验证
  • 通过POSTMAN对认证服务做功能验证

3. 实现

3.1 服务设计

整体设计:

3.2 准备

  • IDEA 环境,安装 lombok插件

  • 安装REDIS及图形化工具

  • 安装Nacos

  • 安装mysql数据库

  • 安装PostMan

3.3 公用组件 (stock-common)

创建公用组件服务, 为了便于微服务之间公用功能的复用, 减少不必要的重复工作, 同时统一组件管理

结构:

  • stock-common-dao: 公用DAO数据层组件, 管理数据层依赖与公用接口。
  • stock-common-service: 公用服务层组件。
  • stock-common-utils: 公用辅助组件, 比如统一工具, 加解密,统一异常等。
  • stock-common-web: 公用WEB层组件。

统一实体工程:

  • stock-entity: 统一的实体工程, 所有实体统一放置, 与stock-common同级。

3.4 认证服务(trade-auth)

  1. 服务说明:

    统一认证服务实现OAUTH2认证功能。服务设计上, 采用的是增强token方式,这套流程会包含token扩展, 缓存集成认证, 密码模式, 加密处理, 自动化配置等功能, 学会这套流程的使用, 能够利用Spring Security处理大部分业务场景,包括jwt的实现。

    这里数据层采用的是JPA实现, 自定义用户服务接口,实现用户密码模式认证。

  2. 工程结构:

  3. 工程依赖

    父级工程依赖属性配置:

    <properties>
        ...
        <spring-cloud-starter.version>2.1.1.RELEASE</spring-cloud-starter.version>
        <druid.version>1.1.18</druid.version>
        <project.stock.version>1.0.0</project.stock.version>
    </properties>
    

    POM.XML文件:

    <dependencies>
            <!-- OATUH2 的核心组件 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-oauth2</artifactId>
                <version>$spring-cloud-starter.version</version>
            </dependency>
    
            <!-- Spring Security 依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-security</artifactId>
                <version>$spring-cloud-starter.version</version>
            </dependency>
    
            <!-- 自动化缓存依赖 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-cache</artifactId>
                <version>$spring-cloud-starter.version</version>
            </dependency>
    
            <!--缓存依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
                <version>$spring-cloud-starter.version</version>
            </dependency>
    
            <!-- jpa -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
                <version>$spring-cloud-starter.version</version>
            </dependency>
    
            <!-- Nacos服务注册发现依赖 -->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
    
    
            <!-- 公用utils工具的依赖 -->
            <dependency>
                <groupId>com.itcast.trade</groupId>
                <artifactId>bulls-stock-common-utils</artifactId>
                <version>$project.stock.version</version>
            </dependency>
    
            <!-- 公用实体的依赖 -->
            <dependency>
                <groupId>com.itcast.trade</groupId>
                <artifactId>bulls-stock-entity</artifactId>
                <version>$project.stock.version</version>
            </dependency>
    
            <!-- druid 连接池依赖 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>$druid.version</version>
            </dependency>
    
            <!-- 解决druid log4j监控组件无法使用问题, 不用再额外增加其他LOG框架 -->
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-log4j12</artifactId>
            </dependency>
    
            <!-- mysql-connector-java -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.14</version>
            </dependency>
        
         	<!--freemarker-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-freemarker</artifactId>
            </dependency>
            <!--web 模块-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
        	<!--tomcat容器, javax servelet相关依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </dependency>
    
        </dependencies>
    
    <!-- 打包配置 -->
        <build>
            <finalName>trade-auth</finalName>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-resources-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>default-resources</id>
                            <phase>validate</phase>
                            <goals>
                                <goal>copy-resources</goal>
                            </goals>
                            <configuration>
                                <outputDirectory>target/classes</outputDirectory>
                                <useDefaultDelimiters>false</useDefaultDelimiters>
                                <delimiters>
                                    <delimiter>$*</delimiter>
                                </delimiters>
                                <resources>
                                    <resource>
                                        <directory>src/main/resources/</directory>
                                        <filtering>true</filtering>
                                    </resource>
                                    <resource>
                                        <directory>src/main/java</directory>
                                        <includes>
                                            <include>**/*.xml</include>
                                        </includes>
                                        <filtering>false</filtering>
                                    </resource>
                                </resources>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <executions>
                        <execution>
                            <goals>
                                <goal>repackage</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
    
        </build>
    

  4. 启动类(TradeAuthApplication)

    @SpringBootApplication
    @EnableDiscoveryClient
    @ComponentScan(basePackages = "com.itcast")
    @EntityScan(basePackages = "com.itcast")
    @EnableJpaRepositories(basePackages =  "com.itcast")
    @EnableCaching
    public class TradeAuthApplication 
    
        public static void main(String[] args) 
            SpringApplication.run(TradeAuthApplication.class, args);
        
    
    
    

    使用JPA功能, 需要开启@EntityScan与@EnableJpaRepositories两个注解, 扫描指定路径。

    @EnableCaching是启用了Spring Cache缓存功能, 缓存是基于redis实现。

  5. 实体

    去实体工程里面, 创建用户信息实体TradeUser:

    @Data
    @Entity
    @Table(name = "t_trade_user")
    public class TradeUser extends BaseEntity 
    
        @Id
        private Long id;
    
        /**
         * 用户编号
         */
        private String userNo;
    
        /**
         * 用户名称
         */
        private String name;
    
        /**
         * 用户密码
         */
        private String userPwd;
    
        /**
         * 电话号码
         */
        private String phone;
    
        /**
         * 公司ID
         */
        private Long companyId;
    
        /**
         * 邮箱
         */
        private String email;
    
        /**
         * 地址
         */
        private String address;
    
        /**
         * 最近一次用户登陆IP
         */
        private String lastLoginIp;
    
        /**
         * 最近一次登陆时间
         */
        private Date lastLoginTime;
    
        /**
         * 状态(0:有效, 1:锁定, 2:禁用)
         */
        private int status;
    
        /**
         * 创建时间
         */
        private Date craeteTime;
    
    
    
    

    JPA实体要开启Entity,Table, ID三个标注。我们采用自动转换处理, 不用再加Column注解。

  6. 数据层

    用户信息数据接口TradeUserRepository

    /**
     * 用户信息数据层接口
     */
    @Repository("tradeUserRepository")
    public interface TradeUserRepository extends PagingAndSortingRepository<TradeUser, String>, JpaSpecificationExecutor<TradeUser> 
    
        /**
         * 根据用户账号获取用户对象
         * @param userNo
         * @return
         */
        public TradeUser findByUserNo(String userNo);
    
    

    我们通过JPA, 实现一个根据用户账号获取用户对象接口, 用于用户登陆处理。注意路径与实体TradeUser的路径要在上面讲的JPA扫描路径范围之内。

  7. 服务层

    • 用户信息服务接口AuthStockUserDetailServiceImpl

      
      @Service("authStockUserDetailService")
      public class AuthStockUserDetailServiceImpl implements UserDetailsService 
      
          @Autowired
          private TradeUserRepository tradeUserRepository;
      
          @Autowired
          private CacheManager cacheManager;
      
          @Override
          public UserDetails loadUserByUsername(String userNo) throws UsernameNotFoundException 
      
              // 查询缓存
              Cache cache = cacheManager.getCache(GlobalConstants.OAUTH_KEY_STOCK_USER_DETAILS);
              if (cache != null && cache.get(userNo) != null) 
                  return (UserDetails) cache.get(userNo).get();
              
              // 缓存未找到, 查询数据库
              TradeUser tradeUser = tradeUserRepository.findByUserNo(userNo);;
              if(null == tradeUser)
                  throw new UsernameNotFoundException(userNo  + " not valid !");
              
              // 封装成OAUTH鉴权的用户对象
              UserDetails userDetails = new OAuthTradeUser(tradeUser);
              // 将用户信息放入缓存
              cache.put(userNo, userDetails);
              return userDetails;
          
      
      
      
      

      这是Spring Security 提供的用户信息接口, 采用OAUTH的密码模式, 需要实现该接口的loadUserByUsername方法,为提升性能, 这里我们加入了Spring Cache缓存处理。

    • OAuthTradeUser用户封装信息:

      public class OAuthTradeUser extends User 
      
          private static final long serialVersionUUID = -1L;
      
          /**
           * 业务用户信息
           */
          private TradeUser tradeUser;
      
          public OAuthTradeUser(TradeUser tradeUser) 
              // OAUTH2认证用户信息构造处理
              super(tradeUser.getUserNo(), tradeUser.getUserPwd(), (tradeUser.getStatus() == 0 ? true : false),
                      true, true, (tradeUser.getStatus() == 0 ? true : false), Collections.emptyList());
              this.tradeUser = tradeUser;
          
      
      
      
      

    • 客户端信息服务接口

      public class AuthClientDetailService extends JdbcClientDetailsService 
      
          public AuthClientDetailService(DataSource dataSource) 
              super(dataSource);
          
      
          /**
           * 重写原生方法支持redis缓存
           *
           * @param clientId
           * @return
           * @throws InvalidClientException
           */
          @Override
          @Cacheable(value = GlobalConstants.OAUTH_KEY_CLIENT_DETAILS, key = "#clientId", unless = "#result == null")
          public ClientDetails loadClientByClientId(String clientId) 
              return super.loadClientByClientId(clientId);
          
      
      
      
      

      这是OAUTH内置的客户端信息, 重新它是为了实现缓存, 减少数据库查询。 对应的表为t_oauth_client_details。因为走的是redis缓存来处理鉴权, 其他OAUTH的内置表可以不用加入。

  8. 编写Web接口

    TradeStockTokenController, Token信息接口:

    @RestController
    @RequestMapping("/token")
    @Log4j2
    public class TradeStockTokenController 
    
        private static final String STOCK_OAUTH_ACCESS = GlobalConstants.OAUTH_PREFIX_KEY;
    
        @Autowired
        private RedisTemplate stockRedisTemplate;
    
        @Autowired
        private TokenStore tokenStore;
    
        @Autowired
        private CacheManager cacheManager;
    
        /**
         * 认证页面
         *
         * @return ModelAndView
         */
        @RequestMapping("/login")
        public ModelAndView require() 
            return new ModelAndView("ftl/login");
        
    
    
        /**
         * 认证页面
         *
         * @return ModelAndView
         */
        @RequestMapping("/success")
        public String success() 
            log.info("token login success!");
            return "login success";
        
    
    
        /**
         * 退出token
         *
         * @param authHeader Authorization
         */
        @DeleteMapping("/logout")
        public String logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) 
            if (StringUtils.isEmpty(authHeader)) 
                return "退出失败,token 为空";
            
    
            String tokenValue = authHeader.replace(OAuth2AccessToken.BEARER_TYPE, "").trim();
            OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
            if (accessToken == null || StringUtils.isEmpty(accessToken.getValue())) 
                return "退出失败,token 无效";
            
    
            OAuth2Authentication auth2Authentication = tokenStore.readAuthentication(accessToken);
            cacheManager.getCache(GlobalConstants.OAUTH_KEY_STOCK_USER_DETAILS).evict(auth2Authentication.getName());
            tokenStore.removeAccessToken(accessToken);
            return "退出成功, token 已清除";
        
    
        /**
         * 令牌管理调用
         *
         * @param token token
         * @return
         */
        @DeleteMapping("/token")
        public String delToken(@PathVariable("token") String token) 
            OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token);
            tokenStore.removeAccessToken(oAuth2AccessToken);
            return "token 已清除";
        
    
    

    TradeUserController用户接口:

    @RestController
    @RequestMapping("/trade")
    public class TradeUserController 
    
        @Autowired
        private UserDetailsService authStockUserDetailService;
    
    	 /**
         * 获取用户信息
         * @param username
         * @return
         */
        @RequestMapping("/user")
        @ResponseBody
        public UserDetails getUser(@RequestParam("username")String username) 
    
            UserDetails userDetails = authStockUserDetailService.loadUserByUsername(username);
    
            return userDetails;
        
    
    
    

  9. 配置类

    • 认证服务配置AuthorizationServerConfig,限于篇幅,贴核心代码:
        /**
         * Redis 缓存配置
         * @return
         */
        @Bean
        public RedisTemplate<String, Object> stockRedisTemplate() 
            RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
            redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
            redisTemplate.setConnectionFactory(redisConnectionFactory);
            return redisTemplate;
        
    
    
        /**
         * 自定义Client查询,可以修改表名, 字段等
         * @param clients
         */
    	@Override
    	@SneakyThrows
    	public void configure(ClientDetailsServiceConfigurer clients) 
            AuthClientDetailService clientDetailsService = new AuthClientDetailService(dataSource);
    		clientDetailsService.setSelectClientDetailsSql(DEFAULT_SELECT_STATEMENT);
    		clientDetailsService.setFindClientDetailsSql(DEFAULT_FIND_STATEMENT);
    		clients.withClientDetails(clientDetailsService);
    	
    
    
        /**
         * t_oauth_client_details 表的字段,不包括client_id、client_secret
         */
        String CLIENT_FIELDS = "client_id, client_secret, resource_ids, scope, "
                + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "
                + "refresh_token_validity, additional_information, autoapprove";
    
         /**
         * JdbcClientDetailsService 查询语句
         */
        String BASE_FIND_STATEMENT = "select " + CLIENT_FIELDS
                + " from t_oauth_client_details";
    
        /**
         * 默认的查询语句
         */
        String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + " order by client_id";
    
        /**
         * 按条件client_id 查询
         */
        String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?";
    
    
        /**
         * 防止申请token时出现401错误
         * @param oauthServer
         */
    	@Override
    	public void configure(AuthorizationServerSecurityConfigurer oauthServer) 
    		oauthServer
                    .tokenKeyAccess("permitAll()")
                    .checkTokenAccess("permitAll()")
    				.allowFormAuthenticationForClients();
    	
    
        /**
         * 认证服务配置
         * @param endpoints
         */
    	@Override
    	public void configure(AuthorizationServerEndpointsConfigurer endpoints) 
    		endpoints
    				.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
    				.tokenStore(tokenStore())
    				.tokenEnhancer(tokenEnhancer())
    				.userDetailsService(authStockUserDetailService)
    				.authenticationManager(authenticationManager)
    				.reuseRefreshTokens(false);
    	
    
    
    	
    

    这里包含了认证服务的配置, TokenStore实现配置, Token增强配置以及自定义Client的查询实现。

    注意要加上@EnableAuthorizationServer注解。

    • Web服务认证配置WebSecurityConfigurer

      这里我们实现了一个获取用户信息接口,以及自定义login登陆处理, 用于OAUTH的验证。

      @Primary
      @Order(90)
      @Configuration
      public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter 
      
          @Autowired
          private UserDetailsService authStockUserDetailService;
      
          @Autowired
          private StockPasswordEncoder stockPasswordEncoder;
          
      	/**
           * Web服务认证配置
           * @param http
           */
      	@Override
      	@SneakyThrows
      	protected void configure(HttpSecurity http) 
      		http
      			.formLogin()
      			.loginPage("/token/login")
      			.loginProcessingUrl("/token/form")
                  .defaultSuccessUrl("/token/success")
      			.and()
      			.authorizeRequests()
      			.antMatchers(
      				"/token/**",
      				"/actuator/**",
                          "/druid/**").permitAll()
      			.anyRequest().authenticated()
      			.and().csrf().disable();
      	
      
      

    /**
    * 不拦截静态资源

    • @param web

      */
      @Override
      public void configure(WebSecurity web)
      web.ignoring().antMatchers(“/css/**”);

      @Bean
      @Override
      @SneakyThrows
      public AuthenticationManager authenticationManagerBean()
      return super.authenticationManagerBean();

      @Autowired
      @Override
      protected void configure(AuthenticationManagerBuilder auth) throws Exception
      auth.userDetailsService(authStockUserDetailService).passwordEncoder(stockPasswordEncoder);

    
    + 密码加密处理器StockPasswordEncoder
    
    使用密码加密器后, OAuth内置的Client认证以及用户密码认证都会加密处理。
    
    ```java
    @Component
    @Log4j2
    public class StockPasswordEncoder implements PasswordEncoder 
    
        /**
         * 编码处理
         * @param rawPassword
         * @return
         */
        @Override
        public String encode(CharSequence rawPassword) 
            return rawPassword.toString();
        
    
        /**
         * 密码校验判断
         * @param rawPassword
         * @param encodedPassword
         * @return
         */
        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) 
            if(rawPassword != null && rawPassword.length() > 0)
                try 
                    // 这里通过MD5及B64加密
                    String password = EncryptUtil.encryptSigned(rawPassword.toString());
                    boolean isMatch= encodedPassword.equals(password);
                    if(!isMatch) 
                        log.warn("password 不一致!");
                    
                    return isMatch;
                 catch (ComponentException e) 
                   log.error(e.getMessage(), e);
                
            
            return false;
        
    
    
    
    

    • EncryptUtils加密类的实现

      public class EncryptUtil 
      
          private static final Logger logger = LoggerFactory.getLogger(EncryptUtil.class);
      
      
          private final static char[] hexDigits =  '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
                  'e', 'f' ;
          public static String BASE64Encrypt;
      
          public final static String MD5ToString(String signed) 
      
              char hexDigits[] =  '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' ;
              try 
      
                  byte[] res = signed.getBytes("UTF-8");
                  MessageDigest mdTemp = MessageDigest.getInstance("MD5".toUpperCase());
                  mdTemp.update(res);
                  byte[] md = mdTemp.digest();
      
                  // 把密文转换成十六进制的字符串形式
                  int j = md.length;
                  char str[] = new char[j * 2];
                  int k = 0;
                  for (int i = 0; i < j; i++) 
                      byte byte0 = md[i];
                      str[k++] = hexDigits[byte0 >>> 4 & 0xf];
                      str[k++] = hexDigits[byte0 & 0xf];
                  
                  return new String(str);
               catch (Exception e) 
                  logger.error(e.getMessage(), e);
                  return null;
              
          
      
          public final static byte[] MD5(String str) 
              try 
                  byte[] res = str.getBytes("UTF-8");
                  MessageDigest mdTemp = MessageDigest.getInstance("MD5".toUpperCase());
               mdTemp.update(res);
                  byte[] hash = mdTemp.digest();
               return hash;
               catch (Exception e) 
                  return null;
              
          
      
          /**
           * MD5值计算<p>
           * MD5的算法在RFC1321 中定义:
           * 在RFC 1321中,给出了Test suite用来检验你的实现是否正确:
           * MD5 ("") = d41d8cd98f00b204e9800998ecf8427e
           * MD5 ("a") = 0cc175b9c0f1b6a831c399e269772661
           * MD5 ("abc") = 900150983cd24fb0d6963f7d28e17f72
           * MD5 ("message digest") = f96b697d7cb7938d525a2f31aaf161d0
           * MD5 ("abcdefghijklmnopqrstuvwxyz") = c3fcd3d76192e4007dfb496cca67e13b
           *
           * @param res 源字符串
           * @return md5值
           */
          public final static byte[] MD5EncrtyReutrnhexDigitsByteArray(String str) 
              try 
                  byte[] res = str.getBytes("UTF-8");
                  MessageDigest mdTemp = MessageDigest.getInstance("MD5".toUpperCase());
                  mdTemp.update(res);
                  byte[] hash = mdTemp.digest();
                  return hash;
               catch (Exception e) 
                  return null;
              
          
      
          public final static String MD5EncrtyReturnString(String str) 
      
              byte[] b = MD5EncrtyReutrnhexDigitsByteArray(str);
      
              StringBuffer resultSb = new StringBuffer();
              for (int i = 0; i < b.length; i++) 
                  int n = b[i];
                  if (n < 0)
                      n = 256 + n;
                  int d1 = n / 16;
                  int d2 = n % 16;
                  resultSb.append(hexDigits[d1]);
                  resultSb.append(hexDigits[d2]);
              
              return resultSb.toString();
      
          
      
          // 加密后解密
          public static String JM(byte[] inStr) 
              String newStr = new String(inStr);
              char[] a = newStr.toCharArray();
              for (int i = 0; i < a.length; i++) 
                  a[i] = (char) (a[i] ^ 't');
              
              String k = new String(a);
              return k;
          
      
          /**
           * BASE64加密MD5EncrtyReutrnhexDigitsByteArray
           *
           * @param key
           * @return
           * @throws Exception
           */
          public static String BASE64Encrypt(byte[] key) throws ComponentException 
              String edata = null;
              try 
                  edata = (new BASE64Encoder()).encodeBuffer(key).trim();
               catch (Exception e) 
                  throw new ComponentException(e.getMessage() +
                          "BASE64编码错误!key=" + new String(key) + ", error=" + e.getMessage());
              
              return edata;
          
      
          /**
           * BASE64解密
           *
           * @param key
           * @return
           * @throws Exception
           */
          public static byte[] BASE64Decrypt(String data) 
              if (data == null)
                  return null;
              byte[] edata = null;
              try 
                  edata = (new BASE64Decoder()).decodeBuffer(data);
                  return edata;
               catch (Exception e) 
                  logger.error(e.getMessage(), e);
              
              return null;
          
      
      
          /**
           * 方法用途: 签名加密<br>
           * 实现步骤: <br>
           * @param signStr :签名的字符串
           * @return
           */
          public static String encryptSigned(String signed) throws ComponentException 
      
              try 
                  byte[] md5SignStr = MD5EncrtyReutrnhexDigitsByteArray(signed);
                  String b64SignStr = BASE64Encrypt(md5SignStr);
                  return b64SignStr;
              catch(Exception e) 
                  throw new ComponentException(e.getMessage()+ "BASE64或MD5加密签名错误!signed=" + signed + ", error=" + e.getMessage());
              
          
      
          public static void main(String[] args) throws  Exception 
              System.out.println(encryptSigned("app"));
              System.out.println(encryptSigned("123"));
          
      
      
      

  10. 工程配置信息

application.yml配置:

server:
  port: 9999
spring:
  application:
    name: trade-auth
  # 配置中心
  cloud:
    # 注册中心配置
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
      config:
        server-addr: 127.0.0.1:8848
  # 数据源配置, 采用Druid
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 654321
    url: jdbc:mysql://192.168.19.150:3306/trade_stock?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    druid:
      # 连接池的配置信息
      # 初始化大小,最小,最大
      initial-size: 5
      min-idle: 5
      maxActive: 20
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      # 打开PSCache,并且指定每个连接上PSCache的大小
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
      filters: stat,wall,log4j
      # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
      connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000
      # 配置DruidStatFilter
      web-stat-filter:
        enabled: true
        url-pattern: "/*"
        exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
      # 配置DruidStatViewServlet
      stat-view-servlet:
        url-pattern: "/druid/*"
        # IP白名单(没有配置或者为空,则允许所有访问)
        allow:
        # IP黑名单 (存在共同时,deny优先于allow)
        deny:
        #  禁用html页面上的“Reset All”功能
        reset-enable: false
        # 登录名
        login-username: admin
        # 登录密码
        login-password: admin123
        # 监控后台开关, 开启可通过后台管理查看
        enabled: true
  # Freemarker模板引擎配置
  freemarker:
      allow-request-override: false
      allow-session-override: false
      cache: true
      charset: UTF-8
      check-template-location: true
      content-type: text/html
      enabled: true
      expose-request-attributes: false
      expose-session-attributes: false
      expose-spring-macro-helpers: true
      prefer-file-system-access: true
      suffix: .ftl
      template-loader-path: classpath:/templates/
  # Spring Boot 的自动化配置, 排除过滤
  autoconfigure:
      exclude: org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration

  # Jpa功能配置
  jpa:
    hibernate:
      ddl-auto: none
      naming:
        # 实际命名, 无转换
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    show-sql: true

  # Redis 缓存配置
  redis:
    host: 127.0.0.1
    password:
    port: 6379

## spring security 配置
security:
  oauth2:
    resource:
      loadBalanced: true
      token-info-uri: http://trade-auth/oauth/check_token
    client:
      client-id: app
      client-secret: app
      scope: server
      # 默认放行url,如果子模块重写这里的配置就会被覆盖
      ignore-urls:
        - /actuator/**
        - /v2/api-docs
        - /swagger-ui.html
        - /doc.html

  • 这里包含Durid的数据源配置, 如果要开启后台监控页面, spring.datasource.druid.stat-view-servlet.enabled要设为true。

  • Freemarker模板引擎的配置, 这里我们使用了自定义的OAUTH login 页面。

  • Jpa功能的配置, 数据库和实体都采用驼峰命名方式, 不须再转换, physical-strategy选项值要设置为org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

  • Redis的配置, 用于OAuth TokenStore 缓存以及Spring Cache缓存

  • Spring Security 认证配置, 我们采用支持负载方式配置, 增强可用性。

  1. 拷贝静态服务资源

    这里采用了自定义的登陆页面, 将静态资源css与ftl模板copy至resources目录下:

3.5认证服务启动与验证

  1. 数据库数据初始化:

    
    -- ----------------------------
    -- Table structure for t_oauth_client_details
    -- ----------------------------
    DROP TABLE IF EXISTS `t_oauth_client_details`;
    CREATE TABLE `t_oauth_client_details`  (
      `client_id` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      `resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `access_token_validity` int(11) NULL DEFAULT NULL,
      `refresh_token_validity` int(11) NULL DEFAULT NULL,
      `additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      PRIMARY KEY (`client_id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    INSERT INTO `trade_stock`.`t_oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('admin', NULL, 'ISMvKXpXpadDiUoOSoAfww==', 'read_writer', 'password_refresh_token', NULL, NULL, NULL, NULL, NULL, NULL);
    INSERT INTO `trade_stock`.`t_oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('app', NULL, '0qV9wdiD/SH7mVFpnfccxw==', 'server', 'password,refresh_token', NULL, NULL, NULL, NULL, NULL, 'true');
    
    

    新增t_oauth_client_details表, 初始化两条client信息, 名称app, 密码app; 名称admin, 密码admin。

    初始化t_trade_user表数据

    INSERT INTO `trade_stock`.`t_trade_user`(`id`, `userNo`, `name`, `userPwd`, `phone`, `companyId`, `email`, `address`, `lastLoginIp`, `lastLoginTime`, `status`, `craeteTime`) VALUES (1, 'admin', 'admin', 'ISMvKXpXpadDiUoOSoAfww==', '123', 1, NULL, NULL, NULL, NULL, 0, NULL);
    
    

    在用户信息表t_trade_user里面初始化一条数据, 用户名为admin, 密码为admin。

  2. 启动相关服务

    • 启动Nacos
    • 启动Redis
    • 启动数据库
  3. 启动认证服务

  1. 使用验证
    端口为9999, 我们访问用户信息接口, 地址: http://127.0.0.1:9999/trade/user?username=admin
    自动进行拦截, 需要输入用户认证信息:

输入完用户名与密码, admin/admin, 返回结果:

3.6 认证服务与PostMan使用

  1. 申请token

    新建一个请求, 输入地址: 127.0.0.1:9999/oauth/token

    注意, 最后不要加/结束, 不是127.0.0.1:9999/oauth/token/

    选择认证方式, 填入Client的用户与密码信息,非t_trade_user信息。

填入grant_type, 认证模式; username和password对应t_trade_user表信息,scope作用域对应client用户的scope, 不能填错。

采用post和get方式请求都可以, 返回结果可以看到有两部分, 一部分是内置的token信息, 另外一部分就是我们自定义增强的token扩展信息。

  1. 刷新token

刷新token, 地址不变, 需要传递refresh_token, 这个在申请token时可以拿到, grant_type类型要设置为refresh_token。

刷新成功, 和上面申请的token值发生了变化, access_token已重新生成。

4. 总结

  • 了解整体服务设计, 完成认证服务的搭建配置, 通过postman对功能进行验证, 实现TOKEN的申请与刷新功能。

第3章 用户服务

1. 目标

  • 完善用户服务的配置, OAUTH2的鉴权功能以及验证

2. 步骤

  • 工程结构说明
  • 核心类实现说明
  • 统一异常处理
  • 统一接口数据返回
  • 功能验证

3. 实现

改进用户服务, 实现用户登陆接口, 集成OAUTH2鉴权功能。

3.1 工程结构

和最初搭建的用户服务已有较多差别, 加入了Mybatis,Druid数据源,Spring Security OAuth2集成。

3.2 统一异常处理说明

为规范各微服务对异常的处理, 采用统一封装异常及错误码, 使用方法:

在stock-common-utils下面封装了两类异常:

一个是ComponentException为组件异常, 使用相对简单 ;

另一个是BusinessException为业务异常,相比组件异常,可以封装更多信息,便于日志分析排查。

错误码统一要实现IErrorCodeEnum接口, 不能随便定义, ApplicationErrorCodeEnum是一个错误码的实现类:

public enum ApplicationErrorCodeEnum implements IErrorCodeEnum 


    SUCCESS("200", "成功"),
    FAILURE("300", "系统异常"),
    COMPONENT_LOAD_PROPERTIES_OBJ_HAD_EXIST("000001", "配置文件加载类已经存在" ),
    SYS_ERROR_ENCRYPT_SINGED(IErrorCodeEnum.MODULE_SYSTEM, "000002", "签名加密错误"),

    USER_NOT_FOUND(IErrorCodeEnum.MODULE_USER, "000003", "用户不存在!"),
    USER_PWD_ERROR(IErrorCodeEnum.MODULE_USER, "000004", "用户密码错误!"),
    ;

    /**
     * 业务模块
     */
    private String module;

    /**
     * 错误编号
     */
    private String code;

    /**
     * 消息
     */
    private String message;

    /**
     * 错误级别
     */
    private WarningLevelEnum warningLevel;


    ApplicationErrorCodeEnum(String code, String message, WarningLevelEnum warningLevelEnum) 
        this.code = code;
        this.message = message;
        this.warningLevel = warningLevelEnum;
    

    ApplicationErrorCodeEnum(String module, String code, String message, WarningLevelEnum warningLevelEnum) 
        this.module = module;
        this.code = code;
        this.message = message;
        this.warningLevel = warningLevelEnum;
    

    ApplicationErrorCodeEnum(String module, String code, String message) 
        this.module = module;
        this.code = code;
        this.message = message;
        this.warningLevel = WarningLevelEnum.COMMON;;
    


    ApplicationErrorCodeEnum(String code, String message) 
        this.module = IErrorCodeEnum.MODULE_SYSTEM;
        this.code = code;
        this.message = message;
        this.warningLevel = WarningLevelEnum.COMMON;
    


    @Override
    public String getCode() 
        return IErrorCodeEnum.MODULE_SYSTEM + this.code;
    

    @Override
    public String getMessage() 
        return this.message;
    

    @Override
    public WarningLevelEnum getLevel() 
        return warningLevel;
    


    @Override
    public String toString() 
        return IErrorCodeEnum.MODULE_SYSTEM + this.code + ", " + this.message;
    

里面主要包含服务模块, 错误编号, 消息和错误级别信息, 便于我们规范性的根据错误码来排查系统的错误信息。

有了异常与错误码, 并非可以任意使用, 为避免调用一个接口, 同一个异常在多处抛出, 规范在service业务层统一处理异常, 由controller接入层捕获异常, 封装返回给调用方。

3.3 统一接口数据返回说明

使用ApiRespResult统一返回数据,规范数据返回格式, 代码:


/**
 * 统一API接口数据返回对象
 * @param <T>
 */
public class ApiRespResult<T> implements Serializable 

    private static final long serialVersionUID = -1L;

    /**
     * 结果码
     */
    private String code = ApplicationErrorCodeEnum.SUCCESS.getCode();

    /**
     * 结果信息
     */
    private String msg = ApplicationErrorCodeEnum.SUCCESS.getMessage();

    /**
     * 扩展对象(放置分页信息、其他信息等)
     */
    private Object extendData;

    /**
     * 返回结果的数据对象
     */
    private T data;

    public ApiRespResult() 
    

    public ApiRespResult(String code) 
        this.code = code;
    
    
    public ApiRespResult(String code, String message)
    	this.code = code;
    	this.msg = message;
    
    

    public ApiRespResult(IErrorCodeEnum errorCodeEnum)
    	this.code = errorCodeEnum.getCode();
    	this.msg = errorCodeEnum.getMessage();
    
    
    public static <T> ApiRespResult<T> error(IErrorCodeEnum errorCodeEnum)
    	return new ApiRespResult<T>(errorCodeEnum);
    

    public static <T> ApiRespResult<T> sysError(String exceptionMsg)

        ApiRespResult error =  new ApiRespResult<T>(ApplicationErrorCodeEnum.FAILURE);
        error.setMsg(error.getMsg() + ":"

以上是关于SpringBoot 基于 OAuth2 统一身份认证流程详解的主要内容,如果未能解决你的问题,请参考以下文章

在 Spring Boot 2 上实现基于过滤器的 JWT 身份验证与 OAuth2 JWT 身份验证

Spring Cloud Gateway 基于 OAuth2.0 的身份认证

OAuth2用户详细信息未在春季安全(SpringBoot)中更新

spring cloud gateway + oauth2 实现网关统一权限认证

Springboot + JWT +OAuth2 + AngularJS 无状态会话

security + oauth2 + jwt + gateway 实现统一身份认证和鉴权(基础)