自定义同意授权页面
TIP
自定义授权服务器同意授权页面页面扩展
效果图

consent.html
文件目录:src\main\resources\templates\consent.html
html
<!--
~ Copyright [yyyy] [name of copyright owner]
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>需要同意 | PIGX 微服务开发平台</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.css" th:href="@{/webjars/bootstrap/css/bootstrap.css}" />
<script>
function cancelConsent() {
document.consent_form.reset();
document.consent_form.submit();
}
</script>
</head>
<body>
<div class="container">
<div class="row py-5">
<h1 class="text-center text-primary">应用程序权限</h1>
</div>
<div class="row">
<div class="col text-center">
<p>
应用程序
<span class="fw-bold text-primary" th:text="${clientId}"></span>
想要访问您的帐户
<span class="fw-bold" th:text="${principalName}"></span>
</p>
</div>
</div>
<div th:if="${userCode}" class="row">
<div class="col text-center">
<p class="alert alert-warning">
You have provided the code
<span class="fw-bold" th:text="${userCode}"></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" th:action="${requestURI}">
<input type="hidden" name="client_id" th:value="${clientId}">
<input type="hidden" name="state" th:value="${state}">
<input th:if="${userCode}" type="hidden" name="user_code" th:value="${userCode}">
<div th:each="scope: ${scopes}" class="form-check py-1">
<input class="form-check-input"
style="float: none"
type="checkbox"
name="scope"
th:value="${scope.scope}"
th:id="${scope.scope}">
<label class="form-check-label fw-bold px-2" 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-check py-1">
<input class="form-check-input"
style="float: none"
type="checkbox"
th:id="${scope.scope}"
disabled
checked>
<label class="form-check-label fw-bold px-2" th:for="${scope.scope}" th:text="${scope.scope}"></label>
<p class="text-primary" th:text="${scope.description}"></p>
</div>
<div class="pt-3">
<button class="btn btn-primary btn-lg" type="submit" id="submit-consent">
确认授权
</button>
</div>
<div class="pt-3">
<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/>
如果您不批准,请单击“取消”,在这种情况下,将不会与应用程序共享任何信息。
</small>
</p>
</div>
</div>
</div>
</body>
</html>AuthorizationConsentController.java
java
/*
* Copyright [yyyy] [name of copyright owner]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pigcloud.pigx.auth.web;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.security.Principal;
import java.util.*;
/**
* 认证服务器相关自定接口
*
* @author Daniel Garnier-Moiroux
*/
@Controller
public class AuthorizationConsentController {
/**
* 客户端repository
*/
private final RegisteredClientRepository registeredClientRepository;
/**
* 授权确认管理服务
*/
private final OAuth2AuthorizationConsentService authorizationConsentService;
/**
* 参数注入
*
* @param registeredClientRepository registeredClientRepository
* @param authorizationConsentService authorizationConsentService
*/
public AuthorizationConsentController(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationConsentService authorizationConsentService) {
this.registeredClientRepository = registeredClientRepository;
this.authorizationConsentService = authorizationConsentService;
}
/**
* 自定义确认授权页面
*
* @param principal principal
* @param model model
* @param clientId clientId
* @param scope scope
* @param state state
* @param userCode userCode
* @return String
*/
@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,
@RequestParam(name = OAuth2ParameterNames.USER_CODE, required = false) String userCode) {
// 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 (OidcScopes.OPENID.equals(requestedScope)) {
continue;
}
if (authorizedScopes.contains(requestedScope)) {
previouslyApprovedScopes.add(requestedScope);
} else {
scopesToApprove.add(requestedScope);
}
}
model.addAttribute("clientId", clientId);
model.addAttribute("clientName", registeredClient.getClientName());
model.addAttribute("state", state);
model.addAttribute("scopes", withDescription(scopesToApprove));
model.addAttribute("previouslyApprovedScopes", withDescription(previouslyApprovedScopes));
model.addAttribute("principalName", principal.getName());
model.addAttribute("userCode", userCode);
if (StringUtils.hasText(userCode)) {
model.addAttribute("requestURI", "/oauth2/device_verification");
} else {
model.addAttribute("requestURI", "/oauth2/authorize");
}
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 {
/**
* DEFAULT_DESCRIPTION
*/
private static final String DEFAULT_DESCRIPTION = "UNKNOWN SCOPE - 我们无法提供有关此权限的信息,请在授予此权限时小心。";
/**
* scopeDescriptions
*/
private static final Map<String, String> scopeDescriptions = new HashMap<>();
static {
scopeDescriptions.put(
OidcScopes.PROFILE,
"此应用程序将能够读取您的个人资料信息。"
);
scopeDescriptions.put(
"message.read",
"此应用程序将能够读取您的消息。"
);
scopeDescriptions.put(
"message.write",
"此应用程序将能够添加新消息。它还可以编辑和删除现有消息。"
);
scopeDescriptions.put(
"other.scope",
"这是范围描述的另一个范围示例。"
);
}
/**
* scope
*/
public final String scope;
/**
* description
*/
public final String description;
ScopeWithDescription(String scope) {
this.scope = scope;
this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION);
}
}
}AuthorizationServerConfig.java
java
/*
* Copyright [yyyy] [name of copyright owner]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pigcloud.pigx.auth.config;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
/**
* 授权服务器配置
*
* @author heyuq
*/
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
/**
* 自定义同意授权页面。
*/
private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
/**
* 授权服务器端点配置
*
* @param http spring security核心配置类
* @return 过滤器链
* @throws Exception 抛出
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
// 应用OAuth2授权服务器的默认安全配置。
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// 配置OAuth2授权服务器的安全过滤链,启用OpenID Connect 1.0。
// OpenID Connect是OAuth 2.0的一个扩展,用于添加用户身份验证。
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
// 设置自定义用户确认授权页
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint
.consentPage(CUSTOM_CONSENT_PAGE_URI)
)
.oidc(Customizer.withDefaults());
// @formatter:off
http
// 配置异常处理,当从授权端点未认证时,重定向到登录页面。
// 这里定义了一个登录URL认证入口点,并指定它只适用于TEXT_HTML媒体类型。
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// 配置OAuth2资源服务器,接受用户信息和/或客户端注册的访问令牌。
// 这里使用JWT(JSON Web Tokens)作为访问令牌的格式。
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
// @formatter:on
// 构建并返回配置好的SecurityFilterChain对象。
return http.build();
}
/**
* 默认安全过滤器配置
*
* @param http spring security核心配置类
* @return 过滤器链
* @throws Exception 抛出
*/
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
// @formatter:off
http
// 配置默认的HTTP请求授权规则,要求所有请求都必须经过身份验证。
.authorizeHttpRequests((authorize) -> authorize
// 放行静态资源
.requestMatchers("favicon.svg", "/assets/**", "/webjars/**", "/error", "/login").permitAll()
.anyRequest().authenticated()
)
// 配置表单登录,使用默认设置。
// 这处理从授权服务器过滤链重定向到登录页面的情况。
// .formLogin(Customizer.withDefaults())
.formLogin(formLogin ->
formLogin
.loginPage("/login")
);
// @formatter:on
return http.build();
}
/**
* UserDetailsService是Spring Security中的一个接口,用于加载用户特定的数据。
*
* @return UserDetailsService
*/
@Bean
public UserDetailsService userDetailsService() {
// 创建一个具有默认密码编码器的用户详情对象。
// 这里定义了一个用户名为"user",密码为"password",角色为"USER"的用户。
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
// 返回一个基于内存的用户详情服务,它仅包含上面定义的用户。
return new InMemoryUserDetailsManager(userDetails);
}
/**
* 注册一个 OAuth 2.0客户端
*
* @return RegisteredClientRepository实例
*/
@Bean
public RegisteredClientRepository registeredClientRepository() {
// 创建一个RegisteredClient对象,并设置其属性。
// 使用UUID生成一个唯一的客户端ID。
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
// 设置客户端ID为"messaging-client"。
.clientId("messaging-client")
// 设置客户端秘密为"{noop}secret",这里的"{noop}"表示不加密。
.clientSecret("{noop}secret")
// 设置客户端认证方法为基于客户端秘密的基本认证。
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 设置授权类型为授权码。
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
// 添加刷新令牌授权类型。
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// 添加客户端凭证授权类型。
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 设置授权码流程的重定向URI。
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
// 设置另一个重定向URI,可能用于其他授权流程。
.redirectUri("http://127.0.0.1:8080/authorized")
// 设置用户注销后的重定向URI。
.postLogoutRedirectUri("http://127.0.0.1:8080/logged-out")
// 添加OpenID范围。
.scope(OidcScopes.OPENID)
// 添加Profile范围。
.scope(OidcScopes.PROFILE)
// 添加自定义范围"message.read"。
.scope("message.read")
// 添加自定义范围"message.write"。
.scope("message.write")
// 设置客户端设置,要求每次都需要用户授权同意。
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build(); // 构建RegisteredClient对象。
// 使用上面创建的RegisteredClient对象,创建一个新的InMemoryRegisteredClientRepository实例,并返回它。
// InMemoryRegisteredClientRepository是一个简单的内存中的存储库,用于存储和检索RegisteredClient对象。
return new InMemoryRegisteredClientRepository(registeredClient);
}
/**
* 存储新授权同意和查询现有授权同意的核心组件。它主要由实现 OAuth2 授权请求流(例如 authorization_code 授权)的组件使用
*
* @return JdbcOAuth2AuthorizationConsentService
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService() {
// Will be used by the ConsentController
return new InMemoryOAuth2AuthorizationConsentService();
}
/**
* 配置jwk源,使用非对称加密,公开用于检索匹配指定选择器的JWK的方法
*
* @return JWKSource
*/
@Bean
public JWKSource<SecurityContext> jwkSource() {
// 生成RSA密钥对。
KeyPair keyPair = generateRsaKey();
// 从密钥对中获取公钥。
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
// 从密钥对中获取私钥。
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 使用公钥和私钥创建一个RSAKey对象,并为其分配一个随机的UUID作为keyID。
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// 创建一个包含这个RSAKey的JWKSet对象。
JWKSet jwkSet = new JWKSet(rsaKey);
// 返回一个不可变的JWKSet对象,这通常是为了保证安全而设计的。
return new ImmutableJWKSet<>(jwkSet);
}
/**
* 私有方法,用于生成RSA密钥对。
*
* @return KeyPair
*/
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
// 实例化一个RSA密钥对生成器。
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
// 初始化密钥对生成器,指定密钥长度为2048位。
keyPairGenerator.initialize(2048);
// 生成RSA密钥对。
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
// 如果发生异常,则抛出IllegalStateException。
throw new IllegalStateException(ex);
}
// 返回生成的密钥对。
return keyPair;
}
/**
* 用于解码签名访问令牌的 JwtDecoder 实例。
*
* @param jwkSource jwk源
* @return JwtDecoder
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
// 使用OAuth2AuthorizationServerConfiguration类中的静态方法jwtDecoder,
// 根据提供的JWKSource对象创建JwtDecoder。
// JwtDecoder是用于解码JWT的组件。
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* 配置OAuth 2.0授权服务器。
*
* @return AuthorizationServerSettings
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
// 创建一个AuthorizationServerSettings对象,并使用默认设置。
// 这个对象通常用于配置授权服务器的各种设置。
return AuthorizationServerSettings.builder().build();
}
}