JavaEE企业级实战项目 智牛股第七天 权限与网关的搭建

JavaEE企业级实战项目 智牛股第七天 权限与网关的搭建,第1张

交易平台 - Day 7 学习目标

目标1:了解OAUTH2统一认证协议, 使用方法与原理

目标2:认证服务搭建, Spring Security OAuth2 的集成使用

目标3:改进用户服务,集成OAUTH2, 实现登陆功能

目标4:改进GateWay网关服务, 支持OAUTH2路由

目标5:Druid监控数据源配置使用

目标6:Spring Data JPA 配置使用

第1章 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. 工程依赖

    父级工程依赖属性配置:

    
        ...
        2.1.1.RELEASE
        1.1.18
        1.0.0
    
    

    POM.XML文件:

    
    
        
        
            org.springframework.cloud
            spring-cloud-starter-oauth2
            ${spring-cloud-starter.version}
        
        
        
            org.springframework.cloud
            spring-cloud-starter-security
            ${spring-cloud-starter.version}
        
    
    
        
        
            org.springframework.boot
            spring-boot-starter-cache
            ${spring-cloud-starter.version}
        
    
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
            ${spring-cloud-starter.version}
        
        
        
            org.springframework.boot
            spring-boot-starter-data-jpa
            ${spring-cloud-starter.version}
        
    
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-discovery
        
    
    
        
        
            com.itcast.trade
            bulls-stock-common-utils
            ${project.stock.version}
        
    
    
        
        
            com.itcast.trade
            bulls-stock-entity
            ${project.stock.version}
        
    
    
        
        
            com.alibaba
            druid-spring-boot-starter
            ${druid.version}
        
    
    
        
        
            org.slf4j
            slf4j-log4j12
        
    
        
        
            mysql
            mysql-connector-java
            8.0.14
        
    
    
        
        
            org.springframework.boot
            spring-boot-starter-freemarker
        
    
    
        
        
            org.springframework.boot
            spring-boot-starter-web
        
    
        
        
            org.springframework.boot
            spring-boot-starter-tomcat
        
    
    
    
        trade-auth
        
            
                org.apache.maven.plugins
                maven-resources-plugin
                
                    
                        default-resources
                        validate
                        
                            copy-resources
                        
                        
                            target/classes
                            false
                            
                                ${*}
                            
                            
                                
                                    src/main/resources/
                                    true
                                
                                
                                    src/main/java
                                    
                                        **/*.xml
                                    
                                    false
                                
                            
                        
                    
                
            
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            repackage
                        
                    
                
            
        
    
    
    
     
    
    
  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, JpaSpecificationExecutor {
        /**
         * 根据用户账号获取用户对象
         * @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 stockRedisTemplate() {
            RedisTemplate 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认证以及用户密码认证都会加密处理。

@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值计算

* 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; } /** * 方法用途: 签名加密
* 实现步骤:
* @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")); }

}


 

1. **工程配置信息**

 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目录下: ![image-20200102175456119](http://www.kaotop.com/file/tupian/20220426/51e4add4f8c3371aef75ab506dfe7555.png)

#### 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. **启动认证服务**

![1563778164794](http://www.kaotop.com/file/tupian/20220426/94be6b0e56fd8d3e33b8188b58e884f5.png)

4. **使用验证**  端口为9999, 我们访问用户信息接口, 地址: http://127.0.0.1:9999/trade/user?username=admin 自动进行拦截, 需要输入用户认证信息: ![1563778328192](http://www.kaotop.com/file/tupian/20220426/d50d0b08889598c08ca76953830d8cf3.png) 输入完用户名与密码, admin/admin, 返回结果: ![1563778384144](http://www.kaotop.com/file/tupian/20220426/ec7f8cc654f73fd3c46e99186d2496cb.png)

#### 3.6 认证服务与PostMan使用

1. **申请token**

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

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

![1563778846720](http://www.kaotop.com/file/tupian/20220426/3a3f09ff9d8182797d57eac0ebda8ba6.png)

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

![1563778915479](http://www.kaotop.com/file/tupian/20220426/32c10211a13265b81dc6771a63dce93a.png)

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

![1563779060847](http://www.kaotop.com/file/tupian/20220426/3c97b4fcc9c9e40361460ba25dbd492b.png)

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

2. **刷新token**

![1563779640435](http://www.kaotop.com/file/tupian/20220426/3e1bd185acf77a4cc69f447ae20c47c8.png)

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

![1563779793602](http://www.kaotop.com/file/tupian/20220426/83e44a51f78e97e2ca59757671238d02.png)

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



### 4. 总结

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



### 第3章 用户服务

### 1. 目标

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

### 2. 步骤

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

### 3. 实现

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

#### 3.1 工程结构

![1563779968169](http://www.kaotop.com/file/tupian/20220426/5587a21c3c32ebf9854c3dc2fee1abfa.png) 和最初搭建的用户服务已有较多差别, 加入了Mybatis,Druid数据源,Spring Security OAuth2集成。

#### 3.2 统一异常处理说明

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

![1563781260362](http://www.kaotop.com/file/tupian/20220426/29abdafd150145e058d62a4943e07c01.png) 在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
    */
    public class ApiRespResult 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 ApiRespResult error(IErrorCodeEnum errorCodeEnum){
    return new ApiRespResult(errorCodeEnum);
    }

    public static ApiRespResult sysError(String exceptionMsg){

     ApiRespResult error =  new ApiRespResult(ApplicationErrorCodeEnum.FAILURE);
     error.setMsg(error.getMsg() + ":" + exceptionMsg);
     return error;
    

    }

    public static ApiRespResult error(String code, String msg){
    return new ApiRespResult(code,msg);
    }

    public static ApiRespResult error(String code, String msg,T data){
    return new ApiRespResult(code,msg).setData(data);
    }

    public static ApiRespResult success(){
    return success(null);
    }

    public static ApiRespResult success(T data){
    return success(data, null);
    }

    public static ApiRespResult success(T data, Object extendData ){
    return new ApiRespResult().setData(data).setExtendData(extendData);
    }

    public Boolean isSuccess(){
    return ApplicationErrorCodeEnum.SUCCESS.getCode().equals(getCode());
    }

    public String getCode() {
    return code;
    }

    public void setCode(String code) {
    this.code = code;
    }

    public String getMsg() {
    return msg;
    }

    public ApiRespResult setMsg(String msg) {
    this.msg = msg;
    return this;
    }

    public Object getExtendData() {
    return extendData;
    }

    public ApiRespResult setExtendData(Object extendData) {
    this.extendData = extendData;
    return this;
    }

    public T getData() {
    return data;
    }

    public ApiRespResult setData(T data) {
    this.data = data;
    return this;
    }

    @Override
    public String toString() {
    return “[code=” + code + “, msg=” + msg + “, extendData=” + extendData + “, data=” + data + “]”;
    }
    }


主要封装了结果码, 结果信息, 数据对象和扩展信息, 返回数据示例:

{
“code”: “SYS_200”,
“msg”: “成功”,
“extendData”: null,
“data”: {
“id”: null,
“userNo”: “admin”,
“name”: “admin”,
“userPwd”: “ISMvKXpXpadDiUoOSoAfww==”,
“phone”: “123”,
“companyId”: 1,
“email”: null,
“address”: null,
“lastLoginIp”: null,
“lastLoginTime”: null,
“status”: 0,
“craeteTime”: null
},
“success”: true
}


#### 3.4 核心类实现说明

- 用户服务, 添加pom依赖

org.springframework.boot spring-boot-starter-web
      

      
      



          com.alibaba.cloud



          spring-cloud-starter-alibaba-nacos-discovery



      



      



      



          org.springframework.boot



          spring-boot-starter-actuator



      



      



      



          com.itcast.trade



          bulls-stock-common-dao



          ${project.stock.version}



      







      



      



          com.itcast.trade



          bulls-stock-entity



          ${project.stock.version}



      



      



      



          com.itcast.trade



          bulls-stock-common-web



          ${project.stock.version}



      







      



      



          com.itcast.trade



          bulls-stock-common-utils



          ${project.stock.version}



      







      



      



          org.springframework.cloud



          spring-cloud-starter-openfeign



          ${spring-cloud-starter.version}



      



      



          org.springframework.cloud



          spring-cloud-openfeign-core



          ${spring-cloud-starter.version}



      







      



      



          com.netflix.archaius



          archaius-core



          0.7.6



      







      



      



          org.springframework.cloud



          spring-cloud-starter-oauth2



          ${spring-cloud-starter.version}



      



      



          org.springframework.cloud



          spring-cloud-starter-security



          ${spring-cloud-starter.version}



      



      



          org.springframework.security.oauth.boot



          spring-security-oauth2-autoconfigure



          ${spring-cloud-starter.version}



      



  

common-dao工程下面的POM依赖:

  org.mybatis.spring.boot



  mybatis-spring-boot-starter



  2.1.0
  com.alibaba



  druid-spring-boot-starter



  ${druid.version}
  org.slf4j



  slf4j-log4j12
  mysql



  mysql-connector-java



  8.0.14

 

- 启动类StockUserApplication

@SpringBootApplication

@EnableDiscoveryClient

@EnableFeignClients

@ComponentScan(basePackages = {“com.itcast”})

@MapperScan(“com.itcast.trade.bulls.stock.user.dao”)

@EnableTransactionManagement

public class StockUserApplication {

  public static void main(String[] args) {







      SpringApplication.run(StockUserApplication.class, args);



  }

}


MapperScan是Mybatis注解,配置扫描路径

EnableTransactionManagement注解是开启事务支持

- 数据层

- 用户服务数据接口IStockUserDao

  ```
  @Repository
  

  
  public interface IStockUserDao {
  

  
  
  

  
  
  

  
      /**
  

  
       * 根据用户账号获取用户对象
  

  
       * @param userNo
  

  
       * @return
  

  
       */
  

  
      TradeUser getByUserNo(String userNo);
  

  
  
  

  
  }
  ```

- Mapper定义StockUserMapper.xml

  
  
  

  
  
  

  
  
  

  
      
  

  
          
  

  
          
  

  
          
  

  
          
  

  
          
  

  
          
  

  
          
  

  
          
  

  
          
  

  
          
  

  
          
  

  
          
  

  
      
  

  
  
  

  
      
  

  
  
  

  
  
  

  
  
  ```

- 服务层StockUserServiceImpl

@Service

@Log4j2

public class StockUserServiceImpl implements IStockUserService {

  @Autowired



  private IStockUserDao stockUserDao;







  /**



   * 用户登陆



   * @param userNo



   * @param userPwd



   * @return



   */



  public TradeUser userLogin(String userNo, String userPwd) throws ComponentException {







      // 获取用户对象



      TradeUser tradeUser= stockUserDao.getByUserNo(userNo);



      if(null == tradeUser) {



          throw new ComponentException(ApplicationErrorCodeEnum.USER_NOT_FOUND);



      }







      // 用户密码加密判断



      String encryptPassword = EncryptUtil.encryptSigned(userPwd);



      boolean pwdMatch= tradeUser.getUserPwd().equals(encryptPassword);



      if(!pwdMatch) {



          log.error(ApplicationErrorCodeEnum.USER_PWD_ERROR);



          throw new ComponentException(ApplicationErrorCodeEnum.USER_PWD_ERROR);



      }







      return tradeUser;



  }

}


用户登陆接口实现逻辑。

- 接入层StockUserController

@RestController()

@RequestMapping(“/user”)

@Log4j2

public class StockUserController {

  @Autowired



  private IStockUserService stockUserService;







  /**



   * 用户登陆接口



   * @param userNo



   * @param userPwd



   * @return



   */



  @RequestMapping("/userLogin")



  public ApiRespResult userLogin(@RequestParam("userNo")String userNo, @RequestParam("userPwd") String userPwd) {







      ApiRespResult  result = null;



      try {



          // 用户登陆逻辑处理



          TradeUser tradeUser = stockUserService.userLogin(userNo, userPwd);



          result = ApiRespResult.success(tradeUser);



      }catch(ComponentException e) {



          log.error(e.getMessage(), e);



          result = ApiRespResult.error(e.geterrorCodeEnum());



      }catch(Exception e) {



          log.error(e.getMessage(), e);



          result = ApiRespResult.sysError(e.getMessage());



      }







      return result;







  }

}


- 配置类

认证配置ResourceSecurityConfigurer

@Primary

@Order(90)

@Configuration

@EnableResourceServer

@EnableGlobalMethodSecurity(prePostEnabled = true)

public class ResourceSecurityConfigurer implements ResourceServerConfigurer {

  @Autowired



  protected RemoteTokenServices remoteTokenServices;







  @Autowired



  private RestTemplate lbRestTemplate;







  /**



   * 远程调用,采用restTemplate方式处理



   * @param resourceServerSecurityConfigurer



   * @throws Exception



   */



  @Override



  public void configure(ResourceServerSecurityConfigurer resourceServerSecurityConfigurer) throws Exception {



      remoteTokenServices.setRestTemplate(lbRestTemplate);



      resourceServerSecurityConfigurer.tokenServices(remoteTokenServices);







  }







  /**



   * 资源服务安全配置



   * @param httpSecurity



   * @throws Exception



   */



  @Override



  public void configure(HttpSecurity httpSecurity) throws Exception {







      httpSecurity.csrf().disable()



              .authorizeRequests()



              .antMatchers("/user/**").authenticated().and()



              .formLogin().loginPage("/login")



              .failureUrl("/login?error")



              .defaultSuccessUrl("/home");



  }







  /**



   * RestTemplate配置



   * @return



   */



  @Bean



  @Primary



  @LoadBalanced



  public RestTemplate lbRestTemplate() {



      RestTemplate restTemplate = new RestTemplate();



      restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {



          @Override



          public void handleError(ClientHttpResponse response) throws IOException {



              if (response.getRawStatusCode() != HttpStatus.BAD_REQUEST.value()) {



                  super.handleError(response);



              }



          }



      });



      return restTemplate;



  }

}


用户服务为资源服务, 认证采用RestTemplate调用方式。 资源服务一定要开启@EnableResourceServer注解, @EnableGlobalMethodSecurity为方法级别安全控制。

- 工程配置

配置文件bootstrap.yml

server:

port: 10681

spring:

application:



  name: stock-user



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
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



    access-token-uri: http://trade-auth/oauth/token



    user-authorization-uri: http://trade-auth/oauth/authorize

#mybatis 配置

mybatis:

mapper-locations: classpath:com/itcast/trade/stock/user/dao/mapper/*.xml



#### 3.5 启动与验证

1. 启动Nacos、Redis和trade-auth认证服务

2. 启动用户服务, 端口为10681, 访问登陆接口: 127.0.0.1:10681/user/userLogin?userNo=admin&userPwd=admin

 在没有认证,不传递token的情况下, 返回错误信息:

 ![1563782268240](http://www.kaotop.com/file/tupian/20220426/30ac6f049c9b5e8108ca7f8baa99362a.png)

3. 在Authorization下选择Bearer Token ,  加入access_token信息, 再次请求, 能够成功获取用户数据:

 ![1563782393001](http://www.kaotop.com/file/tupian/20220426/0c4d4e11a1be156ee7ec5d4cd676c198.png)





### 4. 总结

- 完成用户服务的配置的改进以及 Spring Security的集成, 核心类的处理实现; 在整个项目中, 存在公用的地方, 要做统一处理, 比如统一异常和统一接口数据返回, 复用方便, 直观清晰, 便于维护与扩展。



### 第4章 网关服务与Druid监控数据源

### 1. 目标

- 完成网关服务认证转发配置与功能验证
- 实现监控数据源Druid的集成配置

### 2. 步骤

- Druid简介说明
- Druid监控参数配置
- 工程结构说明
- Druid集成配置
- 核心类实现
- 功能验证
- Druid访问验证

### 3. 实现

#### 3.1 Druid简介说明

上面演示了如何使用Druid数据源, 大家应该有了一个大概了解, 这里再扩展一下, 了解下它的功能, 以及监控台的使用。

Druid是一个非常优秀的数据库连接池。在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource。并且Druid在阿里巴巴部署了超过600个应用, 是经过了严苛的生产环境检验, 具有较强的可靠性。

Durid监控台包含数据源配置信息、SQL监控、防火墙信息、URI监控、Session监控和Spring 监控等。



#### 3.2 Druid监控参数配置说明

| 配置                                      | 缺省值             | 说明                                                         |
| :---------------------------------------- | :----------------- | :----------------------------------------------------------- |
| name                                      |                    | 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。如果没有配置,将会生成一个名字,格式是:”DataSource-” + System.identityHashCode(this). 另外配置此属性至少在1.0.5版本中是不起作用的,强行设置name会出错。 |
| url                                       |                    | 连接数据库的url,不同数据库不一样。例如: mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto |
| username                                  |                    | 连接数据库的用户名                                           |
| password                                  |                    | 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。 |
| driverClassName                           | 根据url自动识别    | 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName |
| initialSize                               | 0                  | 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 |
| maxActive                                 | 8                  | 最大连接池数量                                               |
| maxIdle                                   | 8                  | 已经不再使用,配置了也没效果                                 |
| minIdle                                   |                    | 最小连接池数量                                               |
| maxWait                                   |                    | 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 |
| poolPreparedStatements                    | false              | 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 |
| maxPoolPreparedStatementPerConnectionSize | -1                 | 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 |
| validationQuery                           |                    | 用来检测连接是否有效的sql,要求是一个查询语句,常用select ‘x’。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 |
| validationQueryTimeout                    |                    | 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法 |
| testOnBorrow                              | true               | 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 |
| testOnReturn                              | false              | 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 |
| testWhileIdle                             | false              | 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 |
| timeBetweenEvictionRunsMillis             | 1分钟(1.0.14)    | 有两个含义: 1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 |
| numTestsPerEvictionRun                    | 30分钟(1.0.14)   | 不再使用,一个DruidDataSource只支持一个EvictionRun           |
| minEvictableIdleTimeMillis                |                    | 连接保持空闲而不被驱逐的最长时间                             |
| connectionInitSqls                        |                    | 物理连接初始化的时候执行的sql                                |
| exceptionSorter                           | 根据dbType自动识别 | 当数据库抛出一些不可恢复的异常时,抛弃连接                   |
| filters                                   |                    | 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat 日志用的filter:log4j 防御sql注入的filter:wall |
| proxyFilters                              |                    | 类型是List,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系 |









#### 3.3 工程结构

上面搭建了认证服务和用户资源服务, 能够成功使用OAUTH2进行认证,作为微服务, 需要由网关进行统一转发处理, 接下来我们改进网关服务, 通过路由转发支持OAUTH2的使用。

![1563782733532](http://www.kaotop.com/file/tupian/20220426/95f8c89ddeb011f4987f807625f13a56.png) 经过实践摸索,如果网关只是转发,按照我们OAUTH2的设计方案,  Spring Cloud Gateway 可以不用集成Spring Security。网关的职责就是接收客户端的请求并进行转发, 所以鉴权可以不用放置在网关, 各微服务直接作为资源服务进行认证,也可以避免微服务直接对外暴露产生的安全问题, 在这里学习如何通过Gateway转发请求, 实现OAuth2的认证。

备注: 工程当中存有摸索实践的代码与配置, 已经注释,可以忽略, 如后续扩展, 可以参考。



#### 3.4 Druid集成配置

用户服务工程集成Druid监控台配置, bootstrap.yml

spring:

数据源配置, 采用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


主要是stat-view-servlet下面配置:

url-pattern是配置监控台的访问地址。

allow是允许哪些IP进行访问。

deny是IP黑名单, 优先级高于allow。

reset-enable是复位功能, 谨慎使用。

login-username是登陆了用户名。

login-password是登陆密码。

enabled是监控台的启用开关, 此项一定要开启。

#### 3.5 核心类实现说明

- 启动类StockGatewayApplication

@SpringBootApplication

@EnableDiscoveryClient

@ComponentScan(basePackages = {“com.itcast”})

public class StockGatewayApplication {

  public static void main(String[] args) {



      SpringApplication.run(StockGatewayApplication.class, args);



  }

}

  • 全局过滤器StockRequestGlobalFilter

    @Component

    @Log4j2

    public class StockRequestGlobalFilter implements GlobalFilter, Ordered {

    /**
    
    
    
     * 通过filter来自定义配置转发信息
    
    
    
     * @param exchange
    
    
    
     * @param chain
    
    
    
     * @return
    
    
    
     */
    
    
    
    @Override
    
    
    
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
    
        String authentication = exchange.getRequest().getHeaders().getFirst("Authorization");
    
    
    
        if(!StringUtil.isNullOrEmpty(authentication)){
    
    
    
            log.info("enter stockRequestGlobalFilter filter method: " + authentication);
    
    
    
            exchange.getRequest().mutate().header("Authorization",authentication);
    
    
    
        }
    
    
    
        return chain.filter(exchange.mutate().build());
    
    
    
    }
    
    
    
    
    
    
    
    @Override
    
    
    
    public int getOrder() {
    
    
    
        return -1000;
    
    
    
    }
    

    }

    
    这是自定义全局过滤器的实现, 防止header中的Authorization没有转发的问题。
    
    
  • 工程配置application.yml

    server:
    
    
    
      port: 10680
    
    
    
    
    
    
    
    spring:
    
    
    
      application:
    
    
    
        name: stock-gateway
    
    
    
      cloud:
    
    
    
        nacos:
    
    
    
          discovery:
    
    
    
            server-addr: 127.0.0.1:8848
    
    
    
            service: stock-gateway
    
    
    
        gateway:
    
    
    
          discovery:
    
    
    
            # 允许通过服务名称进行路由转发访问, http://service-id/user 
    
    
    
            locator:
    
    
    
              enabled: true
    
    
    
          # 路由配置
    
    
    
          routes:
    
    
    
          - id: stock-user
    
    
    
            uri: lb://stock-user
    
    
    
            predicates:
    
    
    
            - Path=/user/**
    
    
    
          - id: trade-auth
    
    
    
            uri: lb://trade-auth
    
    
    
            predicates:
    
    
    
            # - Method=GET,POST  不要开启此项
    
    
    
            - Path=/oauth/**
    
    
    
    logging:
    
    
    
      level:
    
    
    
        root: info
    
    
    
    
    
    
    
    
    

    我们定义了两个路由, 一个是stock-user,映射路径为/user开头, 转发至用户服务; 另一个是 trade-auth, 映射路径为/oauth, 转发至认证服务。
    注意不要开启Method=GET,POST, 默认允许所有请求方式, 认证会用到。

3.6 启动与验证
  1. 启动Gateway网关服务, 端口为10680。

  2. 通过网关申请token, 访问地址: 127.0.0.1:10680/oauth/token

    与请求认证服务一样, 输入token申请参数,可以看到, 通过网关访问, 能够成功返回token数据。

  3. 通过网关访问用户资源服务, 请求登陆接口: 127.0.0.1:10680/user/userLogin

    增加Bearer Token, 通过网关请求用户服务, 也能够成功返回登陆数据。

    通过以上验证, 网关能够正常路由转发, 对外我们只需提供一个地址, 客户端即可访问认证服务与受保护的资源服务。

3.7 Druid访问验证

启动服务即可访问, 把用户服务启动, 访问地址: http://127.0.0.1:10681/druid/index.html

成功启动, 可以看到监控台信息, 这个在生产环境中, 如果遇到问题时, 可以开启, 帮助我们定位分析线上的故障。

4. 总结
  • 学会 Spring Cloud Gateway的集成与配置, 通过Gateway代理OAUTH2认证服务, 申请TOKEN, 并请求资源服务,对外统一地址服务, 通过路径映射。

  • 了解Durid连接池的作用, 掌握集成配置, 通过Druid控制后台, 实时监控查看数据库连接状况, 根据提供的信息, 能够尽快定位和排查线上的问题。

FAQ

实践过程当中会碰到各种问题, 这里例举一些, 避免大家碰到类似情况。

  1. 为什么认证报出401错误?

    首先检查数据库脚本是否完整, t_oauth_client_details表的初始化数据是否正确;

    其次检查用户名和加密密码是否匹配, 可以通过debug跟踪查看, 是不是字符编码等问题引起。

  2. 为什么用户名和密码输入正确, 仍不能申请token?

    需要在认证服务的配置中开启allowFormAuthenticationForClients,允许进行form请求认证;

    可以在redis中清除client_detail缓存,一般是oauth开头的键值, 不能找到就执行flushall全部清除。

  3. 为什么资源服务通过token调用, 会出现超时情况?

    检查认证服务是否在nacos成功注册;

    检查资源服务的security配置中, loadbalance是否开启;

    检查资源服务的安全配置,是否定义了RestTemplate, 且RemoteTokenServices需要注入RestTemplate。

  4. Gateway网关调用认证服务, GET请求正常, POST请求失败, 出现404错误?

    在网关工程的配置中, 检查predicates下的Method配置, 要允许所有请求类型, 可以将此项配置去除, 默认是允许所有请求类型。

其他资料参考:

oauth2 授权流程:

http://terasolunaorg.github.io/guideline/5.3.0.RELEASE/en/Security/OAuth.html

10分钟理解OAUTH2协议 https://deepzz.com/post/what-is-oauth2-protocol.html

Spring Boot OAuth2.0密码模式服务器实现

https://blog.csdn.net/qq_34873338/article/details/80218212

Redis plus 客户端工具:

https://pan.baidu.com/s/1ETwWnEj4rbsE1S3GlYHlWg#list/path=%2F

配置监控统计拦截的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


主要是stat-view-servlet下面配置:

url-pattern是配置监控台的访问地址。

allow是允许哪些IP进行访问。

deny是IP黑名单, 优先级高于allow。

reset-enable是复位功能, 谨慎使用。

login-username是登陆了用户名。

login-password是登陆密码。

enabled是监控台的启用开关, 此项一定要开启。

#### 3.5 核心类实现说明

- 启动类StockGatewayApplication

@SpringBootApplication

@EnableDiscoveryClient

@ComponentScan(basePackages = {“com.itcast”})

public class StockGatewayApplication {

  public static void main(String[] args) {



      SpringApplication.run(StockGatewayApplication.class, args);



  }

}


- 全局过滤器StockRequestGlobalFilter

@Component

@Log4j2

public class StockRequestGlobalFilter implements GlobalFilter, Ordered {

  /**



   * 通过filter来自定义配置转发信息



   * @param exchange



   * @param chain



   * @return



   */



  @Override



  public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {



      String authentication = exchange.getRequest().getHeaders().getFirst("Authorization");



      if(!StringUtil.isNullOrEmpty(authentication)){



          log.info("enter stockRequestGlobalFilter filter method: " + authentication);



          exchange.getRequest().mutate().header("Authorization",authentication);



      }



      return chain.filter(exchange.mutate().build());



  }







  @Override



  public int getOrder() {



      return -1000;



  }

}


这是自定义全局过滤器的实现, 防止header中的Authorization没有转发的问题。

- 工程配置application.yml

server:

port: 10680

spring:

application:



  name: stock-gateway



cloud:



  nacos:



    discovery:



      server-addr: 127.0.0.1:8848



      service: stock-gateway



  gateway:



    discovery:



      # 允许通过服务名称进行路由转发访问, http://service-id/user 



      locator:



        enabled: true



    # 路由配置



    routes:



    - id: stock-user



      uri: lb://stock-user



      predicates:



      - Path=/user/**



    - id: trade-auth



      uri: lb://trade-auth



      predicates:



      # - Method=GET,POST  不要开启此项



      - Path=/oauth/**

logging:

level:



  root: info

我们定义了两个路由, 一个是stock-user,映射路径为/user开头, 转发至用户服务; 另一个是  trade-auth, 映射路径为/oauth,  转发至认证服务。
注意不要开启Method=GET,POST, 默认允许所有请求方式, 认证会用到。

#### 3.6 启动与验证

1. 启动Gateway网关服务, 端口为10680。 

2. 通过网关申请token, 访问地址: 127.0.0.1:10680/oauth/token 

 [外链图片转存中...(img-chIKGSEz-1650684970230)]

 与请求认证服务一样, 输入token申请参数,可以看到,  通过网关访问, 能够成功返回token数据。

3. 通过网关访问用户资源服务, 请求登陆接口: 127.0.0.1:10680/user/userLogin

 [外链图片转存中...(img-fzf9ORQj-1650684970231)]

 增加Bearer Token, 通过网关请求用户服务, 也能够成功返回登陆数据。

 通过以上验证, 网关能够正常路由转发, 对外我们只需提供一个地址, 客户端即可访问认证服务与受保护的资源服务。







#### 3.7 Druid访问验证

启动服务即可访问, 把用户服务启动, 访问地址: http://127.0.0.1:10681/druid/index.html

[外链图片转存中...(img-CYfCr93c-1650684970232)]



成功启动, 可以看到监控台信息, 这个在生产环境中, 如果遇到问题时, 可以开启, 帮助我们定位分析线上的故障。

### 4. 总结

- 学会 Spring Cloud Gateway的集成与配置, 通过Gateway代理OAUTH2认证服务, 申请TOKEN, 并请求资源服务,对外统一地址服务, 通过路径映射。 

- 了解Durid连接池的作用, 掌握集成配置, 通过Druid控制后台, 实时监控查看数据库连接状况, 根据提供的信息, 能够尽快定位和排查线上的问题。

 

### FAQ

实践过程当中会碰到各种问题, 这里例举一些, 避免大家碰到类似情况。

1. 为什么认证报出401错误?

 首先检查数据库脚本是否完整, t_oauth_client_details表的初始化数据是否正确;

 其次检查用户名和加密密码是否匹配, 可以通过debug跟踪查看, 是不是字符编码等问题引起。

2. 为什么用户名和密码输入正确, 仍不能申请token?

 需要在认证服务的配置中开启allowFormAuthenticationForClients,允许进行form请求认证;

 可以在redis中清除client_detail缓存,一般是oauth开头的键值,  不能找到就执行flushall全部清除。

3. 为什么资源服务通过token调用, 会出现超时情况? 

 检查认证服务是否在nacos成功注册;

 检查资源服务的security配置中, loadbalance是否开启;

 检查资源服务的安全配置,是否定义了RestTemplate, 且RemoteTokenServices需要注入RestTemplate。

4. Gateway网关调用认证服务, GET请求正常, POST请求失败, 出现404错误?

 在网关工程的配置中, 检查predicates下的Method配置, 要允许所有请求类型, 可以将此项配置去除, 默认是允许所有请求类型。



其他资料参考:

oauth2 授权流程:

http://terasolunaorg.github.io/guideline/5.3.0.RELEASE/en/Security/OAuth.html

10分钟理解OAUTH2协议 https://deepzz.com/post/what-is-oauth2-protocol.html

Spring Boot OAuth2.0密码模式服务器实现

https://blog.csdn.net/qq_34873338/article/details/80218212

Redis plus 客户端工具:

https://pan.baidu.com/s/1ETwWnEj4rbsE1S3GlYHlWg#list/path=%2F


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/734024.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-27
下一篇 2022-04-27

发表评论

登录后才能评论

评论列表(0条)

保存