Spring Authorization Server(二)自定义授权登录页面
目录
自定义登录授权页面
感谢登录和授权页面是复制的别人的:感谢 雪天前端 和 dante-engine 开源项目提供的前端页面
下面的多出配置都是更具官网demo项目改变而来
新增项目依赖
在上次的项目基础上增加新的以来
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>3.2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>animate.css</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>font-awesome</artifactId>
<version>6.5.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.7.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery-backstretch</artifactId>
<version>2.1.16</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>uniform</artifactId>
<version>2.1.2-1</version>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>mdi__font</artifactId>
<version>7.4.47</version>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>vue</artifactId>
<version>3.4.15</version>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>quasar</artifactId>
<version>2.14.0</version>
</dependency>
修改相关配置自定义登录授权页面
这里主要是 因为我们要自定义修改授权页面就要修改 SecurityFilterChain 默认的配置
以前的配置是:
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
// http
// .authorizeHttpRequests((authorize) -> authorize
// .anyRequest().authenticated()
// )
// .csrf(AbstractHttpConfigurer::disable)
// // Form login handles the redirect to the login page from the
// // authorization server filter chain
// .formLogin(Customizer.withDefaults());
return http.build();
}
修改为我们自定义的登录页面 路径: /custom/login
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
// http
// .authorizeHttpRequests((authorize) -> authorize
// .anyRequest().authenticated()
// )
// .csrf(AbstractHttpConfigurer::disable)
// // Form login handles the redirect to the login page from the
// // authorization server filter chain
// .formLogin(Customizer.withDefaults());
http
.authorizeHttpRequests(authorize ->
authorize
.requestMatchers("/webjars/**","/assets/**", "/login", "/custom/login").permitAll()
.anyRequest().authenticated()
)
.formLogin(formLogin ->
formLogin
.loginPage("/custom/login")
)
.oauth2Login(oauth2Login ->
oauth2Login
.loginPage("/custom/login")
// .successHandler(new FederatedIdentityAuthenticationSuccessHandler())
);
return http.build();
}
新增加bean
cn.com.wuhm.authorization.server.springboot3authorizationserver.config.SecurityConfig
@Bean
public ClientRegistrationRepository clientRegistrationRepository(){
ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("oidc")
.clientId("oidc-client")
.clientSecret("{noop}secret")
.scope("openid", "profile", "email")
.authorizationUri("https://localhost:9000/oauth2/auth")
.tokenUri("https://localhost:9000/oauth2/v4/token")
.userInfoUri("https://localhost:9000/v3/userinfo")
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.userNameAttributeName(IdTokenClaimNames.SUB)
.clientName("Google")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.build();
return new InMemoryClientRegistrationRepository(clientRegistration);
}
因为我们修改了 oauth2Login 的默认配置,在启动项目的时候他需要一个 ClientRegistrationRepository bean
同时我们修改oauthorization-server相关配置
cn.com.wuhm.authorization.server.springboot3authorizationserver.config.SecurityConfig
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, OAuth2TokenGenerator<?> tokenGenerator)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
// 自定义授权页面
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint
.consentPage("/custom/consent")
)
// OAuth2 Token Endpoint提供了自定义OAuth2授权端点的能力。它定义了扩展点,允许您自定义OAuth2授权请求的预处理、主处理和后处理逻辑。
.tokenEndpoint(tokenEndpoint->
tokenEndpoint
// 添加自定义的认证方式
.accessTokenRequestConverters(authenticationConverters ->
authenticationConverters.addAll(
List.of(new CustomAuthenticationConverter())
))
// 添加自定义
.authenticationProviders(providers -> providers.addAll(List.of(new CustomAuthenticationProvider(oAuth2AuthorizationService(), tokenGenerator))))
)
// Enable OpenID Connect 1.0
.oidc(Customizer.withDefaults());
http
// Redirect to the login page when not authenticated from the
// authorization endpoint
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
// new LoginUrlAuthenticationEntryPoint("/login"),
// 需要重定向的前端页面请求路径
new LoginUrlAuthenticationEntryPoint("/custom/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// Accept access tokens for User Info and/or Client Registration
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
编写前端控制器
这里的控制器可以更具官网demo项目直接复制过来稍作修改
cn.com.wuhm.authorization.server.springboot3authorizationserver.controller.Oauth2Controller
@Slf4j
@Controller
@RequestMapping("/")
@RequiredArgsConstructor
public class Oauth2Controller {
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationConsentService authorizationConsentService;
@GetMapping("/custom/login")
public String login(){
return "login";
}
@GetMapping("/custom/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) {
// 待授权的scope
Set<String> scopesToApprove = new HashSet<>();
// 之前已经授权过的scope
Set<String> previouslyApprovedScopes = new HashSet<>();
// 获取客户端注册信息
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
// 获取当前Client下用户之前的consent信息
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(clientId, principal.getName());
// 当前Client下用户已经授权的scope
Set<String> authorizedScopes = Optional.ofNullable(currentAuthorizationConsent)
.map(OAuth2AuthorizationConsent::getScopes)
.orElse(Collections.emptySet());
// 遍历请求的scope,提取之前已授权过 和 待授权的scope
for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
if (OidcScopes.OPENID.equals(requestedScope)) {
continue;
}
if (authorizedScopes.contains(requestedScope)) {
previouslyApprovedScopes.add(requestedScope);
} else {
scopesToApprove.add(requestedScope);
}
}
Set<String> redirectUris = registeredClient.getRedirectUris();
model.addAttribute("clientId", clientId);
model.addAttribute("state", state);
model.addAttribute("scopesToAuthorize", withDescription(scopesToApprove));
model.addAttribute("scopesPreviouslyAuthorized", withDescription(previouslyApprovedScopes));
model.addAttribute("principalName", principal.getName());
model.addAttribute("applicationName", "积至");
model.addAttribute("logo", "https://ts1.cn.mm.bing.net/th/id/R-C.21f651a9b7d96be274f1f0784874b07b?rik=eDRLwTlVgtRWKA&riu=http%3a%2f%2fimg95.699pic.com%2fphoto%2f50089%2f8326.jpg_wh860.jpg&ehk=Hcu8hyvYqUSgjHkijXmJnqZxc%2fvu1KwXGd3wsSLR8Bo%3d&risl=&pid=ImgRaw&r=0");
model.addAttribute("redirectUri", redirectUris.iterator().next());
model.addAttribute("userCode", userCode);
if (StringUtils.hasText(userCode)) {
model.addAttribute("action", "/oauth2/authorize");
}
model.addAttribute("action", "/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 {
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(
OidcScopes.PROFILE,
"This application will be able to read your profile information."
);
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(
"user.read",
"This application will be able to read your user information."
);
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);
}
}
@GetMapping("/hello")
@ResponseBody
public String hello(){
return "hello";
}
}
编写前端页面
resources/templates 目录下
登录页面
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>登录</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 100vh;
background: linear-gradient(to right, #6af4ff, #ffffff);
}
.Box {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 550px;
height: 330px;
display: flex;
}
form {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -60%);
width: 80%;
text-align: center;
}
h3 {
font-size: 24px;
margin-bottom: 20px;
letter-spacing: 5px;
}
input {
width: 100%;
height: 38px;
border: 1px solid #000000;
background-color: transparent;
padding-left: 10px;
font-size: 12px;
color: #000000;
margin-bottom: 15px;
outline: none;
}
.desc {
margin: 0px 20px 30px;
text-align: center;
font-size: 12px;
color: #828282;
}
.loginBtn {
width: 100%;
line-height: 36px;
text-align: center;
font-size: 15px;
color: #fff;
background: rgb(57, 99, 134);
outline: none;
border: none;
margin-top: 10px;
}
.no {
display: flex;
justify-content: space-between;
cursor: pointer;
text-align: center;
font-size: 12px;
color: #828282;
}
</style>
</head>
<body>
<div class="Box">
<form th:action="@{/custom/login}" method="post">
<h3>欢迎登录</h3>
<p class="desc">WELCOME LOGIN</p>
<input type="text" id="username" name="username" placeholder="请输入账号" required>
<input type="password" id="password" name="password" placeholder="请输入密码" required>
<input type="submit" class="loginBtn" value="登录"></button>
<p class="no">
<span>忘记密码</span>
<span>没有账号?立即注册</span>
</p>
</form>
</div>
</body>
</html>
需要注意的是修改form
的action
属性值: /custom/login
授权页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>授权登录</title>
<link rel="stylesheet" type="text/css" href="/webjars/mdi__font/7.4.47/materialdesignicons.min.css" th:href="@{/webjars/mdi__font/7.4.47/css/materialdesignicons.min.css}" />
<link rel="stylesheet" type="text/css" href="/webjars/quasar/2.14.0/dist/quasar.prod.css" th:href="@{/webjars/quasar/2.14.0/dist/quasar.prod.css}"/>
</head>
<body>
<!-- example of injection point where you write your app template -->
<div id="q-app">
<q-layout class="bg-grey-2">
<q-page-container>
<q-page padding class="flex justify-center items-start q-pa-none">
<q-list class="column items-center" style="min-width: 500px">
<!-- <q-item>-->
<!-- <img th:src="@{/herodotus/custom/images/logo.png}" height="240" width="320"/>-->
<!-- </q-item>-->
<q-item>
<div class="text-h5 text-weight-bold">
授权 [[${applicationName}]]
</div>
</q-item>
<q-item>
<q-card flat bordered style="min-width: 500px" >
<q-form name="consent_form" method="post" th:action="@{${action}}">
<q-card-section>
<q-list>
<q-item>
<q-item-section avatar>
<q-avatar rounded th:with="condition=${logo ne null}">
<img th:if="${condition}" th:src="@{${logo}}"/>
<img th:unless="${condition}" th:src="@{/herodotus/custom/images/boy-avatar.png}"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label lines="1">
<span class="text-weight-bold text-primary">[[${applicationName}]]</span>
</q-item-label>
<q-item-label lines="1">
想要访问您的
<span class="text-weight-bold">[[${principalName}]]</span>
账户
</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="code">
<q-item-section>
<q-banner rounded class="bg-orange text-white">
您已经提供代码
<span class="text-weight-bold"> {{code}}</span
>。验证此代码是否与设备上显示内容匹配
</q-banner>
</q-item-section>
</q-item>
<q-item-label header>
上述应用程序请求以下权限。如果您同意,请予以授权。
</q-item-label>
<q-item>
<q-item-section>
<q-option-group name="scope" v-model="selectAuthorizeScopes" :options="authorizeScopesOptions" color="primary" type="checkbox" />
</q-item-section>
</q-item>
<template th:if="${not #sets.isEmpty(scopesPreviouslyAuthorized)}">
<q-item-label header>
您已向上述应用程序授予以下权限:
</q-item-label>
<q-item>
<q-item-section>
<q-option-group name="scope" v-model="selectPreviouslyAuthorizedScopes" :options="previouslyAuthorizedScopesOptions" color="primary" type="checkbox" />
</q-item-section>
</q-item>
</template>
<q-item-label header>
如果您不授权,请单击“取消”,将不会与应用程序共享任何信息。
</q-item-label>
</q-list>
</q-card-section>
<q-separator></q-separator>
<q-card-section>
<q-list>
<q-item>
<q-item-section>
<div class="row justify-between q-gutter-md">
<div class="col">
<q-btn color="grey" label="取消" type="reset" class="full-width" />
</div>
<div class="col">
<q-btn color="primary" label="授权" type="submit" class="full-width" />
</div>
</div>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
<q-card-section>
<q-list class="column items-center">
<div class="text-subtitle2">授权将重定向到</div>
<div class="text-subtitle2 text-weight-bold text-center q-mt-xs" style="width: 400px; word-break: break-all">
[[${redirectUri}]]
</div>
</q-list>
</q-card-section>
<input type="hidden" name="client_id" th:value="${clientId}">
<input type="hidden" name="state" th:value="${state}">
<input v-if="code" type="hidden" name="user_code" th:value="${userCode}">
</q-form>
</q-card>
</q-item>
</q-list>
</q-page>
</q-page-container>
</q-layout>
</div>
<!-- Add the following at the end of your body tag -->
<script type="text/javascript" src="/webjars/vue/3.4.15/vue.global.prod.js" th:src="@{/webjars/vue/3.4.15/dist/vue.global.prod.js}"></script>
<script type="text/javascript" src="/webjars/quasar/2.14.0/dist/quasar.umd.prod.js" th:src="@{/webjars/quasar/2.14.0/dist/quasar.umd.prod.js}"></script>
<script type="text/javascript" src="/webjars/quasar/dist/icon-set/svg-mdi-v7.umd.prod.js" th:src="@{/webjars/quasar/2.14.0/dist/icon-set/svg-mdi-v7.umd.prod.js}"></script>
<script th:inline="javascript">
const app = Vue.createApp({
setup() {
debugger
const userCode = [[${userCode}]];
const scopesToAuthorize = [[${scopesToAuthorize}]]
const scopesPreviouslyAuthorized = [[${scopesPreviouslyAuthorized}]]
const selectedScopesPreviouslyAuthorized = scopesPreviouslyAuthorized.map(item => item.value);
const code = Vue.ref(userCode);
const authorizeScopesOptions = Vue.ref([
{label: 'This application will be able to read your profile information.', value: 'profile'},
{label: 'This application will be able to read your openid information.', value: 'openid'}
]);
const selectAuthorizeScopes = Vue.ref([]);
const previouslyAuthorizedScopesOptions = Vue.ref(scopesPreviouslyAuthorized);
const selectPreviouslyAuthorizedScopes = Vue.ref(selectedScopesPreviouslyAuthorized);
return {
code,
authorizeScopesOptions,
selectAuthorizeScopes,
previouslyAuthorizedScopesOptions,
selectPreviouslyAuthorizedScopes
};
},
});
app.use(Quasar);
Quasar.iconSet.set(Quasar.iconSet.svgMdiV7);
app.mount("#q-app");
</script>
</body>
</html>
注意这里的:authorizeScopesOptions : 值目前我是写死在代码里面的后面可以通过数据库查询出来之后传入给页面。
开始我们的授权码模式:
友情提示测试前先重启服务并且清除浏览器缓存
先在地址栏中请求:
http://127.0.0.1:9000/oauth2/authorize?response_type=code&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/oidc-client&client_id=oidc-client&scope=profile
第一次访问重定向到登录页:
输入账号密码之后会跳转到授权页面
授权后浏览器重定向到:
http://127.0.0.1:8080/login/oauth2/code/oidc-client?code=CeWxJ315sPInSnPTYvT1ZjA2daH1ZLasQog5d54nZqPB2SZ1pcNKERzkpkeD8_pUew7k3mzEUs0SwyEwgphPmrbx0afn3wSTpv0TdB8uPwYsikXbeJffUOOHzpMc6_Wv
然后通过code置换token