修改默认登录页

This commit is contained in:
孙小云 2025-07-19 09:06:49 +08:00
parent 2fb93f2b5d
commit d053df88d1
13 changed files with 279 additions and 19 deletions

View File

@ -5,7 +5,10 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="b713637a-3b19-4c5b-9e88-95e35dd83d2e" name="更改" comment=""> <list default="true" id="b713637a-3b19-4c5b-9e88-95e35dd83d2e" name="更改" comment="">
<change beforePath="$PROJECT_DIR$/../oidc/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/../oidc/pom.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/../oidc/src/main/java/com/tuoheng/oauth/oidc/config/SecurityConfig.java" beforeDir="false" afterPath="$PROJECT_DIR$/../oidc/src/main/java/com/tuoheng/oauth/oidc/config/SecurityConfig.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../resourceservice/target/classes/application.properties" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../resourceservice/target/classes/com/tuoheng/resourceservice/HelloController.class" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../resourceservice/target/classes/com/tuoheng/resourceservice/ResourceServiceApplication.class" beforeDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -106,6 +109,7 @@
<workItem from="1752745264222" duration="2832000" /> <workItem from="1752745264222" duration="2832000" />
<workItem from="1752751160098" duration="1320000" /> <workItem from="1752751160098" duration="1320000" />
<workItem from="1752798688493" duration="8426000" /> <workItem from="1752798688493" duration="8426000" />
<workItem from="1752886146802" duration="618000" />
</task> </task>
<servers /> <servers />
</component> </component>

View File

@ -1,15 +0,0 @@
server.port=8080
spring.cloud.gateway.routes[0].id=resource-server-a
spring.cloud.gateway.routes[0].uri=http://localhost:8081
spring.cloud.gateway.routes[0].predicates[0]=Path=/a/**
spring.cloud.gateway.routes[0].filters[0]=RewritePath=/a/(?<segment>.*), /api/${segment}
spring.cloud.gateway.routes[0].filters[1]=TokenRelay
spring.cloud.gateway.routes[1].id=resource-server-b
spring.cloud.gateway.routes[1].uri=http://localhost:8082
spring.cloud.gateway.routes[1].predicates[0]=Path=/b/**
spring.cloud.gateway.routes[1].filters[0]=RewritePath=/b/(?<segment>.*), /api/${segment}
spring.cloud.gateway.routes[1].filters[1]=TokenRelay
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000/oauth2/jwks

View File

@ -84,7 +84,13 @@ public class SecurityConfig {
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.oauth2ResourceServer(oauth2 -> oauth2.jwt()) // 新增支持JWT .oauth2ResourceServer(oauth2 -> oauth2.jwt()) // 新增支持JWT
.formLogin(Customizer.withDefaults()) .formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error=bad_credentials")
.permitAll()
)
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // 添加CORS支持 .cors(cors -> cors.configurationSource(corsConfigurationSource())) // 添加CORS支持
.csrf(csrf -> csrf.ignoringRequestMatchers("/logout")) // 禁用logout端点的CSRF保护 .csrf(csrf -> csrf.ignoringRequestMatchers("/logout")) // 禁用logout端点的CSRF保护
.logout(logout -> logout .logout(logout -> logout

View File

@ -0,0 +1,32 @@
package com.tuoheng.oauth.oidc.controller;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
@RestController
public class LoginController {
@GetMapping("/login")
@ResponseBody
public String login(HttpServletRequest request) throws IOException {
// 读取静态HTML文件
String htmlContent = new String(Files.readAllBytes(Paths.get("src/main/resources/static/login.html")));
// 获取CSRF token
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrfToken != null) {
// 替换CSRF token占位符
htmlContent = htmlContent.replace("id=\"csrf-parameter\" name=\"\" value=\"\"",
"id=\"csrf-parameter\" name=\"" + csrfToken.getParameterName() + "\" value=\"" + csrfToken.getToken() + "\"");
}
return htmlContent;
}
}

View File

@ -0,0 +1,235 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OIDC 登录</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 100%;
max-width: 400px;
margin: 20px;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
color: #333;
font-size: 28px;
font-weight: 600;
margin-bottom: 8px;
}
.login-header p {
color: #666;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
.form-group input[type="text"],
.form-group input[type="password"] {
width: 100%;
padding: 12px 16px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s ease;
background: #f8f9fa;
}
.form-group input[type="text"]:focus,
.form-group input[type="password"]:focus {
outline: none;
border-color: #667eea;
background: white;
}
.form-group input[type="checkbox"] {
margin-right: 8px;
transform: scale(1.2);
}
.checkbox-group {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.checkbox-group label {
margin-bottom: 0;
font-size: 14px;
color: #666;
}
.login-btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.login-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
}
.login-btn:active {
transform: translateY(0);
}
.error-message {
background: #fee;
border: 1px solid #fcc;
color: #c33;
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
font-size: 14px;
display: none;
}
.error-message.show {
display: block;
}
.footer {
text-align: center;
margin-top: 20px;
color: #999;
font-size: 12px;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1>OIDC 登录</h1>
<p>请输入您的凭据以继续</p>
</div>
<div id="error-message" class="error-message"></div>
<form id="login-form" method="post" action="/login">
<input type="hidden" id="csrf-parameter" name="" value="" />
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" required autocomplete="username">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<div class="checkbox-group">
<input type="checkbox" id="remember-me" name="remember-me">
<label for="remember-me">记住我</label>
</div>
<button type="submit" class="login-btn">登录</button>
</form>
<div class="footer">
<p>OIDC Authorization Server</p>
</div>
</div>
<script>
// 页面加载时检查错误参数
window.addEventListener('DOMContentLoaded', function() {
// 检查是否有错误参数
const urlParams = new URLSearchParams(window.location.search);
const error = urlParams.get('error');
if (error) {
const errorMessage = document.getElementById('error-message');
errorMessage.textContent = getErrorMessage(error);
errorMessage.classList.add('show');
}
});
// 表单提交处理
document.getElementById('login-form').addEventListener('submit', function(e) {
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value.trim();
if (!username || !password) {
e.preventDefault();
showError('请输入用户名和密码');
return;
}
});
// 显示错误信息
function showError(message) {
const errorMessage = document.getElementById('error-message');
errorMessage.textContent = message;
errorMessage.classList.add('show');
}
// 根据错误类型返回对应的错误信息
function getErrorMessage(error) {
const errorMessages = {
'bad_credentials': '用户名或密码错误',
'account_locked': '账户已被锁定',
'account_disabled': '账户已被禁用',
'account_expired': '账户已过期',
'credentials_expired': '密码已过期',
'session_authentication_exception': '会话认证失败',
'default': '登录失败,请重试'
};
return errorMessages[error] || errorMessages['default'];
}
// 输入框获得焦点时隐藏错误信息
document.getElementById('username').addEventListener('focus', function() {
document.getElementById('error-message').classList.remove('show');
});
document.getElementById('password').addEventListener('focus', function() {
document.getElementById('error-message').classList.remove('show');
});
</script>
</body>
</html>

View File

@ -1 +0,0 @@
server.port=8081

View File

@ -1 +0,0 @@
server.port=8082