Java框架快速入门: Spring Security+OAuth2之定制登录页

概述

本文基于实际项目需求,讲解如何替换 Spring Security 默认的登录页面,实现自定义登录界面,并处理静态资源拦截、国际化、表单参数定制等问题。

内容覆盖从依赖引入、安全配置到视图映射的全过程,并提供完整可运行的代码示例。

纲要

  • 环境与依赖管理
    • spring-boot-starter-securityspring-boot-starter-thymeleaf
    • webjars-locator-core 与前端资源本地化
  • 自定义登录页面
    • src/main/resources/templates/ 下创建 login.html
    • 页面中引入 Bootstrap 等静态资源
  • 视图控制器与资源映射
    • 实现 WebMvcConfigurer,添加视图控制器映射
    • 处理静态资源与 webjars 的映射关系
  • 安全配置改造
    • 配置 HttpSecurity,指定自定义登录页、放行静态资源
    • 避免重定向循环:permitAll() 放行登录页
  • 国际化文本支持
    • 创建 messages_zh_CN.properties
    • 配置 Spring 消息源,解决中文乱码
  • 表单字段与行为定制
    • 自定义用户名、密码参数名
    • 登录处理地址、成功/失败跳转逻辑
  • 完整可运行项目演示

环境与依赖管理

pom.xml 中引入以下关键依赖。spring-boot-starter-security 提供安全框架支持,spring-boot-starter-thymeleaf 为模板引擎,用于渲染登录页面。webjars-locator-core 可以帮助我们直接在模板中引用 WebJars 中的静态资源而不需写死版本号。为让页面美观,引入 Bootstrap 的 WebJar。

xml 复制代码
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.13</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>custom-login-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>custom-login-demo</name>

    <properties>
        <java.version>11</java.version>
        <bootstrap.version>4.6.2</bootstrap.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- WebJars 支持,无需在路径中写版本号 -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>webjars-locator-core</artifactId>
        </dependency>
        <!-- Bootstrap WebJar,用于前端样式 -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>${bootstrap.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

自定义登录页面

src/main/resources/templates/ 下新建 login.html。使用 Thymeleaf 模板语法,并引入 Bootstrap 样式。同时准备好表单,提交地址为 /login,方法为 POST,用户名、密码输入框的 name 属性分别使用 usernamepassword(后续可通过配置修改)。

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="#{login.title}">登录</title>
    <link rel="stylesheet" th:href="@{/webjars/bootstrap/css/bootstrap.min.css}">
</head>
<body class="bg-light">
<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-4">
            <h3 class="text-center mb-4" th:text="#{login.header}">请登录</h3>
            <form th:action="@{/login}" method="post">
                <div class="form-group">
                    <label for="username" th:text="#{login.username}">用户名</label>
                    <input type="text" id="username" name="username" class="form-control" required autofocus>
                </div>
                <div class="form-group">
                    <label for="password" th:text="#{login.password}">密码</label>
                    <input type="password" id="password" name="password" class="form-control" required>
                </div>
                <button type="submit" class="btn btn-primary btn-block" th:text="#{login.submit}">登录</button>
            </form>
        </div>
    </div>
</div>
</body>
</html>

视图控制器与资源映射

我们需要将 /login 这个 URL 映射到 login 视图(即 login.html),同时保证静态资源(如 webjars)能够被正常访问。这通过实现 WebMvcConfigurer 并重写 addViewControllersaddResourceHandlers 来完成。

java 复制代码
package com.example.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 将 /login 路径映射到名为 "login" 的视图
        registry.addViewController("/login").setViewName("login");
        // 可以继续添加其他视图控制器
        registry.addViewController("/").setViewName("index");
    }
}

一般情况下,Spring Boot 的自动配置已经能够正确处理 webjars-locator-core 的映射,因此无需手动配置 addResourceHandlers。若确需显式声明,可在同一配置类中增加:

java 复制代码
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/webjars/**")
            .addResourceLocations("classpath:/META-INF/resources/webjars/");
}

安全配置改造

定制登录页面后,必须在安全配置中进行相应声明,否则自定义页面无法生效。关键配置项包括:

  • 指定登录页面 URL:.loginPage("/login")
  • 表单登录的提交地址(通常与登录页面相同):.loginProcessingUrl("/login")
  • 放行登录页面和静态资源,防止重定向循环:使用 permitAll() 以及 requestMatchers 排除静态资源路径。
  • 自定义用户名/密码参数名称(可选)。

以下是一个典型的 SecurityConfig 配置类,继承自 WebSecurityConfigurerAdapter(Spring Boot 2.x 版本)。

java 复制代码
package com.example.config;

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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                // 放行静态资源,避免被拦截
                .requestMatchers(org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher("/webjars/**")).permitAll()
                .antMatchers("/css/**", "/js/**", "/images/**").permitAll()
                // 放行登录页面和登录请求
                .antMatchers("/login").permitAll()
                // 其余请求均需认证
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")                // 自定义登录页
                .loginProcessingUrl("/login")       // 处理登录的 URL,与表单 action 一致
                .usernameParameter("username")      // 表单中用户名的 name
                .passwordParameter("password")      // 表单中密码的 name
                .defaultSuccessUrl("/home", true)   // 登录成功后的默认跳转
                .failureUrl("/login?error")         // 登录失败后的跳转
                .permitAll()
            .and()
            .logout()
                .logoutSuccessUrl("/login?logout")
                .permitAll();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

为了避免配置类中因 requestMatchers 的写法过于复杂,Spring Security 提供了便捷方法 web.ignoring() 来完全跳过某些路径的安全过滤器链,也可以用于静态资源。但由于这种方式会导致静态资源完全绕过安全上下文,仅推荐用于无认证需求的纯静态文件。上述配置已经足够演示。

国际化文本支持

登录页面中使用了 th:text="#{...}" 引用国际化消息。为此,我们需要在 src/main/resources 下创建消息文件,并在配置文件中指定基础名。

创建 messages_zh_CN.properties

properties 复制代码
login.title=自定义登录页
login.header=请登录
login.username=用户名
login.password=密码
login.submit=登录

同时在 application.properties(或 application.yml)中指定消息源的基础名称:

properties 复制代码
spring.messages.basename=messages
spring.messages.encoding=UTF-8

为了确保浏览器能正确识别中文,也可在 login.html 头部保留 <meta charset="UTF-8">

表单字段与行为定制

.formLogin() 配置块中,我们可以灵活调整与表单交互相关的参数:

  • usernameParameter("user"):如果前端输入框的 name 改为 user,则需要此处同步。
  • passwordParameter("pwd"):同理,修改密码字段名。
  • loginProcessingUrl("/doLogin"):如果表单的 action/doLogin,则需要设置该值,默认是 /login
  • successForwardUrl("/dashboard"):登录成功后执行服务端转发,而非重定向。
  • failureForwardUrl("/login-error"):登录失败后的转发地址。
  • defaultSuccessUrl("/home", true):总是重定向到指定 URL,而不是先前访问的受保护页面。
  • 登录成功处理器与失败处理器:适合前后端分离场景,返回 JSON 数据,后续章节介绍。

示例:修改用户名参数名为 email,密码参数名为 secret,前端需对应调整。

java 复制代码
.formLogin()
    .loginPage("/login")
    .loginProcessingUrl("/authenticate")
    .usernameParameter("email")
    .passwordParameter("secret")
    .successHandler((request, response, authentication) -> {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("{\"code\":200,\"msg\":\"登录成功\"}");
    })
    .failureHandler((request, response, exception) -> {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("{\"code\":401,\"msg\":\"登录失败\"}");
    })

完整项目结构

最终的项目目录结构如下:

dir 复制代码
src
└── main
    ├── java
    │   └── com.example
    │       ├── DemoApplication.java
    │       └── config
    │           ├── SecurityConfig.java
    │           └── WebMvcConfig.java
    └── resources
        ├── application.properties
        ├── messages_zh_CN.properties
        └── templates
            ├── login.html
            └── home.html  (可选的首页)

DemoApplication.java 为 Spring Boot 启动类:

java 复制代码
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

home.html 示例(必须放在 templates 下,并在安全配置中允许认证后访问):

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>主页</title>
</head>
<body>
    <h1 th:text="'欢迎,' + ${#authentication.name} + '!'"></h1>
    <a th:href="@{/logout}">退出登录</a>
</body>
</html>

流程图示

用户访问受保护资源时,Spring Security 会自动重定向到自定义登录页,完成认证后跳转至目标页面。
后端服务 视图控制器 SpringSecurity 浏览器 后端服务 视图控制器 SpringSecurity 浏览器 #mermaid-svg-5xlaYhctZ9ijKTw7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5xlaYhctZ9ijKTw7 .error-icon{fill:#552222;}#mermaid-svg-5xlaYhctZ9ijKTw7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5xlaYhctZ9ijKTw7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5xlaYhctZ9ijKTw7 .marker.cross{stroke:#333333;}#mermaid-svg-5xlaYhctZ9ijKTw7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5xlaYhctZ9ijKTw7 p{margin:0;}#mermaid-svg-5xlaYhctZ9ijKTw7 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5xlaYhctZ9ijKTw7 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-5xlaYhctZ9ijKTw7 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5xlaYhctZ9ijKTw7 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-5xlaYhctZ9ijKTw7 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-5xlaYhctZ9ijKTw7 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-5xlaYhctZ9ijKTw7 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-5xlaYhctZ9ijKTw7 .sequenceNumber{fill:white;}#mermaid-svg-5xlaYhctZ9ijKTw7 #sequencenumber{fill:#333;}#mermaid-svg-5xlaYhctZ9ijKTw7 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-5xlaYhctZ9ijKTw7 .messageText{fill:#333;stroke:none;}#mermaid-svg-5xlaYhctZ9ijKTw7 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5xlaYhctZ9ijKTw7 .labelText,#mermaid-svg-5xlaYhctZ9ijKTw7 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-5xlaYhctZ9ijKTw7 .loopText,#mermaid-svg-5xlaYhctZ9ijKTw7 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-5xlaYhctZ9ijKTw7 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5xlaYhctZ9ijKTw7 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-5xlaYhctZ9ijKTw7 .noteText,#mermaid-svg-5xlaYhctZ9ijKTw7 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-5xlaYhctZ9ijKTw7 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5xlaYhctZ9ijKTw7 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5xlaYhctZ9ijKTw7 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5xlaYhctZ9ijKTw7 .actorPopupMenu{position:absolute;}#mermaid-svg-5xlaYhctZ9ijKTw7 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-5xlaYhctZ9ijKTw7 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5xlaYhctZ9ijKTw7 .actor-man circle,#mermaid-svg-5xlaYhctZ9ijKTw7 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-5xlaYhctZ9ijKTw7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt 认证成功 认证失败 访问 /api/user (受保护) 302 重定向到 /login GET /login 根据 ViewController 映射 返回 login.html 用户填写表单并提交 POST /login (用户名+密码) 校验凭证 302 重定向到之前请求的 /api/user (或 defaultSuccessUrl) GET /api/user 200 响应数据 302 重定向到 /login?error GET /login?error 显示错误信息

总结

本文详细介绍了如何从零开始定制 Spring Security 的登录页面,涵盖视图映射、安全规则配置、静态资源处理、国际化以及表单字段自定义等关键步骤。

所有代码均可在 Spring Boot 2.7.x 环境下直接运行,帮助开发者快速掌握传统模板引擎下的登录定制方案,为后续迁移至前后端分离的 OAuth2 认证体系打下基础。

相关推荐
imgsq1 小时前
MapLibre GL JS v6 正式发布:v5 之后八个月的第一个大版本(编译)
开发语言·javascript·ecmascript
统计学小王子1 小时前
数学建模国赛倒计时 2 天 ——《缺失值处理一(R语言)》
开发语言·数学建模·r语言
霸道流氓气质1 小时前
Spring AI vs Spring AI Alibaba:技术选型与平滑迁移策略
java·人工智能·spring
拽着尾巴的鱼儿1 小时前
Idea:Cannot Run Git 无法运行git
java·git·intellij-idea
嘻哈baby1 小时前
大量消息在 MQ 里长时间积压,该如何解决?
开发语言
编程_大白1 小时前
IDEA配置SQL方言
java·sql·intellij-idea
橘子汽水1681 小时前
Leetcode 128,49最长连续序列,字母异位词分组
java·算法·leetcode
qq_570398571 小时前
Three.js基础使用-案例
开发语言·javascript·ecmascript
Suxing92 小时前
C语言基础分享:C语言文件操作:从“写日记”到“拍电影”的存盘指南
c语言·开发语言