当前位置:   article > 正文

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

springboot auth2

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基本资料, 头像等信息。

授权请求:

  1. response_type=code // 必选项
  2. &client_id={客户端的ID} // 必选项
  3. &redirect_uri={重定向URI} // 可选项
  4. &scope={申请的权限范围} // 可选项
  5. &state={任意值} // 可选项

授权响应参数:

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

令牌请求:

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

令牌响应:

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

3.8 简化模式

  1. +----------+
  2. | Resource |
  3. | Owner |
  4. | |
  5. +----------+
  6. ^
  7. |
  8. (B)
  9. +----|-----+ Client Identifier +---------------+
  10. | -+----(A)-- & Redirection URI --->| |
  11. | User- | | Authorization |
  12. | Agent -|----(B)-- User authenticates -->| Server |
  13. | | | |
  14. | |<---(C)--- Redirection URI ----<| |
  15. | | with Access Token +---------------+
  16. | | in Fragment
  17. | | +---------------+
  18. | |----(D)--- Redirection URI ---->| Web-Hosted |
  19. | | without Fragment | Client |
  20. | | | Resource |
  21. | (F) |<---(E)------- Script ---------<| |
  22. | | +---------------+
  23. +-|--------+
  24. | |
  25. (A) (G) Access Token
  26. | |
  27. ^ v
  28. +---------+
  29. | |
  30. | Client |
  31. | |
  32. +---------+

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

授权请求:

  1. response_type=token // 必选项
  2. &client_id={客户端的ID} // 必选项
  3. &redirect_uri={重定向URI} // 可选项
  4. &scope={申请的权限范围} // 可选项
  5. &state={任意值} // 可选项

授权响应参数:

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

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

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

3.9 密码模式

  1. +----------+
  2. | Resource |
  3. | Owner |
  4. | |
  5. +----------+
  6. v
  7. | Resource Owner
  8. (A) Password Credentials
  9. |
  10. v
  11. +---------+ +---------------+
  12. | |>--(B)---- Resource Owner ------->| |
  13. | | Password Credentials | Authorization |
  14. | Client | | Server |
  15. | |<--(C)---- Access Token ---------<| |
  16. | | (w/ Optional Refresh Token) | |
  17. +---------+ +---------------+

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

令牌请求:

  1. grant_type=password // 必填
  2. &username={用户ID} // 必填
  3. &password={密码} // 必填
  4. &scope={授权范围} // 任意

令牌响应:

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

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

3.10 客户端模式

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

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

令牌请求:

  1. grant_type=client_credentials // 必填
  2. client_id={客户端的ID} // 必填
  3. client_secret={客户端的密钥} // 必填
  4. &scope={授权范围} // 任意

令牌响应:

  1. "access_token":"{访问令牌}", // 必填
  2. "token_type":"{令牌类型}", // 必填
  3. "expires_in":"{过期时间}", // 任意
  4. "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. 工程依赖

    父级工程依赖属性配置:

    1. <properties>
    2. ...
    3. <spring-cloud-starter.version>2.1.1.RELEASE</spring-cloud-starter.version>
    4. <druid.version>1.1.18</druid.version>
    5. <project.stock.version>1.0.0</project.stock.version>
    6. </properties>

    POM.XML文件:

    1. <dependencies>
    2. <!-- OATUH2 的核心组件 -->
    3. <dependency>
    4. <groupId>org.springframework.cloud</groupId>
    5. <artifactId>spring-cloud-starter-oauth2</artifactId>
    6. <version>${spring-cloud-starter.version}</version>
    7. </dependency>
    8. <!-- Spring Security 依赖 -->
    9. <dependency>
    10. <groupId>org.springframework.cloud</groupId>
    11. <artifactId>spring-cloud-starter-security</artifactId>
    12. <version>${spring-cloud-starter.version}</version>
    13. </dependency>
    14. <!-- 自动化缓存依赖 -->
    15. <dependency>
    16. <groupId>org.springframework.boot</groupId>
    17. <artifactId>spring-boot-starter-cache</artifactId>
    18. <version>${spring-cloud-starter.version}</version>
    19. </dependency>
    20. <!--缓存依赖-->
    21. <dependency>
    22. <groupId>org.springframework.boot</groupId>
    23. <artifactId>spring-boot-starter-data-redis</artifactId>
    24. <version>${spring-cloud-starter.version}</version>
    25. </dependency>
    26. <!-- jpa -->
    27. <dependency>
    28. <groupId>org.springframework.boot</groupId>
    29. <artifactId>spring-boot-starter-data-jpa</artifactId>
    30. <version>${spring-cloud-starter.version}</version>
    31. </dependency>
    32. <!-- Nacos服务注册发现依赖 -->
    33. <dependency>
    34. <groupId>com.alibaba.cloud</groupId>
    35. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    36. </dependency>
    37. <!-- 公用utils工具的依赖 -->
    38. <dependency>
    39. <groupId>com.itcast.trade</groupId>
    40. <artifactId>bulls-stock-common-utils</artifactId>
    41. <version>${project.stock.version}</version>
    42. </dependency>
    43. <!-- 公用实体的依赖 -->
    44. <dependency>
    45. <groupId>com.itcast.trade</groupId>
    46. <artifactId>bulls-stock-entity</artifactId>
    47. <version>${project.stock.version}</version>
    48. </dependency>
    49. <!-- druid 连接池依赖 -->
    50. <dependency>
    51. <groupId>com.alibaba</groupId>
    52. <artifactId>druid-spring-boot-starter</artifactId>
    53. <version>${druid.version}</version>
    54. </dependency>
    55. <!-- 解决druid log4j监控组件无法使用问题, 不用再额外增加其他LOG框架 -->
    56. <dependency>
    57. <groupId>org.slf4j</groupId>
    58. <artifactId>slf4j-log4j12</artifactId>
    59. </dependency>
    60. <!-- mysql-connector-java -->
    61. <dependency>
    62. <groupId>mysql</groupId>
    63. <artifactId>mysql-connector-java</artifactId>
    64. <version>8.0.14</version>
    65. </dependency>
    66. <!--freemarker-->
    67. <dependency>
    68. <groupId>org.springframework.boot</groupId>
    69. <artifactId>spring-boot-starter-freemarker</artifactId>
    70. </dependency>
    71. <!--web 模块-->
    72. <dependency>
    73. <groupId>org.springframework.boot</groupId>
    74. <artifactId>spring-boot-starter-web</artifactId>
    75. </dependency>
    76. <!--tomcat容器, javax servelet相关依赖-->
    77. <dependency>
    78. <groupId>org.springframework.boot</groupId>
    79. <artifactId>spring-boot-starter-tomcat</artifactId>
    80. </dependency>
    81. </dependencies>
    82. <!-- 打包配置 -->
    83. <build>
    84. <finalName>trade-auth</finalName>
    85. <plugins>
    86. <plugin>
    87. <groupId>org.apache.maven.plugins</groupId>
    88. <artifactId>maven-resources-plugin</artifactId>
    89. <executions>
    90. <execution>
    91. <id>default-resources</id>
    92. <phase>validate</phase>
    93. <goals>
    94. <goal>copy-resources</goal>
    95. </goals>
    96. <configuration>
    97. <outputDirectory>target/classes</outputDirectory>
    98. <useDefaultDelimiters>false</useDefaultDelimiters>
    99. <delimiters>
    100. <delimiter>${*}</delimiter>
    101. </delimiters>
    102. <resources>
    103. <resource>
    104. <directory>src/main/resources/</directory>
    105. <filtering>true</filtering>
    106. </resource>
    107. <resource>
    108. <directory>src/main/java</directory>
    109. <includes>
    110. <include>**/*.xml</include>
    111. </includes>
    112. <filtering>false</filtering>
    113. </resource>
    114. </resources>
    115. </configuration>
    116. </execution>
    117. </executions>
    118. </plugin>
    119. <plugin>
    120. <groupId>org.springframework.boot</groupId>
    121. <artifactId>spring-boot-maven-plugin</artifactId>
    122. <executions>
    123. <execution>
    124. <goals>
    125. <goal>repackage</goal>
    126. </goals>
    127. </execution>
    128. </executions>
    129. </plugin>
    130. </plugins>
    131. </build>

  4. 启动类(TradeAuthApplication)

    1. @SpringBootApplication
    2. @EnableDiscoveryClient
    3. @ComponentScan(basePackages = {"com.itcast"})
    4. @EntityScan(basePackages = {"com.itcast"})
    5. @EnableJpaRepositories(basePackages = {"com.itcast"})
    6. @EnableCaching
    7. public class TradeAuthApplication {
    8. public static void main(String[] args) {
    9. SpringApplication.run(TradeAuthApplication.class, args);
    10. }
    11. }

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

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

  5. 实体

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

    1. @Data
    2. @Entity
    3. @Table(name = "t_trade_user")
    4. public class TradeUser extends BaseEntity {
    5. @Id
    6. private Long id;
    7. /**
    8. * 用户编号
    9. */
    10. private String userNo;
    11. /**
    12. * 用户名称
    13. */
    14. private String name;
    15. /**
    16. * 用户密码
    17. */
    18. private String userPwd;
    19. /**
    20. * 电话号码
    21. */
    22. private String phone;
    23. /**
    24. * 公司ID
    25. */
    26. private Long companyId;
    27. /**
    28. * 邮箱
    29. */
    30. private String email;
    31. /**
    32. * 地址
    33. */
    34. private String address;
    35. /**
    36. * 最近一次用户登陆IP
    37. */
    38. private String lastLoginIp;
    39. /**
    40. * 最近一次登陆时间
    41. */
    42. private Date lastLoginTime;
    43. /**
    44. * 状态(0:有效, 1:锁定, 2:禁用)
    45. */
    46. private int status;
    47. /**
    48. * 创建时间
    49. */
    50. private Date craeteTime;
    51. }

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

  6. 数据层

    用户信息数据接口TradeUserRepository

    1. /**
    2. * 用户信息数据层接口
    3. */
    4. @Repository("tradeUserRepository")
    5. public interface TradeUserRepository extends PagingAndSortingRepository<TradeUser, String>, JpaSpecificationExecutor<TradeUser> {
    6. /**
    7. * 根据用户账号获取用户对象
    8. * @param userNo
    9. * @return
    10. */
    11. public TradeUser findByUserNo(String userNo);
    12. }

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

  7. 服务层

    • 用户信息服务接口AuthStockUserDetailServiceImpl

      1. @Service("authStockUserDetailService")
      2. public class AuthStockUserDetailServiceImpl implements UserDetailsService {
      3. @Autowired
      4. private TradeUserRepository tradeUserRepository;
      5. @Autowired
      6. private CacheManager cacheManager;
      7. @Override
      8. public UserDetails loadUserByUsername(String userNo) throws UsernameNotFoundException {
      9. // 查询缓存
      10. Cache cache = cacheManager.getCache(GlobalConstants.OAUTH_KEY_STOCK_USER_DETAILS);
      11. if (cache != null && cache.get(userNo) != null) {
      12. return (UserDetails) cache.get(userNo).get();
      13. }
      14. // 缓存未找到, 查询数据库
      15. TradeUser tradeUser = tradeUserRepository.findByUserNo(userNo);;
      16. if(null == tradeUser){
      17. throw new UsernameNotFoundException(userNo + " not valid !");
      18. }
      19. // 封装成OAUTH鉴权的用户对象
      20. UserDetails userDetails = new OAuthTradeUser(tradeUser);
      21. // 将用户信息放入缓存
      22. cache.put(userNo, userDetails);
      23. return userDetails;
      24. }
      25. }

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

    • OAuthTradeUser用户封装信息:

      1. public class OAuthTradeUser extends User {
      2. private static final long serialVersionUUID = -1L;
      3. /**
      4. * 业务用户信息
      5. */
      6. private TradeUser tradeUser;
      7. public OAuthTradeUser(TradeUser tradeUser) {
      8. // OAUTH2认证用户信息构造处理
      9. super(tradeUser.getUserNo(), tradeUser.getUserPwd(), (tradeUser.getStatus() == 0 ? true : false),
      10. true, true, (tradeUser.getStatus() == 0 ? true : false), Collections.emptyList());
      11. this.tradeUser = tradeUser;
      12. }
      13. }

    • 客户端信息服务接口

      1. public class AuthClientDetailService extends JdbcClientDetailsService {
      2. public AuthClientDetailService(DataSource dataSource) {
      3. super(dataSource);
      4. }
      5. /**
      6. * 重写原生方法支持redis缓存
      7. *
      8. * @param clientId
      9. * @return
      10. * @throws InvalidClientException
      11. */
      12. @Override
      13. @Cacheable(value = GlobalConstants.OAUTH_KEY_CLIENT_DETAILS, key = "#clientId", unless = "#result == null")
      14. public ClientDetails loadClientByClientId(String clientId) {
      15. return super.loadClientByClientId(clientId);
      16. }
      17. }

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

  8. 编写Web接口

    TradeStockTokenController, Token信息接口:

    1. @RestController
    2. @RequestMapping("/token")
    3. @Log4j2
    4. public class TradeStockTokenController {
    5. private static final String STOCK_OAUTH_ACCESS = GlobalConstants.OAUTH_PREFIX_KEY;
    6. @Autowired
    7. private RedisTemplate stockRedisTemplate;
    8. @Autowired
    9. private TokenStore tokenStore;
    10. @Autowired
    11. private CacheManager cacheManager;
    12. /**
    13. * 认证页面
    14. *
    15. * @return ModelAndView
    16. */
    17. @RequestMapping("/login")
    18. public ModelAndView require() {
    19. return new ModelAndView("ftl/login");
    20. }
    21. /**
    22. * 认证页面
    23. *
    24. * @return ModelAndView
    25. */
    26. @RequestMapping("/success")
    27. public String success() {
    28. log.info("token login success!");
    29. return "login success";
    30. }
    31. /**
    32. * 退出token
    33. *
    34. * @param authHeader Authorization
    35. */
    36. @DeleteMapping("/logout")
    37. public String logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) {
    38. if (StringUtils.isEmpty(authHeader)) {
    39. return "退出失败,token 为空";
    40. }
    41. String tokenValue = authHeader.replace(OAuth2AccessToken.BEARER_TYPE, "").trim();
    42. OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
    43. if (accessToken == null || StringUtils.isEmpty(accessToken.getValue())) {
    44. return "退出失败,token 无效";
    45. }
    46. OAuth2Authentication auth2Authentication = tokenStore.readAuthentication(accessToken);
    47. cacheManager.getCache(GlobalConstants.OAUTH_KEY_STOCK_USER_DETAILS).evict(auth2Authentication.getName());
    48. tokenStore.removeAccessToken(accessToken);
    49. return "退出成功, token 已清除";
    50. }
    51. /**
    52. * 令牌管理调用
    53. *
    54. * @param token token
    55. * @return
    56. */
    57. @DeleteMapping("/{token}")
    58. public String delToken(@PathVariable("token") String token) {
    59. OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token);
    60. tokenStore.removeAccessToken(oAuth2AccessToken);
    61. return "token 已清除";
    62. }
    63. }

    TradeUserController用户接口:

    1. @RestController
    2. @RequestMapping("/trade")
    3. public class TradeUserController {
    4. @Autowired
    5. private UserDetailsService authStockUserDetailService;
    6. /**
    7. * 获取用户信息
    8. * @param username
    9. * @return
    10. */
    11. @RequestMapping("/user")
    12. @ResponseBody
    13. public UserDetails getUser(@RequestParam("username")String username) {
    14. UserDetails userDetails = authStockUserDetailService.loadUserByUsername(username);
    15. return userDetails;
    16. }
    17. }

  9. 配置类

    • 认证服务配置AuthorizationServerConfig,限于篇幅,贴核心代码:
    1. /**
    2. * Redis 缓存配置
    3. * @return
    4. */
    5. @Bean
    6. public RedisTemplate<String, Object> stockRedisTemplate() {
    7. RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    8. redisTemplate.setKeySerializer(new StringRedisSerializer());
    9. redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    10. redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
    11. redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
    12. redisTemplate.setConnectionFactory(redisConnectionFactory);
    13. return redisTemplate;
    14. }
    15. /**
    16. * 自定义Client查询,可以修改表名, 字段等
    17. * @param clients
    18. */
    19. @Override
    20. @SneakyThrows
    21. public void configure(ClientDetailsServiceConfigurer clients) {
    22. AuthClientDetailService clientDetailsService = new AuthClientDetailService(dataSource);
    23. clientDetailsService.setSelectClientDetailsSql(DEFAULT_SELECT_STATEMENT);
    24. clientDetailsService.setFindClientDetailsSql(DEFAULT_FIND_STATEMENT);
    25. clients.withClientDetails(clientDetailsService);
    26. }
    27. /**
    28. * t_oauth_client_details 表的字段,不包括client_id、client_secret
    29. */
    30. String CLIENT_FIELDS = "client_id, client_secret, resource_ids, scope, "
    31. + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "
    32. + "refresh_token_validity, additional_information, autoapprove";
    33. /**
    34. * JdbcClientDetailsService 查询语句
    35. */
    36. String BASE_FIND_STATEMENT = "select " + CLIENT_FIELDS
    37. + " from t_oauth_client_details";
    38. /**
    39. * 默认的查询语句
    40. */
    41. String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + " order by client_id";
    42. /**
    43. * 按条件client_id 查询
    44. */
    45. String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?";
    46. /**
    47. * 防止申请token时出现401错误
    48. * @param oauthServer
    49. */
    50. @Override
    51. public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
    52. oauthServer
    53. .tokenKeyAccess("permitAll()")
    54. .checkTokenAccess("permitAll()")
    55. .allowFormAuthenticationForClients();
    56. }
    57. /**
    58. * 认证服务配置
    59. * @param endpoints
    60. */
    61. @Override
    62. public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    63. endpoints
    64. .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
    65. .tokenStore(tokenStore())
    66. .tokenEnhancer(tokenEnhancer())
    67. .userDetailsService(authStockUserDetailService)
    68. .authenticationManager(authenticationManager)
    69. .reuseRefreshTokens(false);
    70. }
    71. }

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

    注意要加上@EnableAuthorizationServer注解。

    • Web服务认证配置WebSecurityConfigurer

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

      1. @Primary
      2. @Order(90)
      3. @Configuration
      4. public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
      5. @Autowired
      6. private UserDetailsService authStockUserDetailService;
      7. @Autowired
      8. private StockPasswordEncoder stockPasswordEncoder;
      9. /**
      10. * Web服务认证配置
      11. * @param http
      12. */
      13. @Override
      14. @SneakyThrows
      15. protected void configure(HttpSecurity http) {
      16. http
      17. .formLogin()
      18. .loginPage("/token/login")
      19. .loginProcessingUrl("/token/form")
      20. .defaultSuccessUrl("/token/success")
      21. .and()
      22. .authorizeRequests()
      23. .antMatchers(
      24. "/token/**",
      25. "/actuator/**",
      26. "/druid/**").permitAll()
      27. .anyRequest().authenticated()
      28. .and().csrf().disable();
      29. }

    /**
    * 不拦截静态资源

    • @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);
      }

    }

    1. + 密码加密处理器StockPasswordEncoder
    2. 使用密码加密器后, OAuth内置的Client认证以及用户密码认证都会加密处理。
    3. ```java
    4. @Component
    5. @Log4j2
    6. public class StockPasswordEncoder implements PasswordEncoder {
    7. /**
    8. * 编码处理
    9. * @param rawPassword
    10. * @return
    11. */
    12. @Override
    13. public String encode(CharSequence rawPassword) {
    14. return rawPassword.toString();
    15. }
    16. /**
    17. * 密码校验判断
    18. * @param rawPassword
    19. * @param encodedPassword
    20. * @return
    21. */
    22. @Override
    23. public boolean matches(CharSequence rawPassword, String encodedPassword) {
    24. if(rawPassword != null && rawPassword.length() > 0){
    25. try {
    26. // 这里通过MD5及B64加密
    27. String password = EncryptUtil.encryptSigned(rawPassword.toString());
    28. boolean isMatch= encodedPassword.equals(password);
    29. if(!isMatch) {
    30. log.warn("password 不一致!");
    31. }
    32. return isMatch;
    33. } catch (ComponentException e) {
    34. log.error(e.getMessage(), e);
    35. }
    36. }
    37. return false;
    38. }
    39. }

    • EncryptUtils加密类的实现

      1. public class EncryptUtil {
      2. private static final Logger logger = LoggerFactory.getLogger(EncryptUtil.class);
      3. private final static char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
      4. 'e', 'f' };
      5. public static String BASE64Encrypt;
      6. public final static String MD5ToString(String signed) {
      7. char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
      8. try {
      9. byte[] res = signed.getBytes("UTF-8");
      10. MessageDigest mdTemp = MessageDigest.getInstance("MD5".toUpperCase());
      11. mdTemp.update(res);
      12. byte[] md = mdTemp.digest();
      13. // 把密文转换成十六进制的字符串形式
      14. int j = md.length;
      15. char str[] = new char[j * 2];
      16. int k = 0;
      17. for (int i = 0; i < j; i++) {
      18. byte byte0 = md[i];
      19. str[k++] = hexDigits[byte0 >>> 4 & 0xf];
      20. str[k++] = hexDigits[byte0 & 0xf];
      21. }
      22. return new String(str);
      23. } catch (Exception e) {
      24. logger.error(e.getMessage(), e);
      25. return null;
      26. }
      27. }
      28. public final static byte[] MD5(String str) {
      29. try {
      30. byte[] res = str.getBytes("UTF-8");
      31. MessageDigest mdTemp = MessageDigest.getInstance("MD5".toUpperCase());
      32. mdTemp.update(res);
      33. byte[] hash = mdTemp.digest();
      34. return hash;
      35. } catch (Exception e) {
      36. return null;
      37. }
      38. }
      39. /**
      40. * MD5值计算<p>
      41. * MD5的算法在RFC1321 中定义:
      42. * 在RFC 1321中,给出了Test suite用来检验你的实现是否正确:
      43. * MD5 ("") = d41d8cd98f00b204e9800998ecf8427e
      44. * MD5 ("a") = 0cc175b9c0f1b6a831c399e269772661
      45. * MD5 ("abc") = 900150983cd24fb0d6963f7d28e17f72
      46. * MD5 ("message digest") = f96b697d7cb7938d525a2f31aaf161d0
      47. * MD5 ("abcdefghijklmnopqrstuvwxyz") = c3fcd3d76192e4007dfb496cca67e13b
      48. *
      49. * @param res 源字符串
      50. * @return md5值
      51. */
      52. public final static byte[] MD5EncrtyReutrnhexDigitsByteArray(String str) {
      53. try {
      54. byte[] res = str.getBytes("UTF-8");
      55. MessageDigest mdTemp = MessageDigest.getInstance("MD5".toUpperCase());
      56. mdTemp.update(res);
      57. byte[] hash = mdTemp.digest();
      58. return hash;
      59. } catch (Exception e) {
      60. return null;
      61. }
      62. }
      63. public final static String MD5EncrtyReturnString(String str) {
      64. byte[] b = MD5EncrtyReutrnhexDigitsByteArray(str);
      65. StringBuffer resultSb = new StringBuffer();
      66. for (int i = 0; i < b.length; i++) {
      67. int n = b[i];
      68. if (n < 0)
      69. n = 256 + n;
      70. int d1 = n / 16;
      71. int d2 = n % 16;
      72. resultSb.append(hexDigits[d1]);
      73. resultSb.append(hexDigits[d2]);
      74. }
      75. return resultSb.toString();
      76. }
      77. // 加密后解密
      78. public static String JM(byte[] inStr) {
      79. String newStr = new String(inStr);
      80. char[] a = newStr.toCharArray();
      81. for (int i = 0; i < a.length; i++) {
      82. a[i] = (char) (a[i] ^ 't');
      83. }
      84. String k = new String(a);
      85. return k;
      86. }
      87. /**
      88. * BASE64加密MD5EncrtyReutrnhexDigitsByteArray
      89. *
      90. * @param key
      91. * @return
      92. * @throws Exception
      93. */
      94. public static String BASE64Encrypt(byte[] key) throws ComponentException {
      95. String edata = null;
      96. try {
      97. edata = (new BASE64Encoder()).encodeBuffer(key).trim();
      98. } catch (Exception e) {
      99. throw new ComponentException(e.getMessage() +
      100. "BASE64编码错误!key=" + new String(key) + ", error=" + e.getMessage());
      101. }
      102. return edata;
      103. }
      104. /**
      105. * BASE64解密
      106. *
      107. * @param key
      108. * @return
      109. * @throws Exception
      110. */
      111. public static byte[] BASE64Decrypt(String data) {
      112. if (data == null)
      113. return null;
      114. byte[] edata = null;
      115. try {
      116. edata = (new BASE64Decoder()).decodeBuffer(data);
      117. return edata;
      118. } catch (Exception e) {
      119. logger.error(e.getMessage(), e);
      120. }
      121. return null;
      122. }
      123. /**
      124. * 方法用途: 签名加密<br>
      125. * 实现步骤: <br>
      126. * @param signStr :签名的字符串
      127. * @return
      128. */
      129. public static String encryptSigned(String signed) throws ComponentException {
      130. try {
      131. byte[] md5SignStr = MD5EncrtyReutrnhexDigitsByteArray(signed);
      132. String b64SignStr = BASE64Encrypt(md5SignStr);
      133. return b64SignStr;
      134. }catch(Exception e) {
      135. throw new ComponentException(e.getMessage()+ "BASE64或MD5加密签名错误!signed=" + signed + ", error=" + e.getMessage());
      136. }
      137. }
      138. public static void main(String[] args) throws Exception {
      139. System.out.println(encryptSigned("app"));
      140. System.out.println(encryptSigned("123"));
      141. }
      142. }

  10. 工程配置信息

application.yml配置:

  1. server:
  2. port: 9999
  3. spring:
  4. application:
  5. name: trade-auth
  6. # 配置中心
  7. cloud:
  8. # 注册中心配置
  9. nacos:
  10. discovery:
  11. server-addr: 127.0.0.1:8848
  12. config:
  13. server-addr: 127.0.0.1:8848
  14. # 数据源配置, 采用Druid
  15. datasource:
  16. type: com.alibaba.druid.pool.DruidDataSource
  17. driver-class-name: com.mysql.cj.jdbc.Driver
  18. username: root
  19. password: 654321
  20. url: jdbc:mysql://192.168.19.150:3306/trade_stock?useUnicode=true&characterEncoding=UTF-8&useSSL=false
  21. druid:
  22. # 连接池的配置信息
  23. # 初始化大小,最小,最大
  24. initial-size: 5
  25. min-idle: 5
  26. maxActive: 20
  27. # 配置获取连接等待超时的时间
  28. maxWait: 60000
  29. # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
  30. timeBetweenEvictionRunsMillis: 60000
  31. # 配置一个连接在池中最小生存的时间,单位是毫秒
  32. minEvictableIdleTimeMillis: 300000
  33. validationQuery: SELECT 1 FROM DUAL
  34. testWhileIdle: true
  35. testOnBorrow: false
  36. testOnReturn: false
  37. # 打开PSCache,并且指定每个连接上PSCache的大小
  38. poolPreparedStatements: true
  39. maxPoolPreparedStatementPerConnectionSize: 20
  40. # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
  41. filters: stat,wall,log4j
  42. # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
  43. connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
  44. # 配置DruidStatFilter
  45. web-stat-filter:
  46. enabled: true
  47. url-pattern: "/*"
  48. exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
  49. # 配置DruidStatViewServlet
  50. stat-view-servlet:
  51. url-pattern: "/druid/*"
  52. # IP白名单(没有配置或者为空,则允许所有访问)
  53. allow:
  54. # IP黑名单 (存在共同时,deny优先于allow)
  55. deny:
  56. # 禁用HTML页面上的“Reset All”功能
  57. reset-enable: false
  58. # 登录名
  59. login-username: admin
  60. # 登录密码
  61. login-password: admin123
  62. # 监控后台开关, 开启可通过后台管理查看
  63. enabled: true
  64. # Freemarker模板引擎配置
  65. freemarker:
  66. allow-request-override: false
  67. allow-session-override: false
  68. cache: true
  69. charset: UTF-8
  70. check-template-location: true
  71. content-type: text/html
  72. enabled: true
  73. expose-request-attributes: false
  74. expose-session-attributes: false
  75. expose-spring-macro-helpers: true
  76. prefer-file-system-access: true
  77. suffix: .ftl
  78. template-loader-path: classpath:/templates/
  79. # Spring Boot 的自动化配置, 排除过滤
  80. autoconfigure:
  81. exclude: org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
  82. # Jpa功能配置
  83. jpa:
  84. hibernate:
  85. ddl-auto: none
  86. naming:
  87. # 实际命名, 无转换
  88. physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
  89. show-sql: true
  90. # Redis 缓存配置
  91. redis:
  92. host: 127.0.0.1
  93. password:
  94. port: 6379
  95. ## spring security 配置
  96. security:
  97. oauth2:
  98. resource:
  99. loadBalanced: true
  100. token-info-uri: http://trade-auth/oauth/check_token
  101. client:
  102. client-id: app
  103. client-secret: app
  104. scope: server
  105. # 默认放行url,如果子模块重写这里的配置就会被覆盖
  106. ignore-urls:
  107. - /actuator/**
  108. - /v2/api-docs
  109. - /swagger-ui.html
  110. - /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. 数据库数据初始化:

    1. -- ----------------------------
    2. -- Table structure for t_oauth_client_details
    3. -- ----------------------------
    4. DROP TABLE IF EXISTS `t_oauth_client_details`;
    5. CREATE TABLE `t_oauth_client_details` (
    6. `client_id` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
    7. `resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    8. `client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    9. `scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    10. `authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    11. `web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    12. `authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    13. `access_token_validity` int(11) NULL DEFAULT NULL,
    14. `refresh_token_validity` int(11) NULL DEFAULT NULL,
    15. `additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    16. `autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    17. PRIMARY KEY (`client_id`) USING BTREE
    18. ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    19. 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);
    20. 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表数据

    1. 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是一个错误码的实现类:

  1. public enum ApplicationErrorCodeEnum implements IErrorCodeEnum {
  2. SUCCESS("200", "成功"),
  3. FAILURE("300", "系统异常"),
  4. COMPONENT_LOAD_PROPERTIES_OBJ_HAD_EXIST("000001", "配置文件加载类已经存在" ),
  5. SYS_ERROR_ENCRYPT_SINGED(IErrorCodeEnum.MODULE_SYSTEM, "000002", "签名加密错误"),
  6. USER_NOT_FOUND(IErrorCodeEnum.MODULE_USER, "000003", "用户不存在!"),
  7. USER_PWD_ERROR(IErrorCodeEnum.MODULE_USER, "000004", "用户密码错误!"),
  8. ;
  9. /**
  10. * 业务模块
  11. */
  12. private String module;
  13. /**
  14. * 错误编号
  15. */
  16. private String code;
  17. /**
  18. * 消息
  19. */
  20. private String message;
  21. /**
  22. * 错误级别
  23. */
  24. private WarningLevelEnum warningLevel;
  25. ApplicationErrorCodeEnum(String code, String message, WarningLevelEnum warningLevelEnum) {
  26. this.code = code;
  27. this.message = message;
  28. this.warningLevel = warningLevelEnum;
  29. }
  30. ApplicationErrorCodeEnum(String module, String code, String message, WarningLevelEnum warningLevelEnum) {
  31. this.module = module;
  32. this.code = code;
  33. this.message = message;
  34. this.warningLevel = warningLevelEnum;
  35. }
  36. ApplicationErrorCodeEnum(String module, String code, String message) {
  37. this.module = module;
  38. this.code = code;
  39. this.message = message;
  40. this.warningLevel = WarningLevelEnum.COMMON;;
  41. }
  42. ApplicationErrorCodeEnum(String code, String message) {
  43. this.module = IErrorCodeEnum.MODULE_SYSTEM;
  44. this.code = code;
  45. this.message = message;
  46. this.warningLevel = WarningLevelEnum.COMMON;
  47. }
  48. @Override
  49. public String getCode() {
  50. return IErrorCodeEnum.MODULE_SYSTEM + this.code;
  51. }
  52. @Override
  53. public String getMessage() {
  54. return this.message;
  55. }
  56. @Override
  57. public WarningLevelEnum getLevel() {
  58. return warningLevel;
  59. }
  60. @Override
  61. public String toString() {
  62. return IErrorCodeEnum.MODULE_SYSTEM + this.code + ", " + this.message;
  63. }
  64. }

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

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

3.3 统一接口数据返回说明

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

  1. /**
  2. * 统一API接口数据返回对象
  3. * @param <T>
  4. */
  5. public class ApiRespResult<T> implements Serializable {
  6. private static final long serialVersionUID = -1L;
  7. /**
  8. * 结果码
  9. */
  10. private String code = ApplicationErrorCodeEnum.SUCCESS.getCode();
  11. /**
  12. * 结果信息
  13. */
  14. private String msg = ApplicationErrorCodeEnum.SUCCESS.getMessage();
  15. /**
  16. * 扩展对象(放置分页信息、其他信息等)
  17. */
  18. private Object extendData;
  19. /**
  20. * 返回结果的数据对象
  21. */
  22. private T data;
  23. public ApiRespResult() {
  24. }
  25. public ApiRespResult(String code) {
  26. this.code = code;
  27. }
  28. public ApiRespResult(String code, String message){
  29. this.code = code;
  30. this.msg = message;
  31. }
  32. public ApiRespResult(IErrorCodeEnum errorCodeEnum){
  33. this.code = errorCodeEnum.getCode();
  34. this.msg = errorCodeEnum.getMessage();
  35. }
  36. public static <T> ApiRespResult<T> error(IErrorCodeEnum errorCodeEnum){
  37. return new ApiRespResult<T>(errorCodeEnum);
  38. }
  39. public static <T> ApiRespResult<T> sysError(String exceptionMsg){
  40. ApiRespResult error = new ApiRespResult<T>(ApplicationErrorCodeEnum.FAILURE);
  41. error.setMsg(error.getMsg() + ":" + exceptionMsg);
  42. return error;
  43. }
  44. public static <T> ApiRespResult<T> error(String code, String msg){
  45. return new ApiRespResult<T>(code,msg);
  46. }
  47. public static <T> ApiRespResult<T> error(String code, String msg,T data){
  48. return new ApiRespResult<T>(code,msg).setData(data);
  49. }
  50. public static ApiRespResult<Void> success(){
  51. return success(null);
  52. }
  53. public static <T> ApiRespResult<T> success(T data){
  54. return success(data, null);
  55. }
  56. public static <T> ApiRespResult<T> success(T data, Object extendData ){
  57. return new ApiRespResult<T>().setData(data).setExtendData(extendData);
  58. }
  59. public Boolean isSuccess(){
  60. return ApplicationErrorCodeEnum.SUCCESS.getCode().equals(getCode());
  61. }
  62. public String getCode() {
  63. return code;
  64. }
  65. public void setCode(String code) {
  66. this.code = code;
  67. }
  68. public String getMsg() {
  69. return msg;
  70. }
  71. public ApiRespResult<T> setMsg(String msg) {
  72. this.msg = msg;
  73. return this;
  74. }
  75. public Object getExtendData() {
  76. return extendData;
  77. }
  78. public ApiRespResult<T> setExtendData(Object extendData) {
  79. this.extendData = extendData;
  80. return this;
  81. }
  82. public T getData() {
  83. return data;
  84. }
  85. public ApiRespResult<T> setData(T data) {
  86. this.data = data;
  87. return this;
  88. }
  89. @Override
  90. public String toString() {
  91. return "[code=" + code + ", msg=" + msg + ", extendData=" + extendData + ", data=" + data + "]";
  92. }
  93. }

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

  1. {
  2. "code": "SYS_200",
  3. "msg": "成功",
  4. "extendData": null,
  5. "data": {
  6. "id": null,
  7. "userNo": "admin",
  8. "name": "admin",
  9. "userPwd": "ISMvKXpXpadDiUoOSoAfww==",
  10. "phone": "123",
  11. "companyId": 1,
  12. "email": null,
  13. "address": null,
  14. "lastLoginIp": null,
  15. "lastLoginTime": null,
  16. "status": 0,
  17. "craeteTime": null
  18. },
  19. "success": true
  20. }

3.4 核心类实现说明

  • 用户服务, 添加pom依赖

    1. <dependencies>
    2. <!-- spring boot 依赖 -->
    3. <dependency>
    4. <groupId>org.springframework.boot</groupId>
    5. <artifactId>spring-boot-starter-web</artifactId>
    6. </dependency>
    7. <!-- Nacos服务注册发现依赖 -->
    8. <dependency>
    9. <groupId>com.alibaba.cloud</groupId>
    10. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    11. </dependency>
    12. <!-- Spring Boot 监控组件依赖 -->
    13. <dependency>
    14. <groupId>org.springframework.boot</groupId>
    15. <artifactId>spring-boot-starter-actuator</artifactId>
    16. </dependency>
    17. <!-- 公用数据层组件 -->
    18. <dependency>
    19. <groupId>com.itcast.trade</groupId>
    20. <artifactId>bulls-stock-common-dao</artifactId>
    21. <version>${project.stock.version}</version>
    22. </dependency>
    23. <!-- 公用实体层依赖 -->
    24. <dependency>
    25. <groupId>com.itcast.trade</groupId>
    26. <artifactId>bulls-stock-entity</artifactId>
    27. <version>${project.stock.version}</version>
    28. </dependency>
    29. <!-- 公用WEB层依赖 -->
    30. <dependency>
    31. <groupId>com.itcast.trade</groupId>
    32. <artifactId>bulls-stock-common-web</artifactId>
    33. <version>${project.stock.version}</version>
    34. </dependency>
    35. <!-- 公用的工具依赖 -->
    36. <dependency>
    37. <groupId>com.itcast.trade</groupId>
    38. <artifactId>bulls-stock-common-utils</artifactId>
    39. <version>${project.stock.version}</version>
    40. </dependency>
    41. <!-- feign 远程调用 -->
    42. <dependency>
    43. <groupId>org.springframework.cloud</groupId>
    44. <artifactId>spring-cloud-starter-openfeign</artifactId>
    45. <version>${spring-cloud-starter.version}</version>
    46. </dependency>
    47. <dependency>
    48. <groupId>org.springframework.cloud</groupId>
    49. <artifactId>spring-cloud-openfeign-core</artifactId>
    50. <version>${spring-cloud-starter.version}</version>
    51. </dependency>
    52. <!-- OAUTH2 LoadBalance 功能相关依赖 -->
    53. <dependency>
    54. <groupId>com.netflix.archaius</groupId>
    55. <artifactId>archaius-core</artifactId>
    56. <version>0.7.6</version>
    57. </dependency>
    58. <!-- Spring Security OATUH2 鉴权相关依赖 -->
    59. <dependency>
    60. <groupId>org.springframework.cloud</groupId>
    61. <artifactId>spring-cloud-starter-oauth2</artifactId>
    62. <version>${spring-cloud-starter.version}</version>
    63. </dependency>
    64. <dependency>
    65. <groupId>org.springframework.cloud</groupId>
    66. <artifactId>spring-cloud-starter-security</artifactId>
    67. <version>${spring-cloud-starter.version}</version>
    68. </dependency>
    69. <dependency>
    70. <groupId>org.springframework.security.oauth.boot</groupId>
    71. <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    72. <version>${spring-cloud-starter.version}</version>
    73. </dependency>
    74. </dependencies>

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

    1. <!-- Spring Boot Mybatis依赖 -->
    2. <dependency>
    3. <groupId>org.mybatis.spring.boot</groupId>
    4. <artifactId>mybatis-spring-boot-starter</artifactId>
    5. <version>2.1.0</version>
    6. </dependency>
    7. <!-- druid 数据库连接池 -->
    8. <dependency>
    9. <groupId>com.alibaba</groupId>
    10. <artifactId>druid-spring-boot-starter</artifactId>
    11. <version>${druid.version}</version>
    12. </dependency>
    13. <!-- 解决druid log4j监控组件无法使用问题, 不用再额外增加其他LOG框架 -->
    14. <dependency>
    15. <groupId>org.slf4j</groupId>
    16. <artifactId>slf4j-log4j12</artifactId>
    17. </dependency>
    18. <!-- mysql-connector-java -->
    19. <dependency>
    20. <groupId>mysql</groupId>
    21. <artifactId>mysql-connector-java</artifactId>
    22. <version>8.0.14</version>
    23. </dependency>

  • 启动类StockUserApplication

    1. @SpringBootApplication
    2. @EnableDiscoveryClient
    3. @EnableFeignClients
    4. @ComponentScan(basePackages = {"com.itcast"})
    5. @MapperScan("com.itcast.trade.bulls.stock.user.dao")
    6. @EnableTransactionManagement
    7. public class StockUserApplication {
    8. public static void main(String[] args) {
    9. SpringApplication.run(StockUserApplication.class, args);
    10. }
    11. }

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

    EnableTransactionManagement注解是开启事务支持

  • 数据层

    • 用户服务数据接口IStockUserDao

      1. @Repository
      2. public interface IStockUserDao {
      3. /**
      4. * 根据用户账号获取用户对象
      5. * @param userNo
      6. * @return
      7. */
      8. TradeUser getByUserNo(String userNo);
      9. }
    • Mapper定义StockUserMapper.xml

      1. <?xml version="1.0" encoding="UTF-8" ?>
      2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
      3. <mapper namespace="com.itcast.trade.bulls.stock.user.dao.IStockUserDao">
      4. <resultMap id="BaseResultMap" type="com.itcast.bulls.stock.entity.user.TradeUser">
      5. <result column="id" jdbcType="BIGINT" property="id" />
      6. <result column="userNo" jdbcType="VARCHAR" property="userNo" />
      7. <result column="name" jdbcType="VARCHAR" property="name" />
      8. <result column="userPwd" jdbcType="VARCHAR" property="userPwd" />
      9. <result column="phone" jdbcType="VARCHAR" property="phone" />
      10. <result column="companyId" jdbcType="BIGINT" property="companyId" />
      11. <result column="email" jdbcType="VARCHAR" property="email" />
      12. <result column="address" jdbcType="VARCHAR" property="address" />
      13. <result column="lastLoginIp" jdbcType="VARCHAR" property="lastLoginIp" />
      14. <result column="lastLoginTime" jdbcType="TIMESTAMP" property="lastLoginTime" />
      15. <result column="status" jdbcType="TINYINT" property="status" />
      16. <result column="craeteTime" jdbcType="TIMESTAMP" property="craeteTime" />
      17. </resultMap>
      18. <select id="getByUserNo" resultMap="BaseResultMap">
      19. select
      20. userNo, name, userPwd,
      21. phone, companyId, email,
      22. address, lastLoginIp, lastLoginTime,
      23. status, craeteTime
      24. from t_trade_user
      25. where userNo = #{userNo}
      26. </select>
      27. </mapper>

  • 服务层StockUserServiceImpl

    1. @Service
    2. @Log4j2
    3. public class StockUserServiceImpl implements IStockUserService {
    4. @Autowired
    5. private IStockUserDao stockUserDao;
    6. /**
    7. * 用户登陆
    8. * @param userNo
    9. * @param userPwd
    10. * @return
    11. */
    12. public TradeUser userLogin(String userNo, String userPwd) throws ComponentException {
    13. // 获取用户对象
    14. TradeUser tradeUser= stockUserDao.getByUserNo(userNo);
    15. if(null == tradeUser) {
    16. throw new ComponentException(ApplicationErrorCodeEnum.USER_NOT_FOUND);
    17. }
    18. // 用户密码加密判断
    19. String encryptPassword = EncryptUtil.encryptSigned(userPwd);
    20. boolean pwdMatch= tradeUser.getUserPwd().equals(encryptPassword);
    21. if(!pwdMatch) {
    22. log.error(ApplicationErrorCodeEnum.USER_PWD_ERROR);
    23. throw new ComponentException(ApplicationErrorCodeEnum.USER_PWD_ERROR);
    24. }
    25. return tradeUser;
    26. }
    27. }

    用户登陆接口实现逻辑。

  • 接入层StockUserController

    1. @RestController()
    2. @RequestMapping("/user")
    3. @Log4j2
    4. public class StockUserController {
    5. @Autowired
    6. private IStockUserService stockUserService;
    7. /**
    8. * 用户登陆接口
    9. * @param userNo
    10. * @param userPwd
    11. * @return
    12. */
    13. @RequestMapping("/userLogin")
    14. public ApiRespResult userLogin(@RequestParam("userNo")String userNo, @RequestParam("userPwd") String userPwd) {
    15. ApiRespResult result = null;
    16. try {
    17. // 用户登陆逻辑处理
    18. TradeUser tradeUser = stockUserService.userLogin(userNo, userPwd);
    19. result = ApiRespResult.success(tradeUser);
    20. }catch(ComponentException e) {
    21. log.error(e.getMessage(), e);
    22. result = ApiRespResult.error(e.geterrorCodeEnum());
    23. }catch(Exception e) {
    24. log.error(e.getMessage(), e);
    25. result = ApiRespResult.sysError(e.getMessage());
    26. }
    27. return result;
    28. }
    29. }

  • 配置类

    认证配置ResourceSecurityConfigurer

    1. @Primary
    2. @Order(90)
    3. @Configuration
    4. @EnableResourceServer
    5. @EnableGlobalMethodSecurity(prePostEnabled = true)
    6. public class ResourceSecurityConfigurer implements ResourceServerConfigurer {
    7. @Autowired
    8. protected RemoteTokenServices remoteTokenServices;
    9. @Autowired
    10. private RestTemplate lbRestTemplate;
    11. /**
    12. * 远程调用,采用restTemplate方式处理
    13. * @param resourceServerSecurityConfigurer
    14. * @throws Exception
    15. */
    16. @Override
    17. public void configure(ResourceServerSecurityConfigurer resourceServerSecurityConfigurer) throws Exception {
    18. remoteTokenServices.setRestTemplate(lbRestTemplate);
    19. resourceServerSecurityConfigurer.tokenServices(remoteTokenServices);
    20. }
    21. /**
    22. * 资源服务安全配置
    23. * @param httpSecurity
    24. * @throws Exception
    25. */
    26. @Override
    27. public void configure(HttpSecurity httpSecurity) throws Exception {
    28. httpSecurity.csrf().disable()
    29. .authorizeRequests()
    30. .antMatchers("/user/**").authenticated().and()
    31. .formLogin().loginPage("/login")
    32. .failureUrl("/login?error")
    33. .defaultSuccessUrl("/home");
    34. }
    35. /**
    36. * RestTemplate配置
    37. * @return
    38. */
    39. @Bean
    40. @Primary
    41. @LoadBalanced
    42. public RestTemplate lbRestTemplate() {
    43. RestTemplate restTemplate = new RestTemplate();
    44. restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
    45. @Override
    46. public void handleError(ClientHttpResponse response) throws IOException {
    47. if (response.getRawStatusCode() != HttpStatus.BAD_REQUEST.value()) {
    48. super.handleError(response);
    49. }
    50. }
    51. });
    52. return restTemplate;
    53. }
    54. }

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

  • 工程配置

    配置文件bootstrap.yml

    1. server:
    2. port: 10681
    3. spring:
    4. application:
    5. name: stock-user
    6. cloud:
    7. nacos:
    8. discovery:
    9. server-addr: 127.0.0.1:8848
    10. config:
    11. server-addr: 127.0.0.1:8848
    12. # 数据源配置, 采用Druid
    13. datasource:
    14. type: com.alibaba.druid.pool.DruidDataSource
    15. driver-class-name: com.mysql.cj.jdbc.Driver
    16. username: root
    17. password: 654321
    18. url: jdbc:mysql://192.168.19.150:3306/trade_stock?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    19. druid:
    20. # 连接池的配置信息
    21. # 初始化大小,最小,最大
    22. initial-size: 5
    23. min-idle: 5
    24. maxActive: 20
    25. # 配置获取连接等待超时的时间
    26. maxWait: 60000
    27. # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    28. timeBetweenEvictionRunsMillis: 60000
    29. # 配置一个连接在池中最小生存的时间,单位是毫秒
    30. minEvictableIdleTimeMillis: 300000
    31. validationQuery: SELECT 1 FROM DUAL
    32. testWhileIdle: true
    33. testOnBorrow: false
    34. testOnReturn: false
    35. # 打开PSCache,并且指定每个连接上PSCache的大小
    36. poolPreparedStatements: true
    37. maxPoolPreparedStatementPerConnectionSize: 20
    38. # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    39. filters: stat,wall,log4j
    40. # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
    41. connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
    42. # 配置DruidStatFilter
    43. web-stat-filter:
    44. enabled: true
    45. url-pattern: "/*"
    46. exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
    47. # 配置DruidStatViewServlet
    48. stat-view-servlet:
    49. url-pattern: "/druid/*"
    50. # IP白名单(没有配置或者为空,则允许所有访问)
    51. allow:
    52. # IP黑名单 (存在共同时,deny优先于allow)
    53. deny:
    54. # 禁用HTML页面上的“Reset All”功能
    55. reset-enable: false
    56. # 登录名
    57. login-username: admin
    58. # 登录密码
    59. login-password: admin123
    60. # 监控后台开关, 开启可通过后台管理查看
    61. enabled: true
    62. ## spring security 配置
    63. security:
    64. oauth2:
    65. resource:
    66. loadBalanced: true
    67. token-info-uri: http://trade-auth/oauth/check_token
    68. client:
    69. client-id: app
    70. client-secret: app
    71. scope: server
    72. access-token-uri: http://trade-auth/oauth/token
    73. user-authorization-uri: http://trade-auth/oauth/authorize
    74. #mybatis 配置
    75. mybatis:
    76. 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的情况下, 返回错误信息:

在这里插入图片描述

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

在这里插入图片描述

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
initialSize0初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
maxActive8最大连接池数量
maxIdle8已经不再使用,配置了也没效果
minIdle最小连接池数量
maxWait获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
poolPreparedStatementsfalse是否缓存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)方法
testOnBorrowtrue申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturnfalse归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testWhileIdlefalse建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
timeBetweenEvictionRunsMillis1分钟(1.0.14)有两个含义:
1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
numTestsPerEvictionRun30分钟(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的使用。
在这里插入图片描述

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

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

3.4 Druid集成配置

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

  1. spring:
  2. # 数据源配置, 采用Druid
  3. datasource:
  4. type: com.alibaba.druid.pool.DruidDataSource
  5. driver-class-name: com.mysql.cj.jdbc.Driver
  6. username: root
  7. password: 654321
  8. url: jdbc:mysql://192.168.19.150:3306/trade_stock?useUnicode=true&characterEncoding=UTF-8&useSSL=false
  9. druid:
  10. # 连接池的配置信息
  11. # 初始化大小,最小,最大
  12. initial-size: 5
  13. min-idle: 5
  14. maxActive: 20
  15. # 配置获取连接等待超时的时间
  16. maxWait: 60000
  17. # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
  18. timeBetweenEvictionRunsMillis: 60000
  19. # 配置一个连接在池中最小生存的时间,单位是毫秒
  20. minEvictableIdleTimeMillis: 300000
  21. validationQuery: SELECT 1 FROM DUAL
  22. testWhileIdle: true
  23. testOnBorrow: false
  24. testOnReturn: false
  25. # 打开PSCache,并且指定每个连接上PSCache的大小
  26. poolPreparedStatements: true
  27. maxPoolPreparedStatementPerConnectionSize: 20
  28. # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
  29. filters: stat,wall,log4j
  30. # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
  31. connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
  32. # 配置DruidStatFilter
  33. web-stat-filter:
  34. enabled: true
  35. url-pattern: "/*"
  36. exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
  37. # 配置DruidStatViewServlet
  38. stat-view-servlet:
  39. url-pattern: "/druid/*"
  40. # IP白名单(没有配置或者为空,则允许所有访问)
  41. allow:
  42. # IP黑名单 (存在共同时,deny优先于allow)
  43. deny:
  44. # 禁用HTML页面上的“Reset All”功能
  45. reset-enable: false
  46. # 登录名
  47. login-username: admin
  48. # 登录密码
  49. login-password: admin123
  50. # 监控后台开关, 开启可通过后台管理查看
  51. enabled: true

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

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

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

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

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

login-username是登陆了用户名。

login-password是登陆密码。

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

3.5 核心类实现说明

  • 启动类StockGatewayApplication

    1. @SpringBootApplication
    2. @EnableDiscoveryClient
    3. @ComponentScan(basePackages = {"com.itcast"})
    4. public class StockGatewayApplication {
    5. public static void main(String[] args) {
    6. SpringApplication.run(StockGatewayApplication.class, args);
    7. }
    8. }
  • 全局过滤器StockRequestGlobalFilter

    1. @Component
    2. @Log4j2
    3. public class StockRequestGlobalFilter implements GlobalFilter, Ordered {
    4. /**
    5. * 通过filter来自定义配置转发信息
    6. * @param exchange
    7. * @param chain
    8. * @return
    9. */
    10. @Override
    11. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    12. String authentication = exchange.getRequest().getHeaders().getFirst("Authorization");
    13. if(!StringUtil.isNullOrEmpty(authentication)){
    14. log.info("enter stockRequestGlobalFilter filter method: " + authentication);
    15. exchange.getRequest().mutate().header("Authorization",authentication);
    16. }
    17. return chain.filter(exchange.mutate().build());
    18. }
    19. @Override
    20. public int getOrder() {
    21. return -1000;
    22. }
    23. }

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

  • 工程配置application.yml

    1. server:
    2. port: 10680
    3. spring:
    4. application:
    5. name: stock-gateway
    6. cloud:
    7. nacos:
    8. discovery:
    9. server-addr: 127.0.0.1:8848
    10. service: stock-gateway
    11. gateway:
    12. discovery:
    13. # 允许通过服务名称进行路由转发访问, http://service-id/user
    14. locator:
    15. enabled: true
    16. # 路由配置
    17. routes:
    18. - id: stock-user
    19. uri: lb://stock-user
    20. predicates:
    21. - Path=/user/**
    22. - id: trade-auth
    23. uri: lb://trade-auth
    24. predicates:
    25. # - Method=GET,POST 不要开启此项
    26. - Path=/oauth/**
    27. logging:
    28. level:
    29. 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数据。

  1. 通过网关访问用户资源服务, 请求登陆接口: 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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/68868
推荐阅读
相关标签
  

闽ICP备14008679号