1. 前言
1.1 软件系统开发定制为什么使用spring-authorization-server?
真实原因:软件系统开发定制原先是因为个人原因,软件系统开发定制需要研究新版鉴权服务,看到了spring-authorization-server,软件系统开发定制使用过程中,软件系统开发定制想着能不能整合新版本cloud,软件系统开发定制因此此处先以springboot搭建spring-authorization-server,软件系统开发定制后续再替换为springcloud2021。
官方原因:原先使用Spring Security OAuth,软件系统开发定制而该项目已经逐渐被淘汰,软件系统开发定制虽然网上还是有不少该方案,软件系统开发定制但秉着技术要随时代更新,从而使用spring-authorization-server
2.软件系统开发定制项目迭代历程
-
引入gateway网关,swagger文档工具
-
待续
3.项目构建
3.1 以springboot搭建spring-authorization-server(即认证与资源服务器)
数据库相关表结构构建
需要创建3张表,sql分别如下
- CREATE TABLE `oauth2_authorization` (
- `id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- `registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- `authorization_grant_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- `attributes` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
- `state` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
- `authorization_code_value` blob NULL,
- `authorization_code_issued_at` timestamp(0) NULL DEFAULT NULL,
- `authorization_code_expires_at` timestamp(0) NULL DEFAULT NULL,
- `authorization_code_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
- `access_token_value` blob NULL,
- `access_token_issued_at` timestamp(0) NULL DEFAULT NULL,
- `access_token_expires_at` timestamp(0) NULL DEFAULT NULL,
- `access_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
- `access_token_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
- `access_token_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
- `oidc_id_token_value` blob NULL,
- `oidc_id_token_issued_at` timestamp(0) NULL DEFAULT NULL,
- `oidc_id_token_expires_at` timestamp(0) NULL DEFAULT NULL,
- `oidc_id_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
- `refresh_token_value` blob NULL,
- `refresh_token_issued_at` timestamp(0) NULL DEFAULT NULL,
- `refresh_token_expires_at` timestamp(0) NULL DEFAULT NULL,
- `refresh_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-
-
- CREATE TABLE `oauth2_authorization_consent` (
- `registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- `authorities` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- PRIMARY KEY (`registered_client_id`, `principal_name`) USING BTREE
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-
-
-
- CREATE TABLE `oauth2_registered_client` (
- `id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- `client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- `client_id_issued_at` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
- `client_secret` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
- `client_secret_expires_at` timestamp(0) NULL DEFAULT NULL,
- `client_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- `client_authentication_methods` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- `authorization_grant_types` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- `redirect_uris` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
- `scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- `client_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- `token_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
先进行认证服务器相关配置
pom.xml引入依赖
注意!!!spring boot版本需2.6.x以上,是为后面升级成cloud做准备
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <version>1.18.22</version>
- </dependency>
-
- <!-- 此依赖是个人公共依赖,你们引入其他具体依赖即可 -->
- <dependency>
- <groupId>com.xxxx.iov</groupId>
- <artifactId>iov-cloud-framework-web</artifactId>
- <version>2.0.0-SNAPSHOT</version>
- <exclusions>
- <!-- 这里是因为公共依赖中的web版本太低,所以移除 -->
- <exclusion>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- <version>2.6.6</version>
- </dependency>
-
- <!-- hutool -->
- <dependency>
- <groupId>cn.hutool</groupId>
- <artifactId>hutool-all</artifactId>
- <version>5.8.0</version>
- </dependency>
-
- <!-- fastjson -->
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.2.39</version>
- </dependency>
-
- <!-- security -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-security</artifactId>
- </dependency>
-
- <!-- oauth2-authorization-server -->
- <dependency>
- <groupId>org.springframework.security</groupId>
- <artifactId>spring-security-oauth2-authorization-server</artifactId>
- <version>0.2.3</version>
- </dependency>
-
- <!-- security-cas -->
- <dependency>
- <groupId>org.springframework.security</groupId>
- <artifactId>spring-security-cas</artifactId>
- </dependency>
-
- <!-- thymeleaf -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-thymeleaf</artifactId>
- </dependency>
-
- <!-- 数据连接池 -->
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>druid-spring-boot-starter</artifactId>
- <version>1.2.9</version>
- </dependency>
-
- <!-- 数据库驱动 -->
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- <version>8.0.28</version>
- </dependency>
-
- <!-- mybatis-plus -->
- <dependency>
- <groupId>com.baomidou</groupId>
- <artifactId>mybatis-plus-boot-starter</artifactId>
- <version>3.5.1</version>
- </dependency>
-
- <!-- guava -->
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- <version>31.1-jre</version>
- </dependency>
创建自定义登录页面 login.html (可不要,使用自带的登录界面)
- <!DOCTYPE html>
- <html lang="en"
- xmlns:th="https://www.thymeleaf.org"
- xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
- <head>
- <meta charset="utf-8">
- <meta name="author" content="test">
- <meta name="viewport" content="width=device-width,initial-scale=1">
- <meta name="description" content="This is a login page template based on Bootstrap 5">
- <title>Login Page</title>
- <style>
- .is-invalid {
- color: red;
- }
-
- .invalid-feedback {
- color: red;
- }
-
- .mb-3 {
- margin-bottom: 3px;
- }
- </style>
- <script th:inline="javascript">
- /*<![CDATA[*/
- // const baseURL = /*[[@{/}]]*/ ''; /*]]>*/
- if (window !== top) {
- top.location.href = location.href;
- }
- </script>
- </head>
- <body class="hold-transition login-page">
- <div class="login-box">
- <div class="card">
- <div class="card-body login-card-body">
- <p class="login-box-msg">Sign in to start your session</p>
- <div th:if="${param.error}" class="alert alert-error">
- Invalid username and password.
- </div>
- <div th:if="${param.logout}" class="alert alert-success">
- You have been logged out.
- </div>
- <form th:action="@{/login}" method="post" id="loginForm">
- <div class="input-group mb-3">
- <input type="text" class="form-control" value="zxg" name="username" placeholder="Email"
- autocomplete="off">
- </div>
- <div class="input-group mb-3">
- <input type="password" id="password" name="password" value="123" class="form-control"
- maxlength="25" placeholder="Password"
- autocomplete="off">
- </div>
- <div class="row">
- <div class="col-4">
- <button type="submit" id="submitBtn">Sign In</button>
- </div>
- </div>
- </form>
- <p class="mb-1">
- <a href="javascript:void(0)">I forgot my password</a>
- </p>
- <p class="mb-0">
- <a href="javascript:void(0)" class="text-center">Register a new membership</a>
- </p>
- </div>
- </div>
- </div>
-
- <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
- <script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/3.1.0/jsencrypt.min.js"></script>
- <script src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.9.0/jquery.validate.min.js"></script>
- <script src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.9.0/additional-methods.min.js"></script>
-
- <script th:inline="javascript">
-
- $(function () {
- var encrypt = new JSEncrypt();
-
- $.validator.setDefaults({
- submitHandler: function (form) {
- console.log("Form successful submitted!");
- form.submit();
- }
- });
-
- });
- </script>
- </body>
- </html>
创建自定义授权页面 consent.html(可不要,可使用自带的授权页面)
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
- <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
- integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
- <title>授权页面</title>
- <style>
- body {
- background-color: aliceblue;
- }
- </style>
- <script>
- function cancelConsent() {
- document.consent_form.reset();
- document.consent_form.submit();
- }
- </script>
- </head>
- <body>
- <div class="container">
- <div class="py-5">
- <h1 class="text-center text-primary">用户授权确认</h1>
- </div>
- <div class="row">
- <div class="col text-center">
- <p>
- 应用
- <a href="https://felord.cn"><span class="font-weight-bold text-primary" th:text="${clientName}"></span></a>
- 想要访问您的账号
- <span class="font-weight-bold" th:text="${principalName}"></span>
- </p>
- </div>
- </div>
- <div class="row pb-3">
- <div class="col text-center"><p>上述应用程序请求以下权限<br/>请审阅以下选项并勾选您同意的权限</p></div>
- </div>
- <div class="row">
- <div class="col text-center">
- <form name="consent_form" method="post" action="/oauth2/authorize">
- <input type="hidden" name="client_id" th:value="${clientId}">
- <input type="hidden" name="state" th:value="${state}">
-
- <div th:each="scope: ${scopes}" class="form-group form-check py-1">
- <input class="form-check-input"
- type="checkbox"
- name="scope"
- th:value="${scope.scope}"
- th:id="${scope.scope}">
- <label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}"></label>
- <p class="text-primary" th:text="${scope.description}"></p>
- </div>
-
- <p th:if="${not #lists.isEmpty(previouslyApprovedScopes)}">您已对上述应用授予以下权限:</p>
- <div th:each="scope: ${previouslyApprovedScopes}" class="form-group form-check py-1">
- <input class="form-check-input"
- type="checkbox"
- th:id="${scope.scope}"
- disabled
- checked>
- <label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}"></label>
- <p class="text-primary" th:text="${scope.description}"></p>
- </div>
-
- <div class="form-group pt-3">
- <button class="btn btn-primary btn-lg" type="submit" id="submit-consent">
- 同意授权
- </button>
- </div>
- <div class="form-group">
- <button class="btn btn-link regular" type="button" id="cancel-consent" onclick="cancelConsent();">
- 取消授权
- </button>
- </div>
- </form>
- </div>
- </div>
- <div class="row pt-4">
- <div class="col text-center">
- <p>
- <small>
- 需要您同意并提供访问权限。
- <br/>如果您不同意,请单击<span class="font-weight-bold text-primary">取消授权</span>,将不会为上述应用程序提供任何您的信息。
- </small>
- </p>
- </div>
- </div>
- </div>
- </body>
- </html>
修改配置文件 application.yml(配置内容可自行简略)
- server:
- port: 9000
-
- spring:
- application:
- name: authorization-server
- thymeleaf:
- cache: false
- datasource:
- url: jdbc:mysql://192.168.1.69:3306/test
- username: root
- password: root
- driver-class-name: com.mysql.cj.jdbc.Driver
- security:
- oauth2:
- resourceserver:
- jwt:
- issuer-uri: http://127.0.0.1:9000 #认证中心端点,作为资源端的配置
-
- application:
- security:
- excludeUrls: #excludeUrls中存放白名单地址
- - "/favicon.ico"
-
- # mybatis plus配置
- mybatis-plus:
- mapper-locations: classpath:/mapper/*Mapper.xml
- global-config:
- # 关闭MP3.0自带的banner
- banner: false
- db-config:
- #主键类型 0:"数据库ID自增", 1:"不操作", 2:"用户输入ID",3:"数字型snowflake", 4:"全局唯一ID UUID", 5:"字符串型snowflake";
- id-type: AUTO
- #字段策略
- insert-strategy: not_null
- update-strategy: not_null
- select-strategy: not_null
- #驼峰下划线w转换
- table-underline: true
- # 逻辑删除配置
- # 逻辑删除全局值(1表示已删除,这也是Mybatis Plus的默认配置)
- logic-delete-value: 1
- # 逻辑未删除全局值(0表示未删除,这也是Mybatis Plus的默认配置)
- logic-not-delete-value: 0
- configuration:
- #驼峰
- map-underscore-to-camel-case: true
- #打开二级缓存
- cache-enabled: true
- # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志
新增认证服务器配置文件 AuthorizationServerConfig
- @Configuration(proxyBeanMethods = false)
- public class AuthorizationServerConfig {
- /**
- * 自定义授权页面
- * 使用系统自带的即不用
- */
- private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
-
- /**
- * 自定义UserDetailsService
- */
- @Autowired
- private UserService userService;
-
-
- /**
- *
- * 使用默认配置进行form表单登录
- * OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
- */
- @Bean
- @Order(Ordered.HIGHEST_PRECEDENCE)
- public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
- OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>();
-
- authorizationServerConfigurer.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI));
-
- RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
-
- http
- .requestMatcher(endpointsMatcher)
- .userDetailsService(userService)
- .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
- .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
- .apply(authorizationServerConfigurer);
- return http.formLogin(Customizer.withDefaults()).build();
- }
-
- /**
- * 注册客户端应用
- */
- @Bean
- public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
- // Save registered client in db as if in-jdbc
- RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
- .clientId("zxg")
- .clientSecret("123")
- .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
- .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
- .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
- .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
- // 回调地址
- .redirectUri("http://www.baidu.com")
- // scope自定义的客户端范围
- .scope(OidcScopes.OPENID)
- .scope("message.read")
- .scope("message.write")
- // client请求访问时需要授权同意
- .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
- // token配置项信息
- .tokenSettings(TokenSettings.builder()
- // token有效期100分钟
- .accessTokenTimeToLive(Duration.ofMinutes(100L))
- // 使用默认JWT相关格式
- .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
- // 开启刷新token
- .reuseRefreshTokens(true)
- // refreshToken有效期120分钟
- .refreshTokenTimeToLive(Duration.ofMinutes(120L))
- .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256).build()
- )
- .build();
-
- // Save registered client in db as if in-memory
- JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
- registeredClientRepository.save(registeredClient);
- return registeredClientRepository;
- }
-
- /**
- * 授权服务:管理OAuth2授权信息服务
- */
- @Bean
- public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
- return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
- }
-
- /**
- * 授权确认信息处理服务
- */
- @Bean
- public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
- return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
- }
-
- /**
- * 加载JWK资源
- * JWT:指的是 JSON Web Token,不存在签名的JWT是不安全的,存在签名的JWT是不可窜改的
- * JWS:指的是签过名的JWT,即拥有签名的JWT
- * JWK:既然涉及到签名,就涉及到签名算法,对称加密还是非对称加密,那么就需要加密的 密钥或者公私钥对。此处我们将 JWT的密钥或者公私钥对统一称为 JSON WEB KEY,即 JWK。
- */
- @Bean
- public JWKSource<SecurityContext> jwkSource() {
- RSAKey rsaKey = JwksUtils.generateRsa();
- JWKSet jwkSet = new JWKSet(rsaKey);
- return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
- }
-
- /**
- * 配置 OAuth2.0 提供者元信息
- */
- @Bean
- public ProviderSettings providerSettings() {
- return ProviderSettings.builder().issuer("http://127.0.0.1:9000").build();
- }
-
- }
新增Security的配置文件WebSecurityConfig
- @Configuration
- @EnableWebSecurity(debug = true) //开启Security
- public class WebSecurityConfig {
- @Autowired
- private ApplicationProperties properties;
-
- /**
- * 设置加密方式
- */
- @Bean
- public PasswordEncoder passwordEncoder() {
- // // 将密码加密方式采用委托方式,默认以BCryptPasswordEncoder方式进行加密,兼容ldap,MD4,MD5等方式
- // return PasswordEncoderFactories.createDelegatingPasswordEncoder();
-
- // 此处我们使用明文方式 不建议这样
- return NoOpPasswordEncoder.getInstance();
- }
-
- /**
- * 使用WebSecurity.ignoring()忽略某些URL请求,这些请求将被Spring Security忽略
- */
- @Bean
- WebSecurityCustomizer webSecurityCustomizer() {
- return new WebSecurityCustomizer() {
- @Override
- public void customize(WebSecurity web) {
- // 读取配置文件application.security.excludeUrls下的链接进行忽略
- web.ignoring().antMatchers(properties.getSecurity().getExcludeUrls().toArray(new String[]{}));
- }
- };
- }
-
- /**
- * 针对http请求,进行拦截过滤
- *
- * CookieCsrfTokenRepository进行CSRF保护的工作方式:
- * 1.客户端向服务器发出GET请求,例如请求主页
- * 2.Spring发送 GET 请求的响应以及 Set-cookie 标头,其中包含安全生成的XSRF令牌
- */
- @Bean
- public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
- httpSecurity
- .authorizeRequests(authorizeRequests ->
- authorizeRequests.antMatchers("/login").permitAll()
- .anyRequest().authenticated()
- )
-
- //使用默认登录页面
- //.formLogin(withDefaults())
-
- //设置form登录,设置且放开登录页login
- .formLogin(fromlogin -> fromlogin.loginPage("/login").permitAll())
-
- // Spring Security CSRF保护
- .csrf(csrfToken -> csrfToken.csrfTokenRepository(new CookieCsrfTokenRepository()))
-
- // //开启认证服务器的资源服务器相关功能,即需校验token
- // .oauth2ResourceServer()
- // .accessDeniedHandler(new SimpleAccessDeniedHandler())
- // .authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
- // .jwt()
- ;
- return httpSecurity.build();
- }
-
- }
新增读取application配置的类 ApplicationProperties
- /**
- * 此步主要是获取配置文件中配置的白名单,可自行舍去或自定义实现其他方式
- **/
- @Data
- @Component
- @ConfigurationProperties("application")
- public class ApplicationProperties {
- private final Security security = new Security();
-
- @Data
- public static class Security {
- private Oauth2 oauth2;
- private List<String> excludeUrls = new ArrayList<>();
-
- @Data
- public static class Oauth2 {
- private String issuerUrl;
-
- }
- }
- }
新增 JwksUtils 类和 KeyGeneratorUtils,这两个类作为JWT对称加密
- public final class JwksUtils {
-
- private JwksUtils() {
- }
-
- /**
- * 生成RSA加密key (即JWK)
- */
- public static RSAKey generateRsa() {
- // 生成RSA加密的key
- KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
- // 公钥
- RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
- // 私钥
- RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
- // 构建RSA加密key
- return new RSAKey.Builder(publicKey)
- .privateKey(privateKey)
- .keyID(UUID.randomUUID().toString())
- .build();
- }
-
- /**
- * 生成EC加密key (即JWK)
- */
- public static ECKey generateEc() {
- // 生成EC加密的key
- KeyPair keyPair = KeyGeneratorUtils.generateEcKey();
- // 公钥
- ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
- // 私钥
- ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
- // 根据公钥参数生成曲线
- Curve curve = Curve.forECParameterSpec(publicKey.getParams());
- // 构建EC加密key
- return new ECKey.Builder(curve, publicKey)
- .privateKey(privateKey)
- .keyID(UUID.randomUUID().toString())
- .build();
- }
-
- /**
- * 生成HmacSha256密钥
- */
- public static OctetSequenceKey generateSecret() {
- SecretKey secretKey = KeyGeneratorUtils.generateSecretKey();
- return new OctetSequenceKey.Builder(secretKey)
- .keyID(UUID.randomUUID().toString())
- .build();
- }
- }
-
-
- class KeyGeneratorUtils {
-
- private KeyGeneratorUtils() {
- }
-
- /**
- * 生成RSA密钥
- */
- static KeyPair generateRsaKey() {
- KeyPair keyPair;
- try {
- KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
- keyPairGenerator.initialize(2048);
- keyPair = keyPairGenerator.generateKeyPair();
- } catch (Exception ex) {
- throw new IllegalStateException(ex);
- }
- return keyPair;
- }
-
- /**
- * 生成EC密钥
- */
- static KeyPair generateEcKey() {
- EllipticCurve ellipticCurve = new EllipticCurve(
- new ECFieldFp(
- new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951")),
- new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
- new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291"));
- ECPoint ecPoint = new ECPoint(
- new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
- new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109"));
- ECParameterSpec ecParameterSpec = new ECParameterSpec(
- ellipticCurve,
- ecPoint,
- new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"),
- 1);
-
- KeyPair keyPair;
- try {
- KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
- keyPairGenerator.initialize(ecParameterSpec);
- keyPair = keyPairGenerator.generateKeyPair();
- } catch (Exception ex) {
- throw new IllegalStateException(ex);
- }
- return keyPair;
- }
-
- /**
- * 生成HmacSha256密钥
- */
- static SecretKey generateSecretKey() {
- SecretKey hmacKey;
- try {
- hmacKey = KeyGenerator.getInstance("HmacSha256").generateKey();
- } catch (Exception ex) {
- throw new IllegalStateException(ex);
- }
- return hmacKey;
- }
- }
新建 ConsentController,编写登录和认证页面的跳转
如果在上面没有使用自定义的登录和授权页面,下面的跳转方法按需舍去
- @Slf4j
- @Controller
- public class ConsentController {
-
- private final RegisteredClientRepository registeredClientRepository;
- private final OAuth2AuthorizationConsentService authorizationConsentService;
-
- public ConsentController(RegisteredClientRepository registeredClientRepository,
- OAuth2AuthorizationConsentService authorizationConsentService) {
- this.registeredClientRepository = registeredClientRepository;
- this.authorizationConsentService = authorizationConsentService;
- }
-
- @ResponseBody
- @GetMapping("/favicon.ico")
- public String faviconico(){
- return "favicon.ico";
- }
-
- @GetMapping("/login")
- public String loginPage(){
- return "login";
- }
-
- @GetMapping(value = "/oauth2/consent")
- public String consent(Principal principal, Model model,
- @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
- @RequestParam(OAuth2ParameterNames.SCOPE) String scope,
- @RequestParam(OAuth2ParameterNames.STATE) String state) {
-
- // Remove scopes that were already approved
- Set<String> scopesToApprove = new HashSet<>();
- Set<String> previouslyApprovedScopes = new HashSet<>();
- RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
- OAuth2AuthorizationConsent currentAuthorizationConsent =
- this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());
- Set<String> authorizedScopes;
- if (currentAuthorizationConsent != null) {
- authorizedScopes = currentAuthorizationConsent.getScopes();
- } else {
- authorizedScopes = Collections.emptySet();
- }
- for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
- if (authorizedScopes.contains(requestedScope)) {
- previouslyApprovedScopes.add(requestedScope);
- } else {
- scopesToApprove.add(requestedScope);
- }
- }
-
- model.addAttribute("clientId", clientId);
- model.addAttribute("state", state);
- model.addAttribute("scopes", withDescription(scopesToApprove));
- model.addAttribute("previouslyApprovedScopes", withDescription(previouslyApprovedScopes));
- model.addAttribute("principalName", principal.getName());
-
- return "consent";
- }
-
- private static Set<ScopeWithDescription> withDescription(Set<String> scopes) {
- Set<ScopeWithDescription> scopeWithDescriptions = new HashSet<>();
- for (String scope : scopes) {
- scopeWithDescriptions.add(new ScopeWithDescription(scope));
-
- }
- return scopeWithDescriptions;
- }
-
- public static class ScopeWithDescription {
- private static final String DEFAULT_DESCRIPTION = "UNKNOWN SCOPE - We cannot provide information about this permission, use caution when granting this.";
- private static final Map<String, String> scopeDescriptions = new HashMap<>();
- static {
- scopeDescriptions.put(
- "message.read",
- "This application will be able to read your message."
- );
- scopeDescriptions.put(
- "message.write",
- "This application will be able to add new messages. It will also be able to edit and delete existing messages."
- );
- scopeDescriptions.put(
- "other.scope",
- "This is another scope example of a scope description."
- );
- }
-
- public final String scope;
- public final String description;
-
- ScopeWithDescription(String scope) {
- this.scope = scope;
- this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION);
- }
- }
-
- }
新建 UserController,User,UserService等标准的自定义用户业务,此处仅放出UserServiceImpl
- @RequiredArgsConstructor
- @Slf4j
- @Component
- class UserServiceImpl implements UserService {
- private final UserMapper userMapper;
-
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername,username));
- return new org.springframework.security.core.userdetails.User(username, user.getPassword(), new ArrayList<>());
- }
- }
启动项目,如下图
认证服务器整体结构图
----------------------------------------------------------------------------------------------------------------------------------------------------------
资源服务器相关配置
pom.xml引入资源服务器相关依赖
- <!-- resource-server资源服务器 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
- </dependency>
-
- <!-- security -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-security</artifactId>
- </dependency>
新增配置文件 application.yaml
- server:
- port: 9003
- spring:
- application:
- name: resource
- security:
- oauth2:
- resourceserver:
- jwt:
- issuer-uri: http://127.0.0.1:9000
- feign:
- client:
- config:
- default: #配置超时时间
- connect-timeout: 10000
- read-timeout: 10000
新增资源服务器配置文件 ResourceServerConfiguration
- @Configuration
- @EnableWebSecurity(debug = true)
- @EnableGlobalMethodSecurity(prePostEnabled = true) //开启鉴权服务
- public class ResourceServerConfiguration {
-
- @Bean
- public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
- // 所有请求都进行拦截
- httpSecurity.authorizeRequests().anyRequest().authenticated();
- // 关闭session
- httpSecurity.sessionManagement().disable();
- // 配置资源服务器的无权限,无认证拦截器等 以及JWT验证
- httpSecurity.oauth2ResourceServer()
- .accessDeniedHandler(new SimpleAccessDeniedHandler())
- .authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
- .jwt();
- return httpSecurity.build();
- }
-
- }
新增相关无认证无权限统一拦截回复 SimpleAccessDeniedHandler 和 SimpleAuthenticationEntryPoint
- /**
- * 携带了token 而且token合法 但是权限不足以访问其请求的资源 403
- * @author zxg
- */
- public class SimpleAccessDeniedHandler implements AccessDeniedHandler {
-
- @Override
- public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
- response.setStatus(HttpServletResponse.SC_FORBIDDEN);
- response.setCharacterEncoding("utf-8");
- response.setContentType(MediaType.APPLICATION_JSON_VALUE);
- ObjectMapper objectMapper = new ObjectMapper();
- String resBody = objectMapper.writeValueAsString(SingleResultBundle.failed("无权访问"));
- PrintWriter printWriter = response.getWriter();
- printWriter.print(resBody);
- printWriter.flush();
- printWriter.close();
- }
- }
-
-
- /**
- * 在资源服务器中 不携带token 或者token无效 401
- * @author zxg
- */
- @Slf4j
- public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint {
- @Override
- public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
- if (response.isCommitted()){
- return;
- }
-
- Throwable throwable = authException.fillInStackTrace();
-
- String errorMessage = "认证失败";
-
- if (throwable instanceof BadCredentialsException){
- errorMessage = "错误的客户端信息";
- }else {
- Throwable cause = authException.getCause();
-
- if (cause instanceof JwtValidationException) {
- log.warn("JWT Token 过期,具体内容:" + cause.getMessage());
- errorMessage = "无效的token信息";
- } else if (cause instanceof BadJwtException){
- log.warn("JWT 签名异常,具体内容:" + cause.getMessage());
- errorMessage = "无效的token信息";
- } else if (cause instanceof AccountExpiredException){
- errorMessage = "账户已过期";
- } else if (cause instanceof LockedException){
- errorMessage = "账户已被锁定";
- // } else if (cause instanceof InvalidClientException || cause instanceof BadClientCredentialsException){
- // response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed(401,"无效的客户端")));
- // } else if (cause instanceof InvalidGrantException || cause instanceof RedirectMismatchException){
- // response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("无效的类型")));
- // } else if (cause instanceof UnauthorizedClientException) {
- // response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("未经授权的客户端")));
- } else if (throwable instanceof InsufficientAuthenticationException) {
- String message = throwable.getMessage();
- if (message.contains("Invalid token does not contain resource id")){
- errorMessage = "未经授权的资源服务器";
- }else if (message.contains("Full authentication is required to access this resource")){
- errorMessage = "缺少验证信息";
- }
- }else {
- errorMessage = "验证异常";
- }
- }
-
- response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
- response.setCharacterEncoding("utf-8");
- response.setContentType(MediaType.APPLICATION_JSON_VALUE);
- ObjectMapper objectMapper = new ObjectMapper();
- String resBody = objectMapper.writeValueAsString(SingleResultBundle.failed(errorMessage));
- PrintWriter printWriter = response.getWriter();
- printWriter.print(resBody);
- printWriter.flush();
- printWriter.close();
- }
- }
新增 ResourceController 进行接口测试
- @Slf4j
- @RestController
- public class ResourceController {
-
- /**
- * 测试Spring Authorization Server,测试权限
- */
- @PreAuthorize("hasAuthority('SCOPE_message.read')")
- @GetMapping("/getTest")
- public String getTest(){
- return "getTest";
- }
-
- /**
- * 默认登录成功跳转页为 / 防止404状态
- *
- * @return the map
- */
- @GetMapping("/")
- public Map<String, String> index() {
- return Collections.singletonMap("msg", "login success!");
- }
-
- @GetMapping("/getResourceTest")
- public SingleResultBundle<String> getResourceTest(){
- return SingleResultBundle.success("这是resource的测试方法 getResourceTest()");
- }
- }
启动项目,效果如下
项目总体结构如下
测试认证鉴权
- #调用 /oauth2/authorize ,获取code
- http://127.0.0.1:9000/oauth2/authorize?client_id=zxg&response_type=code&scope=message.read&redirect_uri=http://www.baidu.com
- #会判断是否登录,若没有,则跳转到登录页面,如下图1
- #登录完成后,会提示是否授权,若没有,则跳转到授权界面,如下图2
- #授权成功后,跳转到回调地址,并带上code,如图3
打开postman,进行获取access_token
- #访问 /oauth2/token 地址
- #在Authorization中选择Basic Auth模式,填入对应客户端,其会在header中生成Authorization,如下图右侧
返回结果如下
调用ResourceController中的接口,测试token是否生效
源码下载地址
应多位网友反应,上传源码,部分地方与上文代码有所出入
总结
至此,spring-authorization-server的基础使用已完成,总体上和原Spring Security OAuth大差不差,个别配置项不同。期间在网上搜寻了很多资料,然后进行整合,因此文中存在与其他网上教程相同代码,如有争议,请联系我删除改正,谢谢。由于不太会写文章,我就直接贴出代码,代码中我有加上注释,所以上述文章中,没有很具体的描述,基本就是个人开发流程,若文中有那里写不对,欢迎指教,不喜勿喷。
关于部分,请查看后续相关文章