软件系统开发定制(一)学习spring-cloud:2021之spring-authorization-server

1. 前言

1.1 软件系统开发定制为什么使用spring-authorization-server?

        真实原因:软件系统开发定制原先是因为个人原因,软件系统开发定制需要研究新版鉴权服务,看到了spring-authorization-server,软件系统开发定制使用过程中,软件系统开发定制想着能不能整合新版本cloud,软件系统开发定制因此此处先以springboot搭建spring-authorization-server,软件系统开发定制后续再替换为springcloud2021。

        官方原因:原先使用Spring Security OAuth,软件系统开发定制而该项目已经逐渐被淘汰,软件系统开发定制虽然网上还是有不少该方案,软件系统开发定制但秉着技术要随时代更新,从而使用spring-authorization-server

2.软件系统开发定制项目迭代历程

  1. 引入gateway网关,swagger文档工具

  2. 待续

3.项目构建

        

3.1 以springboot搭建spring-authorization-server(即认证与资源服务器)

        数据库相关表结构构建

        需要创建3张表,sql分别如下

  1. CREATE TABLE `oauth2_authorization` (
  2. `id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  3. `registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  4. `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  5. `authorization_grant_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  6. `attributes` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  7. `state` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  8. `authorization_code_value` blob NULL,
  9. `authorization_code_issued_at` timestamp(0) NULL DEFAULT NULL,
  10. `authorization_code_expires_at` timestamp(0) NULL DEFAULT NULL,
  11. `authorization_code_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  12. `access_token_value` blob NULL,
  13. `access_token_issued_at` timestamp(0) NULL DEFAULT NULL,
  14. `access_token_expires_at` timestamp(0) NULL DEFAULT NULL,
  15. `access_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  16. `access_token_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  17. `access_token_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  18. `oidc_id_token_value` blob NULL,
  19. `oidc_id_token_issued_at` timestamp(0) NULL DEFAULT NULL,
  20. `oidc_id_token_expires_at` timestamp(0) NULL DEFAULT NULL,
  21. `oidc_id_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  22. `refresh_token_value` blob NULL,
  23. `refresh_token_issued_at` timestamp(0) NULL DEFAULT NULL,
  24. `refresh_token_expires_at` timestamp(0) NULL DEFAULT NULL,
  25. `refresh_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  26. PRIMARY KEY (`id`) USING BTREE
  27. ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
  28. CREATE TABLE `oauth2_authorization_consent` (
  29. `registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  30. `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  31. `authorities` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  32. PRIMARY KEY (`registered_client_id`, `principal_name`) USING BTREE
  33. ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
  34. CREATE TABLE `oauth2_registered_client` (
  35. `id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  36. `client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  37. `client_id_issued_at` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
  38. `client_secret` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  39. `client_secret_expires_at` timestamp(0) NULL DEFAULT NULL,
  40. `client_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  41. `client_authentication_methods` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  42. `authorization_grant_types` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  43. `redirect_uris` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  44. `scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  45. `client_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  46. `token_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  47. PRIMARY KEY (`id`) USING BTREE
  48. ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

        先进行认证服务器相关配置

        pom.xml引入依赖

        注意!!!spring boot版本需2.6.x以上,是为后面升级成cloud做准备

  1. <dependency>
  2. <groupId>org.projectlombok</groupId>
  3. <artifactId>lombok</artifactId>
  4. <version>1.18.22</version>
  5. </dependency>
  6. <!-- 此依赖是个人公共依赖,你们引入其他具体依赖即可 -->
  7. <dependency>
  8. <groupId>com.xxxx.iov</groupId>
  9. <artifactId>iov-cloud-framework-web</artifactId>
  10. <version>2.0.0-SNAPSHOT</version>
  11. <exclusions>
  12. <!-- 这里是因为公共依赖中的web版本太低,所以移除 -->
  13. <exclusion>
  14. <groupId>org.springframework.boot</groupId>
  15. <artifactId>spring-boot-starter-web</artifactId>
  16. </exclusion>
  17. </exclusions>
  18. </dependency>
  19. <dependency>
  20. <groupId>org.springframework.boot</groupId>
  21. <artifactId>spring-boot-starter-web</artifactId>
  22. <version>2.6.6</version>
  23. </dependency>
  24. <!-- hutool -->
  25. <dependency>
  26. <groupId>cn.hutool</groupId>
  27. <artifactId>hutool-all</artifactId>
  28. <version>5.8.0</version>
  29. </dependency>
  30. <!-- fastjson -->
  31. <dependency>
  32. <groupId>com.alibaba</groupId>
  33. <artifactId>fastjson</artifactId>
  34. <version>1.2.39</version>
  35. </dependency>
  36. <!-- security -->
  37. <dependency>
  38. <groupId>org.springframework.boot</groupId>
  39. <artifactId>spring-boot-starter-security</artifactId>
  40. </dependency>
  41. <!-- oauth2-authorization-server -->
  42. <dependency>
  43. <groupId>org.springframework.security</groupId>
  44. <artifactId>spring-security-oauth2-authorization-server</artifactId>
  45. <version>0.2.3</version>
  46. </dependency>
  47. <!-- security-cas -->
  48. <dependency>
  49. <groupId>org.springframework.security</groupId>
  50. <artifactId>spring-security-cas</artifactId>
  51. </dependency>
  52. <!-- thymeleaf -->
  53. <dependency>
  54. <groupId>org.springframework.boot</groupId>
  55. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  56. </dependency>
  57. <!-- 数据连接池 -->
  58. <dependency>
  59. <groupId>com.alibaba</groupId>
  60. <artifactId>druid-spring-boot-starter</artifactId>
  61. <version>1.2.9</version>
  62. </dependency>
  63. <!-- 数据库驱动 -->
  64. <dependency>
  65. <groupId>mysql</groupId>
  66. <artifactId>mysql-connector-java</artifactId>
  67. <version>8.0.28</version>
  68. </dependency>
  69. <!-- mybatis-plus -->
  70. <dependency>
  71. <groupId>com.baomidou</groupId>
  72. <artifactId>mybatis-plus-boot-starter</artifactId>
  73. <version>3.5.1</version>
  74. </dependency>
  75. <!-- guava -->
  76. <dependency>
  77. <groupId>com.google.guava</groupId>
  78. <artifactId>guava</artifactId>
  79. <version>31.1-jre</version>
  80. </dependency>

        创建自定义登录页面 login.html (可不要,使用自带的登录界面)

  1. <!DOCTYPE html>
  2. <html lang="en"
  3. xmlns:th="https://www.thymeleaf.org"
  4. xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
  5. <head>
  6. <meta charset="utf-8">
  7. <meta name="author" content="test">
  8. <meta name="viewport" content="width=device-width,initial-scale=1">
  9. <meta name="description" content="This is a login page template based on Bootstrap 5">
  10. <title>Login Page</title>
  11. <style>
  12. .is-invalid {
  13. color: red;
  14. }
  15. .invalid-feedback {
  16. color: red;
  17. }
  18. .mb-3 {
  19. margin-bottom: 3px;
  20. }
  21. </style>
  22. <script th:inline="javascript">
  23. /*<![CDATA[*/
  24. // const baseURL = /*[[@{/}]]*/ ''; /*]]>*/
  25. if (window !== top) {
  26. top.location.href = location.href;
  27. }
  28. </script>
  29. </head>
  30. <body class="hold-transition login-page">
  31. <div class="login-box">
  32. <div class="card">
  33. <div class="card-body login-card-body">
  34. <p class="login-box-msg">Sign in to start your session</p>
  35. <div th:if="${param.error}" class="alert alert-error">
  36. Invalid username and password.
  37. </div>
  38. <div th:if="${param.logout}" class="alert alert-success">
  39. You have been logged out.
  40. </div>
  41. <form th:action="@{/login}" method="post" id="loginForm">
  42. <div class="input-group mb-3">
  43. <input type="text" class="form-control" value="zxg" name="username" placeholder="Email"
  44. autocomplete="off">
  45. </div>
  46. <div class="input-group mb-3">
  47. <input type="password" id="password" name="password" value="123" class="form-control"
  48. maxlength="25" placeholder="Password"
  49. autocomplete="off">
  50. </div>
  51. <div class="row">
  52. <div class="col-4">
  53. <button type="submit" id="submitBtn">Sign In</button>
  54. </div>
  55. </div>
  56. </form>
  57. <p class="mb-1">
  58. <a href="javascript:void(0)">I forgot my password</a>
  59. </p>
  60. <p class="mb-0">
  61. <a href="javascript:void(0)" class="text-center">Register a new membership</a>
  62. </p>
  63. </div>
  64. </div>
  65. </div>
  66. <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
  67. <script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/3.1.0/jsencrypt.min.js"></script>
  68. <script src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.9.0/jquery.validate.min.js"></script>
  69. <script src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.9.0/additional-methods.min.js"></script>
  70. <script th:inline="javascript">
  71. $(function () {
  72. var encrypt = new JSEncrypt();
  73. $.validator.setDefaults({
  74. submitHandler: function (form) {
  75. console.log("Form successful submitted!");
  76. form.submit();
  77. }
  78. });
  79. });
  80. </script>
  81. </body>
  82. </html>

创建自定义授权页面 consent.html(可不要,可使用自带的授权页面)

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  6. <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
  7. integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
  8. <title>授权页面</title>
  9. <style>
  10. body {
  11. background-color: aliceblue;
  12. }
  13. </style>
  14. <script>
  15. function cancelConsent() {
  16. document.consent_form.reset();
  17. document.consent_form.submit();
  18. }
  19. </script>
  20. </head>
  21. <body>
  22. <div class="container">
  23. <div class="py-5">
  24. <h1 class="text-center text-primary">用户授权确认</h1>
  25. </div>
  26. <div class="row">
  27. <div class="col text-center">
  28. <p>
  29. 应用
  30. <a href="https://felord.cn"><span class="font-weight-bold text-primary" th:text="${clientName}"></span></a>
  31. 想要访问您的账号
  32. <span class="font-weight-bold" th:text="${principalName}"></span>
  33. </p>
  34. </div>
  35. </div>
  36. <div class="row pb-3">
  37. <div class="col text-center"><p>上述应用程序请求以下权限<br/>请审阅以下选项并勾选您同意的权限</p></div>
  38. </div>
  39. <div class="row">
  40. <div class="col text-center">
  41. <form name="consent_form" method="post" action="/oauth2/authorize">
  42. <input type="hidden" name="client_id" th:value="${clientId}">
  43. <input type="hidden" name="state" th:value="${state}">
  44. <div th:each="scope: ${scopes}" class="form-group form-check py-1">
  45. <input class="form-check-input"
  46. type="checkbox"
  47. name="scope"
  48. th:value="${scope.scope}"
  49. th:id="${scope.scope}">
  50. <label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}"></label>
  51. <p class="text-primary" th:text="${scope.description}"></p>
  52. </div>
  53. <p th:if="${not #lists.isEmpty(previouslyApprovedScopes)}">您已对上述应用授予以下权限:</p>
  54. <div th:each="scope: ${previouslyApprovedScopes}" class="form-group form-check py-1">
  55. <input class="form-check-input"
  56. type="checkbox"
  57. th:id="${scope.scope}"
  58. disabled
  59. checked>
  60. <label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}"></label>
  61. <p class="text-primary" th:text="${scope.description}"></p>
  62. </div>
  63. <div class="form-group pt-3">
  64. <button class="btn btn-primary btn-lg" type="submit" id="submit-consent">
  65. 同意授权
  66. </button>
  67. </div>
  68. <div class="form-group">
  69. <button class="btn btn-link regular" type="button" id="cancel-consent" onclick="cancelConsent();">
  70. 取消授权
  71. </button>
  72. </div>
  73. </form>
  74. </div>
  75. </div>
  76. <div class="row pt-4">
  77. <div class="col text-center">
  78. <p>
  79. <small>
  80. 需要您同意并提供访问权限。
  81. <br/>如果您不同意,请单击<span class="font-weight-bold text-primary">取消授权</span>,将不会为上述应用程序提供任何您的信息。
  82. </small>
  83. </p>
  84. </div>
  85. </div>
  86. </div>
  87. </body>
  88. </html>

        修改配置文件 application.yml(配置内容可自行简略)

  1. server:
  2. port: 9000
  3. spring:
  4. application:
  5. name: authorization-server
  6. thymeleaf:
  7. cache: false
  8. datasource:
  9. url: jdbc:mysql://192.168.1.69:3306/test
  10. username: root
  11. password: root
  12. driver-class-name: com.mysql.cj.jdbc.Driver
  13. security:
  14. oauth2:
  15. resourceserver:
  16. jwt:
  17. issuer-uri: http://127.0.0.1:9000 #认证中心端点,作为资源端的配置
  18. application:
  19. security:
  20. excludeUrls: #excludeUrls中存放白名单地址
  21. - "/favicon.ico"
  22. # mybatis plus配置
  23. mybatis-plus:
  24. mapper-locations: classpath:/mapper/*Mapper.xml
  25. global-config:
  26. # 关闭MP3.0自带的banner
  27. banner: false
  28. db-config:
  29. #主键类型 0:"数据库ID自增", 1:"不操作", 2:"用户输入ID",3:"数字型snowflake", 4:"全局唯一ID UUID", 5:"字符串型snowflake";
  30. id-type: AUTO
  31. #字段策略
  32. insert-strategy: not_null
  33. update-strategy: not_null
  34. select-strategy: not_null
  35. #驼峰下划线w转换
  36. table-underline: true
  37. # 逻辑删除配置
  38. # 逻辑删除全局值(1表示已删除,这也是Mybatis Plus的默认配置)
  39. logic-delete-value: 1
  40. # 逻辑未删除全局值(0表示未删除,这也是Mybatis Plus的默认配置)
  41. logic-not-delete-value: 0
  42. configuration:
  43. #驼峰
  44. map-underscore-to-camel-case: true
  45. #打开二级缓存
  46. cache-enabled: true
  47. # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志

        新增认证服务器配置文件 AuthorizationServerConfig

  1. @Configuration(proxyBeanMethods = false)
  2. public class AuthorizationServerConfig {
  3. /**
  4. * 自定义授权页面
  5. * 使用系统自带的即不用
  6. */
  7. private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
  8. /**
  9. * 自定义UserDetailsService
  10. */
  11. @Autowired
  12. private UserService userService;
  13. /**
  14. *
  15. * 使用默认配置进行form表单登录
  16. * OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
  17. */
  18. @Bean
  19. @Order(Ordered.HIGHEST_PRECEDENCE)
  20. public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
  21. OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>();
  22. authorizationServerConfigurer.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI));
  23. RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
  24. http
  25. .requestMatcher(endpointsMatcher)
  26. .userDetailsService(userService)
  27. .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
  28. .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
  29. .apply(authorizationServerConfigurer);
  30. return http.formLogin(Customizer.withDefaults()).build();
  31. }
  32. /**
  33. * 注册客户端应用
  34. */
  35. @Bean
  36. public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
  37. // Save registered client in db as if in-jdbc
  38. RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
  39. .clientId("zxg")
  40. .clientSecret("123")
  41. .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
  42. .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
  43. .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
  44. .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
  45. // 回调地址
  46. .redirectUri("http://www.baidu.com")
  47. // scope自定义的客户端范围
  48. .scope(OidcScopes.OPENID)
  49. .scope("message.read")
  50. .scope("message.write")
  51. // client请求访问时需要授权同意
  52. .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
  53. // token配置项信息
  54. .tokenSettings(TokenSettings.builder()
  55. // token有效期100分钟
  56. .accessTokenTimeToLive(Duration.ofMinutes(100L))
  57. // 使用默认JWT相关格式
  58. .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
  59. // 开启刷新token
  60. .reuseRefreshTokens(true)
  61. // refreshToken有效期120分钟
  62. .refreshTokenTimeToLive(Duration.ofMinutes(120L))
  63. .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256).build()
  64. )
  65. .build();
  66. // Save registered client in db as if in-memory
  67. JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
  68. registeredClientRepository.save(registeredClient);
  69. return registeredClientRepository;
  70. }
  71. /**
  72. * 授权服务:管理OAuth2授权信息服务
  73. */
  74. @Bean
  75. public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
  76. return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
  77. }
  78. /**
  79. * 授权确认信息处理服务
  80. */
  81. @Bean
  82. public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
  83. return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
  84. }
  85. /**
  86. * 加载JWK资源
  87. * JWT:指的是 JSON Web Token,不存在签名的JWT是不安全的,存在签名的JWT是不可窜改的
  88. * JWS:指的是签过名的JWT,即拥有签名的JWT
  89. * JWK:既然涉及到签名,就涉及到签名算法,对称加密还是非对称加密,那么就需要加密的 密钥或者公私钥对。此处我们将 JWT的密钥或者公私钥对统一称为 JSON WEB KEY,即 JWK。
  90. */
  91. @Bean
  92. public JWKSource<SecurityContext> jwkSource() {
  93. RSAKey rsaKey = JwksUtils.generateRsa();
  94. JWKSet jwkSet = new JWKSet(rsaKey);
  95. return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
  96. }
  97. /**
  98. * 配置 OAuth2.0 提供者元信息
  99. */
  100. @Bean
  101. public ProviderSettings providerSettings() {
  102. return ProviderSettings.builder().issuer("http://127.0.0.1:9000").build();
  103. }
  104. }

        新增Security的配置文件WebSecurityConfig

  1. @Configuration
  2. @EnableWebSecurity(debug = true) //开启Security
  3. public class WebSecurityConfig {
  4. @Autowired
  5. private ApplicationProperties properties;
  6. /**
  7. * 设置加密方式
  8. */
  9. @Bean
  10. public PasswordEncoder passwordEncoder() {
  11. // // 将密码加密方式采用委托方式,默认以BCryptPasswordEncoder方式进行加密,兼容ldap,MD4,MD5等方式
  12. // return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  13. // 此处我们使用明文方式 不建议这样
  14. return NoOpPasswordEncoder.getInstance();
  15. }
  16. /**
  17. * 使用WebSecurity.ignoring()忽略某些URL请求,这些请求将被Spring Security忽略
  18. */
  19. @Bean
  20. WebSecurityCustomizer webSecurityCustomizer() {
  21. return new WebSecurityCustomizer() {
  22. @Override
  23. public void customize(WebSecurity web) {
  24. // 读取配置文件application.security.excludeUrls下的链接进行忽略
  25. web.ignoring().antMatchers(properties.getSecurity().getExcludeUrls().toArray(new String[]{}));
  26. }
  27. };
  28. }
  29. /**
  30. * 针对http请求,进行拦截过滤
  31. *
  32. * CookieCsrfTokenRepository进行CSRF保护的工作方式:
  33. * 1.客户端向服务器发出GET请求,例如请求主页
  34. * 2.Spring发送 GET 请求的响应以及 Set-cookie 标头,其中包含安全生成的XSRF令牌
  35. */
  36. @Bean
  37. public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
  38. httpSecurity
  39. .authorizeRequests(authorizeRequests ->
  40. authorizeRequests.antMatchers("/login").permitAll()
  41. .anyRequest().authenticated()
  42. )
  43. //使用默认登录页面
  44. //.formLogin(withDefaults())
  45. //设置form登录,设置且放开登录页login
  46. .formLogin(fromlogin -> fromlogin.loginPage("/login").permitAll())
  47. // Spring Security CSRF保护
  48. .csrf(csrfToken -> csrfToken.csrfTokenRepository(new CookieCsrfTokenRepository()))
  49. // //开启认证服务器的资源服务器相关功能,即需校验token
  50. // .oauth2ResourceServer()
  51. // .accessDeniedHandler(new SimpleAccessDeniedHandler())
  52. // .authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
  53. // .jwt()
  54. ;
  55. return httpSecurity.build();
  56. }
  57. }

        新增读取application配置的类 ApplicationProperties

  1. /**
  2. * 此步主要是获取配置文件中配置的白名单,可自行舍去或自定义实现其他方式
  3. **/
  4. @Data
  5. @Component
  6. @ConfigurationProperties("application")
  7. public class ApplicationProperties {
  8. private final Security security = new Security();
  9. @Data
  10. public static class Security {
  11. private Oauth2 oauth2;
  12. private List<String> excludeUrls = new ArrayList<>();
  13. @Data
  14. public static class Oauth2 {
  15. private String issuerUrl;
  16. }
  17. }
  18. }

        新增 JwksUtils 类和 KeyGeneratorUtils,这两个类作为JWT对称加密

  1. public final class JwksUtils {
  2. private JwksUtils() {
  3. }
  4. /**
  5. * 生成RSA加密key (即JWK)
  6. */
  7. public static RSAKey generateRsa() {
  8. // 生成RSA加密的key
  9. KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
  10. // 公钥
  11. RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
  12. // 私钥
  13. RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
  14. // 构建RSA加密key
  15. return new RSAKey.Builder(publicKey)
  16. .privateKey(privateKey)
  17. .keyID(UUID.randomUUID().toString())
  18. .build();
  19. }
  20. /**
  21. * 生成EC加密key (即JWK)
  22. */
  23. public static ECKey generateEc() {
  24. // 生成EC加密的key
  25. KeyPair keyPair = KeyGeneratorUtils.generateEcKey();
  26. // 公钥
  27. ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
  28. // 私钥
  29. ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
  30. // 根据公钥参数生成曲线
  31. Curve curve = Curve.forECParameterSpec(publicKey.getParams());
  32. // 构建EC加密key
  33. return new ECKey.Builder(curve, publicKey)
  34. .privateKey(privateKey)
  35. .keyID(UUID.randomUUID().toString())
  36. .build();
  37. }
  38. /**
  39. * 生成HmacSha256密钥
  40. */
  41. public static OctetSequenceKey generateSecret() {
  42. SecretKey secretKey = KeyGeneratorUtils.generateSecretKey();
  43. return new OctetSequenceKey.Builder(secretKey)
  44. .keyID(UUID.randomUUID().toString())
  45. .build();
  46. }
  47. }
  48. class KeyGeneratorUtils {
  49. private KeyGeneratorUtils() {
  50. }
  51. /**
  52. * 生成RSA密钥
  53. */
  54. static KeyPair generateRsaKey() {
  55. KeyPair keyPair;
  56. try {
  57. KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
  58. keyPairGenerator.initialize(2048);
  59. keyPair = keyPairGenerator.generateKeyPair();
  60. } catch (Exception ex) {
  61. throw new IllegalStateException(ex);
  62. }
  63. return keyPair;
  64. }
  65. /**
  66. * 生成EC密钥
  67. */
  68. static KeyPair generateEcKey() {
  69. EllipticCurve ellipticCurve = new EllipticCurve(
  70. new ECFieldFp(
  71. new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951")),
  72. new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
  73. new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291"));
  74. ECPoint ecPoint = new ECPoint(
  75. new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
  76. new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109"));
  77. ECParameterSpec ecParameterSpec = new ECParameterSpec(
  78. ellipticCurve,
  79. ecPoint,
  80. new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"),
  81. 1);
  82. KeyPair keyPair;
  83. try {
  84. KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
  85. keyPairGenerator.initialize(ecParameterSpec);
  86. keyPair = keyPairGenerator.generateKeyPair();
  87. } catch (Exception ex) {
  88. throw new IllegalStateException(ex);
  89. }
  90. return keyPair;
  91. }
  92. /**
  93. * 生成HmacSha256密钥
  94. */
  95. static SecretKey generateSecretKey() {
  96. SecretKey hmacKey;
  97. try {
  98. hmacKey = KeyGenerator.getInstance("HmacSha256").generateKey();
  99. } catch (Exception ex) {
  100. throw new IllegalStateException(ex);
  101. }
  102. return hmacKey;
  103. }
  104. }

        新建 ConsentController,编写登录和认证页面的跳转

        如果在上面没有使用自定义的登录和授权页面,下面的跳转方法按需舍去

  1. @Slf4j
  2. @Controller
  3. public class ConsentController {
  4. private final RegisteredClientRepository registeredClientRepository;
  5. private final OAuth2AuthorizationConsentService authorizationConsentService;
  6. public ConsentController(RegisteredClientRepository registeredClientRepository,
  7. OAuth2AuthorizationConsentService authorizationConsentService) {
  8. this.registeredClientRepository = registeredClientRepository;
  9. this.authorizationConsentService = authorizationConsentService;
  10. }
  11. @ResponseBody
  12. @GetMapping("/favicon.ico")
  13. public String faviconico(){
  14. return "favicon.ico";
  15. }
  16. @GetMapping("/login")
  17. public String loginPage(){
  18. return "login";
  19. }
  20. @GetMapping(value = "/oauth2/consent")
  21. public String consent(Principal principal, Model model,
  22. @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
  23. @RequestParam(OAuth2ParameterNames.SCOPE) String scope,
  24. @RequestParam(OAuth2ParameterNames.STATE) String state) {
  25. // Remove scopes that were already approved
  26. Set<String> scopesToApprove = new HashSet<>();
  27. Set<String> previouslyApprovedScopes = new HashSet<>();
  28. RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
  29. OAuth2AuthorizationConsent currentAuthorizationConsent =
  30. this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());
  31. Set<String> authorizedScopes;
  32. if (currentAuthorizationConsent != null) {
  33. authorizedScopes = currentAuthorizationConsent.getScopes();
  34. } else {
  35. authorizedScopes = Collections.emptySet();
  36. }
  37. for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
  38. if (authorizedScopes.contains(requestedScope)) {
  39. previouslyApprovedScopes.add(requestedScope);
  40. } else {
  41. scopesToApprove.add(requestedScope);
  42. }
  43. }
  44. model.addAttribute("clientId", clientId);
  45. model.addAttribute("state", state);
  46. model.addAttribute("scopes", withDescription(scopesToApprove));
  47. model.addAttribute("previouslyApprovedScopes", withDescription(previouslyApprovedScopes));
  48. model.addAttribute("principalName", principal.getName());
  49. return "consent";
  50. }
  51. private static Set<ScopeWithDescription> withDescription(Set<String> scopes) {
  52. Set<ScopeWithDescription> scopeWithDescriptions = new HashSet<>();
  53. for (String scope : scopes) {
  54. scopeWithDescriptions.add(new ScopeWithDescription(scope));
  55. }
  56. return scopeWithDescriptions;
  57. }
  58. public static class ScopeWithDescription {
  59. private static final String DEFAULT_DESCRIPTION = "UNKNOWN SCOPE - We cannot provide information about this permission, use caution when granting this.";
  60. private static final Map<String, String> scopeDescriptions = new HashMap<>();
  61. static {
  62. scopeDescriptions.put(
  63. "message.read",
  64. "This application will be able to read your message."
  65. );
  66. scopeDescriptions.put(
  67. "message.write",
  68. "This application will be able to add new messages. It will also be able to edit and delete existing messages."
  69. );
  70. scopeDescriptions.put(
  71. "other.scope",
  72. "This is another scope example of a scope description."
  73. );
  74. }
  75. public final String scope;
  76. public final String description;
  77. ScopeWithDescription(String scope) {
  78. this.scope = scope;
  79. this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION);
  80. }
  81. }
  82. }

        新建 UserController,User,UserService等标准的自定义用户业务,此处仅放出UserServiceImpl

  1. @RequiredArgsConstructor
  2. @Slf4j
  3. @Component
  4. class UserServiceImpl implements UserService {
  5. private final UserMapper userMapper;
  6. @Override
  7. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  8. User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername,username));
  9. return new org.springframework.security.core.userdetails.User(username, user.getPassword(), new ArrayList<>());
  10. }
  11. }

        启动项目,如下图

        认证服务器整体结构图

----------------------------------------------------------------------------------------------------------------------------------------------------------

        资源服务器相关配置

        pom.xml引入资源服务器相关依赖

  1. <!-- resource-server资源服务器 -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
  5. </dependency>
  6. <!-- security -->
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-security</artifactId>
  10. </dependency>

        新增配置文件 application.yaml

  1. server:
  2. port: 9003
  3. spring:
  4. application:
  5. name: resource
  6. security:
  7. oauth2:
  8. resourceserver:
  9. jwt:
  10. issuer-uri: http://127.0.0.1:9000
  11. feign:
  12. client:
  13. config:
  14. default: #配置超时时间
  15. connect-timeout: 10000
  16. read-timeout: 10000

        新增资源服务器配置文件 ResourceServerConfiguration

  1. @Configuration
  2. @EnableWebSecurity(debug = true)
  3. @EnableGlobalMethodSecurity(prePostEnabled = true) //开启鉴权服务
  4. public class ResourceServerConfiguration {
  5. @Bean
  6. public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
  7. // 所有请求都进行拦截
  8. httpSecurity.authorizeRequests().anyRequest().authenticated();
  9. // 关闭session
  10. httpSecurity.sessionManagement().disable();
  11. // 配置资源服务器的无权限,无认证拦截器等 以及JWT验证
  12. httpSecurity.oauth2ResourceServer()
  13. .accessDeniedHandler(new SimpleAccessDeniedHandler())
  14. .authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
  15. .jwt();
  16. return httpSecurity.build();
  17. }
  18. }

        新增相关无认证无权限统一拦截回复 SimpleAccessDeniedHandler 和 SimpleAuthenticationEntryPoint

  1. /**
  2. * 携带了token 而且token合法 但是权限不足以访问其请求的资源 403
  3. * @author zxg
  4. */
  5. public class SimpleAccessDeniedHandler implements AccessDeniedHandler {
  6. @Override
  7. public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
  8. response.setStatus(HttpServletResponse.SC_FORBIDDEN);
  9. response.setCharacterEncoding("utf-8");
  10. response.setContentType(MediaType.APPLICATION_JSON_VALUE);
  11. ObjectMapper objectMapper = new ObjectMapper();
  12. String resBody = objectMapper.writeValueAsString(SingleResultBundle.failed("无权访问"));
  13. PrintWriter printWriter = response.getWriter();
  14. printWriter.print(resBody);
  15. printWriter.flush();
  16. printWriter.close();
  17. }
  18. }
  19. /**
  20. * 在资源服务器中 不携带token 或者token无效 401
  21. * @author zxg
  22. */
  23. @Slf4j
  24. public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint {
  25. @Override
  26. public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
  27. if (response.isCommitted()){
  28. return;
  29. }
  30. Throwable throwable = authException.fillInStackTrace();
  31. String errorMessage = "认证失败";
  32. if (throwable instanceof BadCredentialsException){
  33. errorMessage = "错误的客户端信息";
  34. }else {
  35. Throwable cause = authException.getCause();
  36. if (cause instanceof JwtValidationException) {
  37. log.warn("JWT Token 过期,具体内容:" + cause.getMessage());
  38. errorMessage = "无效的token信息";
  39. } else if (cause instanceof BadJwtException){
  40. log.warn("JWT 签名异常,具体内容:" + cause.getMessage());
  41. errorMessage = "无效的token信息";
  42. } else if (cause instanceof AccountExpiredException){
  43. errorMessage = "账户已过期";
  44. } else if (cause instanceof LockedException){
  45. errorMessage = "账户已被锁定";
  46. // } else if (cause instanceof InvalidClientException || cause instanceof BadClientCredentialsException){
  47. // response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed(401,"无效的客户端")));
  48. // } else if (cause instanceof InvalidGrantException || cause instanceof RedirectMismatchException){
  49. // response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("无效的类型")));
  50. // } else if (cause instanceof UnauthorizedClientException) {
  51. // response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("未经授权的客户端")));
  52. } else if (throwable instanceof InsufficientAuthenticationException) {
  53. String message = throwable.getMessage();
  54. if (message.contains("Invalid token does not contain resource id")){
  55. errorMessage = "未经授权的资源服务器";
  56. }else if (message.contains("Full authentication is required to access this resource")){
  57. errorMessage = "缺少验证信息";
  58. }
  59. }else {
  60. errorMessage = "验证异常";
  61. }
  62. }
  63. response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
  64. response.setCharacterEncoding("utf-8");
  65. response.setContentType(MediaType.APPLICATION_JSON_VALUE);
  66. ObjectMapper objectMapper = new ObjectMapper();
  67. String resBody = objectMapper.writeValueAsString(SingleResultBundle.failed(errorMessage));
  68. PrintWriter printWriter = response.getWriter();
  69. printWriter.print(resBody);
  70. printWriter.flush();
  71. printWriter.close();
  72. }
  73. }

        新增 ResourceController 进行接口测试

  1. @Slf4j
  2. @RestController
  3. public class ResourceController {
  4. /**
  5. * 测试Spring Authorization Server,测试权限
  6. */
  7. @PreAuthorize("hasAuthority('SCOPE_message.read')")
  8. @GetMapping("/getTest")
  9. public String getTest(){
  10. return "getTest";
  11. }
  12. /**
  13. * 默认登录成功跳转页为 / 防止404状态
  14. *
  15. * @return the map
  16. */
  17. @GetMapping("/")
  18. public Map<String, String> index() {
  19. return Collections.singletonMap("msg", "login success!");
  20. }
  21. @GetMapping("/getResourceTest")
  22. public SingleResultBundle<String> getResourceTest(){
  23. return SingleResultBundle.success("这是resource的测试方法 getResourceTest()");
  24. }
  25. }

        启动项目,效果如下

        项目总体结构如下

        测试认证鉴权

  1. #调用 /oauth2/authorize ,获取code
  2. http://127.0.0.1:9000/oauth2/authorize?client_id=zxg&response_type=code&scope=message.read&redirect_uri=http://www.baidu.com
  3. #会判断是否登录,若没有,则跳转到登录页面,如下图1
  4. #登录完成后,会提示是否授权,若没有,则跳转到授权界面,如下图2
  5. #授权成功后,跳转到回调地址,并带上code,如图3

        打开postman,进行获取access_token

  1. #访问 /oauth2/token 地址
  2. #在Authorization中选择Basic Auth模式,填入对应客户端,其会在header中生成Authorization,如下图右侧

        返回结果如下

        调用ResourceController中的接口,测试token是否生效

源码下载地址

应多位网友反应,上传源码,部分地方与上文代码有所出入

总结

至此,spring-authorization-server的基础使用已完成,总体上和原Spring Security OAuth大差不差,个别配置项不同。期间在网上搜寻了很多资料,然后进行整合,因此文中存在与其他网上教程相同代码,如有争议,请联系我删除改正,谢谢。由于不太会写文章,我就直接贴出代码,代码中我有加上注释,所以上述文章中,没有很具体的描述,基本就是个人开发流程,若文中有那里写不对,欢迎指教,不喜勿喷。

关于部分,请查看后续相关文章

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发