自定义登录页

This commit is contained in:
bunny 2025-07-10 21:11:46 +08:00
parent 77e3bff09a
commit 7e23ca1c55
17 changed files with 349 additions and 89 deletions

View File

@ -4,8 +4,8 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringSecurityApplication {
public class SpringSecurityOfficialApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityApplication.class, args);
SpringApplication.run(SpringSecurityOfficialApplication.class, args);
}
}

View File

@ -1,5 +1,5 @@
server:
port: 8778
port: 8770
spring:
application:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringSecurityApplicationTests {
class SpringSecurityOfficialApplicationTests {
@Test
void contextLoads() {

View File

@ -14,7 +14,7 @@
<version>0.0.1-SNAPSHOT</version>
<name>spring-security</name>
<description>spring-security</description>
<packaging>pom</packaging>
<modules>
@ -99,6 +99,14 @@
</dependency>
<!-- 前端美化相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
@ -114,14 +122,6 @@
<artifactId>jquery</artifactId>
<version>${jquery.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
</dependencies>
<build>

View File

@ -22,6 +22,6 @@
</properties>
<dependencies>
</dependencies>
</project>

View File

@ -4,8 +4,8 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringSecurityApplication {
public class SpringSecurityStep1Application {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityApplication.class, args);
SpringApplication.run(SpringSecurityStep1Application.class, args);
}
}

View File

@ -1,20 +0,0 @@
package com.spring.config;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.DigestUtils;
import java.util.Arrays;
public class MD5PasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
String rawPasswordString = rawPassword.toString();
byte[] md5Digest = DigestUtils.md5Digest(rawPasswordString.getBytes());
return Arrays.toString(md5Digest);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return false;
}
}

View File

@ -1,9 +0,0 @@
package com.spring.config.security;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@EnableWebSecurity
@Configuration
public class SecurityWebConfiguration {
}

View File

@ -1,35 +1,18 @@
package com.spring.controller.security;
import com.spring.domain.dto.security.LoginRequest;
import com.spring.domain.vo.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Tag(name = "Login接口", description = "登录接口")
@RestController
@RequestMapping("/api/security")
@RequiredArgsConstructor
@Controller
public class LoginController {
private final AuthenticationManager authenticationManager;
@GetMapping("")
public String indexPage() {
return "index";
}
@Operation(summary = "登录接口", description = "系统登录接口")
@PostMapping("login")
public Result<Authentication> login(@RequestBody LoginRequest loginRequest) {
String username = loginRequest.getUsername();
String password = loginRequest.getPassword();
Authentication authenticationRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
Authentication authenticationResponse = authenticationManager.authenticate(authenticationRequest);
return Result.success(authenticationResponse);
@GetMapping("/login-page")
public String showLoginPage() {
return "login";
}
}

View File

@ -1,27 +1,17 @@
package com.spring.config.security;
package com.spring.security;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfiguration {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
;
return http.build();
}
/**
* 添加内存用户
@ -31,10 +21,15 @@ public class SecurityConfiguration {
@Bean
@ConditionalOnMissingBean(UserDetailsService.class)
InMemoryUserDetailsManager inMemoryUserDetailsManager(PasswordEncoder passwordEncoder) {
// 使用注入的密码加密器进行密码加密
String generatedPassword = passwordEncoder.encode("123456");
return new InMemoryUserDetailsManager(User.withUsername("bunny")
.password(generatedPassword).roles("USER").build());
// 创建用户
UserDetails userDetails1 = User.withUsername("bunny").password(generatedPassword).roles("USER").build();
UserDetails userDetails2 = User.withUsername("rabbit").password(generatedPassword).roles("USER").build();
// 返回内存中的用户
return new InMemoryUserDetailsManager(userDetails1, userDetails2);
}
/**
@ -51,5 +46,8 @@ public class SecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
// 自定义实现密码加密器
// return new MD5PasswordEncoder();
}
}

View File

@ -0,0 +1,49 @@
package com.spring.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@EnableMethodSecurity
@EnableWebSecurity
@Configuration
public class SecurityWebConfiguration {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
String[] permitAllUrls = {
"/", "/doc.html/**",
"/webjars/**", "/images/**", ".well-known/**", "favicon.ico", "/error/**",
"/v3/api-docs/**"
};
http.authorizeHttpRequests(authorizeRequests ->
// 访问路径为 /api/** 时需要进行认证
authorizeRequests
.requestMatchers("/api/**").authenticated()
.requestMatchers(permitAllUrls).permitAll()
)
.formLogin(loginPage -> loginPage
// 自定义登录页路径
.loginPage("/login-page")
// 处理登录的URL默认就是/login
.loginProcessingUrl("/login")
// 登录成功跳转
.defaultSuccessUrl("/")
// 登录失败跳转
.failureUrl("/login-page?error=true")
.permitAll()
)
// 使用默认的登录
// .formLogin(Customizer.withDefaults())
.logout(logout -> logout
.logoutSuccessUrl("/login-page?logout=true")
.permitAll()
);
return http.build();
}
}

View File

@ -0,0 +1,53 @@
package com.spring.security.password;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.util.HexFormat;
/**
* <h1>MD5密码编码器实现</h1>
*
* <strong>安全警告</strong>此类使用MD5算法进行密码哈希已不再安全不推荐用于生产环境
*
* <p>MD5算法因其计算速度快且易受彩虹表攻击而被认为不安全即使密码哈希本身是单向的
* 但现代计算能力使得暴力破解和预先计算的彩虹表攻击变得可行</p>
*
* <p>Spring Security推荐使用BCryptPBKDF2Argon2或Scrypt等自适应单向函数替代MD5</p>
*
* @see PasswordEncoder
* @deprecated 此类仅用于遗留系统兼容新系统应使用更安全的密码编码器
*/
@Deprecated
public class MD5PasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("原始密码不能为null");
}
byte[] md5Digest = DigestUtils.md5Digest(rawPassword.toString().getBytes());
return HexFormat.of().formatHex(md5Digest);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("原始密码不能为null");
}
if (!StringUtils.hasText(encodedPassword)) {
return false;
}
return encodedPassword.equalsIgnoreCase(encode(rawPassword));
}
@Override
public boolean upgradeEncoding(String encodedPassword) {
// MD5已不安全始终返回true建议升级到更安全的算法
return true;
}
}

View File

@ -1,5 +1,5 @@
server:
port: 8778
port: 8771
spring:
application:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,206 @@
<!DOCTYPE html>
<html lang="zh-cn" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>登录 | 您的应用名称</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" th:href="@{/webjars/bootstrap/5.1.3/css/bootstrap.min.css}">
<!-- Font Awesome -->
<link rel="stylesheet" th:href="@{/webjars/font-awesome/5.15.4/css/all.min.css}">
<!-- 自定义CSS -->
<style>
:root {
--primary-color: #4e73df;
--secondary-color: #f8f9fc;
--accent-color: #2e59d9;
--text-color: #5a5c69;
}
body {
background-color: var(--secondary-color);
height: 100vh;
display: flex;
align-items: center;
}
.login-container {
width: 100%;
max-width: 450px;
margin: 0 auto;
padding: 2rem;
background: white;
border-radius: 10px;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header img {
height: 80px;
margin-bottom: 1rem;
}
.login-header h1 {
color: var(--primary-color);
font-weight: 600;
font-size: 1.8rem;
}
.form-control {
padding: 0.75rem 1rem;
border-radius: 0.35rem;
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem rgba(78, 115, 223, 0.25);
}
.btn-login {
background-color: var(--primary-color);
border: none;
padding: 0.75rem;
font-weight: 600;
width: 100%;
transition: all 0.3s;
}
.btn-login:hover {
background-color: var(--accent-color);
}
.input-group-text {
background-color: #f8f9fa;
}
.divider {
position: relative;
margin: 1.5rem 0;
text-align: center;
}
.divider::before {
content: "";
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background-color: #e3e6f0;
z-index: 1;
}
.divider-text {
display: inline-block;
padding: 0 1rem;
background-color: white;
position: relative;
z-index: 2;
color: #b7b9cc;
font-size: 0.85rem;
}
.footer-links {
text-align: center;
margin-top: 1.5rem;
font-size: 0.9rem;
}
.footer-links a {
color: var(--text-color);
text-decoration: none;
margin: 0 0.5rem;
}
.footer-links a:hover {
color: var(--primary-color);
text-decoration: underline;
}
.alert {
border-radius: 0.35rem;
}
</style>
</head>
<body>
<div class="container">
<div class="login-container">
<div class="login-header">
<!-- 替换为你的logo -->
<img alt="Logo" class="img-fluid" src="/favicon.ico">
<h1>欢迎回来</h1>
<p class="text-muted">请登录您的账户</p>
</div>
<!-- 错误消息显示 -->
<div class="alert alert-danger alert-dismissible fade show" role="alert" th:if="${param.error}">
<i class="fas fa-exclamation-circle me-2"></i>
<span th:text="#{login.error}">无效的用户名或密码</span>
<button aria-label="Close" class="btn-close" data-bs-dismiss="alert" type="button"></button>
</div>
<div class="alert alert-success alert-dismissible fade show" role="alert" th:if="${param.logout}">
<i class="fas fa-check-circle me-2"></i>
<span th:text="#{login.logout}">您已成功登出</span>
<button aria-label="Close" class="btn-close" data-bs-dismiss="alert" type="button"></button>
</div>
<!-- 登录表单 -->
<form method="post" th:action="@{/login}">
<div class="mb-3">
<label class="form-label" for="username">用户名</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input autofocus class="form-control" id="username" name="username" placeholder="请输入用户名"
required type="text">
</div>
</div>
<div class="mb-3">
<label class="form-label" for="password">密码</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
<input class="form-control" id="password" name="password" placeholder="请输入密码" required
type="password">
</div>
</div>
<div class="mb-3 form-check">
<input class="form-check-input" id="remember-me" name="remember-me" type="checkbox">
<label class="form-check-label" for="remember-me">记住我</label>
</div>
<button class="btn btn-primary btn-login mb-3" type="submit">
<i class="fas fa-sign-in-alt me-2"></i> 登录
</button>
<div class="text-center mb-3">
<a class="text-decoration-none" th:href="@{/forgot-password}">忘记密码?</a>
</div>
<div class="divider">
<span class="divider-text"></span>
</div>
<div class="text-center">
<p class="text-muted">还没有账户? <a class="text-decoration-none" th:href="@{/register}">注册</a></p>
</div>
</form>
<div class="footer-links">
<a href="#">隐私政策</a>
<a href="#">使用条款</a>
<a href="#">帮助中心</a>
</div>
</div>
</div>
</body>
<!-- Bootstrap JS Bundle with Popper -->
<script th:src="@{/webjars/bootstrap/5.1.3/js/bootstrap.bundle.min.js}"></script>
</html>

View File

@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringSecurityApplicationTests {
class SpringSecurityStep1ApplicationTests {
@Test
void contextLoads() {