✨ 自定义登录页
This commit is contained in:
parent
77e3bff09a
commit
7e23ca1c55
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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() {
|
|
@ -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>
|
||||
|
|
|
@ -22,6 +22,6 @@
|
|||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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推荐使用BCrypt、PBKDF2、Argon2或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;
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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>
|
|
@ -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() {
|
Loading…
Reference in New Issue