✨ 自定义登录页
This commit is contained in:
parent
77e3bff09a
commit
7e23ca1c55
|
@ -4,8 +4,8 @@ import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class SpringSecurityApplication {
|
public class SpringSecurityOfficialApplication {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(SpringSecurityApplication.class, args);
|
SpringApplication.run(SpringSecurityOfficialApplication.class, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
server:
|
server:
|
||||||
port: 8778
|
port: 8770
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
application:
|
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;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
class SpringSecurityApplicationTests {
|
class SpringSecurityOfficialApplicationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void contextLoads() {
|
void contextLoads() {
|
|
@ -99,6 +99,14 @@
|
||||||
</dependency>
|
</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>
|
<dependency>
|
||||||
<groupId>org.webjars</groupId>
|
<groupId>org.webjars</groupId>
|
||||||
<artifactId>bootstrap</artifactId>
|
<artifactId>bootstrap</artifactId>
|
||||||
|
@ -114,14 +122,6 @@
|
||||||
<artifactId>jquery</artifactId>
|
<artifactId>jquery</artifactId>
|
||||||
<version>${jquery.version}</version>
|
<version>${jquery.version}</version>
|
||||||
</dependency>
|
</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>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -4,8 +4,8 @@ import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class SpringSecurityApplication {
|
public class SpringSecurityStep1Application {
|
||||||
public static void main(String[] args) {
|
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;
|
package com.spring.controller.security;
|
||||||
|
|
||||||
import com.spring.domain.dto.security.LoginRequest;
|
import org.springframework.stereotype.Controller;
|
||||||
import com.spring.domain.vo.result.Result;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
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;
|
|
||||||
|
|
||||||
@Tag(name = "Login接口", description = "登录接口")
|
@Controller
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/security")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class LoginController {
|
public class LoginController {
|
||||||
|
|
||||||
private final AuthenticationManager authenticationManager;
|
@GetMapping("")
|
||||||
|
public String indexPage() {
|
||||||
|
return "index";
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(summary = "登录接口", description = "系统登录接口")
|
@GetMapping("/login-page")
|
||||||
@PostMapping("login")
|
public String showLoginPage() {
|
||||||
public Result<Authentication> login(@RequestBody LoginRequest loginRequest) {
|
return "login";
|
||||||
String username = loginRequest.getUsername();
|
|
||||||
String password = loginRequest.getPassword();
|
|
||||||
|
|
||||||
Authentication authenticationRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
|
|
||||||
|
|
||||||
Authentication authenticationResponse = authenticationManager.authenticate(authenticationRequest);
|
|
||||||
return Result.success(authenticationResponse);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,27 +1,17 @@
|
||||||
package com.spring.config.security;
|
package com.spring.security;
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.User;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class SecurityConfiguration {
|
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
|
@Bean
|
||||||
@ConditionalOnMissingBean(UserDetailsService.class)
|
@ConditionalOnMissingBean(UserDetailsService.class)
|
||||||
InMemoryUserDetailsManager inMemoryUserDetailsManager(PasswordEncoder passwordEncoder) {
|
InMemoryUserDetailsManager inMemoryUserDetailsManager(PasswordEncoder passwordEncoder) {
|
||||||
|
// 使用注入的密码加密器进行密码加密
|
||||||
String generatedPassword = passwordEncoder.encode("123456");
|
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
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
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:
|
server:
|
||||||
port: 8778
|
port: 8771
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
application:
|
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;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
class SpringSecurityApplicationTests {
|
class SpringSecurityStep1ApplicationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void contextLoads() {
|
void contextLoads() {
|
Loading…
Reference in New Issue