Spring Security6 教程

Spring Security6 自定义登录成功处理

在基于 Spring Security6 构建的 Web 应用中,用户认证是保障系统安全的重要环节。而登录成功后的处理逻辑,直接影响着用户体验和业务流程。

默认情况下,Spring Security6 会将用户重定向到一个固定的默认成功页面,但在实际项目中,我们往往需要根据用户角色、登录来源等因素,实现更灵活的登录成功处理。

下面将介绍如何自定义登录成功处理:

默认登录成功处理

在 Spring Security6 的默认配置下,当用户通过身份验证后,会被重定向到 defaultSuccessUrl 指定的页面。如下:

package com.hxstrive.spring_security.config;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.ForwardAuthenticationFailureHandler;

import java.io.IOException;
import static org.springframework.security.config.Customizer.withDefaults;

/**
 * Spring Security 配置类
 * @author hxstrive.com
 */
@Configuration
public class SecurityConfig {
    // 省略 UserDetailsService 和 PasswordEncoder 的配置...

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        System.out.println("securityFilterChain()");
        // csrf 设置
        // csrfCustomizer.ignoringRequestMatchers(*) 表示所有请求地址都不使用 csrf
        http.csrf(csrfCustomizer -> csrfCustomizer.ignoringRequestMatchers("*"))
                // authorize.anyRequest().authenticated() 表示所有的请求都需要进行身份验证。
                .authorizeHttpRequests(authorize -> authorize
                        // 配置登陆失败页面访问不需要鉴权,如果不配置,不会跳转到登录失败页面
                        .requestMatchers("/view/fail").permitAll()
                        .anyRequest().authenticated())
                // formLogin() 用于配置表单登录功能
                .formLogin(e -> e.loginProcessingUrl("/login") // 配置处理登陆请求到地址
                        // 自定义登陆页面地址
                        .loginPage("/view/login")
                        // 自定义登陆成功页面
                        .defaultSuccessUrl("/view/success")
                        // 登陆失败页面
                        .failureUrl("/view/fail")
                        .permitAll())
                // httpBasic() 启用 HTTP 基本认证,withDefaults() 表示使用默认的 HTTP 基本认证配置。
                .httpBasic(withDefaults());

        return http.build();
    }

}

上述配置中,当用户登录成功后,会被重定向到 /view/success 页面。然而,这种简单的重定向方式在复杂业务场景下存在局限性,如无法根据用户角色跳转到不同页面、记录用户登录信息等等。

successForwardUrl() 和 defaultSuccessUrl() 的区别

在 Spring Security6 中,successForwardUrl()、defaultSuccessUrl() 和 defaultSuccessUrl(..., true) 是用于处理登录成功后跳转逻辑的三个重要方法。看看下面三行代码,它们之间有什么区别呢?

successForwardUrl("/view/success")
defaultSuccessUrl("/view/success")
defaultSuccessUrl("/view/success", true)

successForwardUrl("/view/success")

登录成功后,使用服务器端转发(RequestDispatcher.forward)到指定的 URL。浏览器地址栏保持不变(仍显示登录页的 URL)。转发在服务器内部完成,不会创建新的 HTTP 请求,因此原请求的属性(如 request.setAttribute 设置的数据)会被保留。

适用于需要在登录成功后直接渲染视图,而不希望用户感知到 URL 变化的场景。

源码如下:

/**
 * 转发认证成功处理器
 * @param forwardUrl 成功时的目标URL
 * @return the {@link FormLoginConfigurer} for additional customization
 */
public FormLoginConfigurer<H> successForwardUrl(String forwardUrl) {
    successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl));
    return this;
}

defaultSuccessUrl("/view/success")

登录成功后,使用 HTTP 重定向(302 状态码)到指定的 URL。浏览器地址栏会显示新的 URL(/view/success)。重定向会创建新的 HTTP 请求,原请求的属性(如表单数据)会丢失。

如果用户直接访问登录页(未被拦截的情况下),登录成功后会跳转到 defaultSuccessUrl。

如果用户是因为访问受保护资源被重定向到登录页,登录成功后会回到原请求的 URL(即“记住原请求”)。

源码如下:

/**
 * 指定如果用户在成功认证之前未访问过安全页面,成功认证后将被重定向到何处。
 * 这是调用{@link #defaultSuccessUrl(String, boolean)}的快捷方式。
 * @param defaultSuccessUrl 默认成功URL
 * @return the {@link FormLoginConfigurer} for additional customization
 */
public final T defaultSuccessUrl(String defaultSuccessUrl) {
    return defaultSuccessUrl(defaultSuccessUrl, false);
}

defaultSuccessUrl("/view/success", true)

登录成功后,强制使用 HTTP 重定向 到指定的 URL,忽略原请求。无论用户是如何到达登录页的,登录成功后都会固定跳转到 /view/success。常用于需要统一登录后跳转逻辑的场景(如管理后台)。

源码如下:

/**
 * 指定如果用户在成功认证后尚未访问过安全页面,或者`alwaysUse`为`true`,
 * 将用户重定向到何处。
 * 这是调用`{@link #successHandler(AuthenticationSuccessHandler)}`的快捷方式。 
 * @param defaultSuccessUrl 默认成功URL
 * @param alwaysUse 如果在身份验证后,无论之前是否访问过受保护页面,
 *    都应使用{@code defaultSuccessUrl},则为true
 * @return the {@link FormLoginConfigurer} for additional customization
 */
public final T defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) {
    SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
    handler.setDefaultTargetUrl(defaultSuccessUrl);
    handler.setAlwaysUseDefaultTargetUrl(alwaysUse);
    this.defaultSuccessHandler = handler;
    return successHandler(handler);
}

下面通过一个表格,清晰说明它们的区别:

方法

跳转方式

地址栏变化

原请求属性

是否忽略原请求

defaultSuccessUrl("/path")

重定向(302)

改变

丢失

否(默认)

defaultSuccessUrl("/path", true)

重定向(302)

改变

丢失

是(强制跳转)

successForwardUrl("/path")

服务器转发

不变

保留

它们到适用场景:

  • defaultSuccessUrl(默认):适用于用户可能从多个入口登录,需要回到原请求页面的场景(如电商网站)。

  • defaultSuccessUrl(..., true):适用于统一登录后体验的场景(如管理系统,所有用户登录后固定跳转到控制台)。

  • successForwardUrl:适用于需要在登录成功后继续处理原请求数据的场景(如表单提交后登录,登录成功后继续处理表单)。

自定义登录成功处理器

要实现自定义登录成功处理,我们需要创建一个实现 AuthenticationSuccessHandler 接口的类,并重写onAuthenticationSuccess() 方法。然后,将 AuthenticationSuccessHandler 实现类的实例传递给 successHandler() 方法,如下:

/**
 * 指定要使用的{@link AuthenticationSuccessHandler}。默认值为
 * {@link SavedRequestAwareAuthenticationSuccessHandler},且未设置其他属性。
 * 
 * @param successHandler {@link AuthenticationSuccessHandler}(认证成功处理器)
 * @return the {@link FormLoginConfigurer} for additional customization
 */
public final T successHandler(AuthenticationSuccessHandler successHandler) {
    this.successHandler = successHandler;
    return getSelf();
}

用户需要在 AuthenticationSuccessHandler 的 onAuthenticationSuccess() 方法中实现用户认证成功后的自定义逻辑。

示例:我们通过匿名内部类来实现,如下:

http.csrf(csrfCustomizer -> csrfCustomizer.ignoringRequestMatchers("*"))
    // authorize.anyRequest().authenticated() 表示所有的请求都需要进行身份验证。
    .authorizeHttpRequests(authorize -> authorize
            // 配置登陆失败页面访问不需要鉴权,如果不配置,不会跳转到登录失败页面
            .requestMatchers("/view/fail").permitAll()
            .anyRequest().authenticated())
    // formLogin() 用于配置表单登录功能
    .formLogin(e -> e.loginProcessingUrl("/login") // 配置处理登陆请求到地址
            // 自定义登陆页面地址
            .loginPage("/view/login")
            // 自定义登陆成功页面【看这里】
            .successHandler(new AuthenticationSuccessHandler() {
                @Override
                public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                            Authentication authentication) throws IOException, ServletException {
                    System.out.println("onAuthenticationSuccess()");
                    response.sendRedirect("/view/success");
                }
            })
            // 登陆失败页面
            .failureUrl("/view/fail")
            .permitAll())
    // httpBasic() 启用 HTTP 基本认证,withDefaults() 表示使用默认的 HTTP 基本认证配置。
    .httpBasic(withDefaults());

在上述代码中,onAuthenticationSuccess() 方法接收 HttpServletRequest、HttpServletResponse 和 Authentication 对象。通过 Authentication 对象可以获取当前登录用户的信息,如用户名、角色等。这里只是简单打印了登录成功信息,并将用户重定向到 /view/success 页面,实际项目中可以根据业务需求进行更复杂的处理。

基于用户角色的动态处理

在实际应用中,常常需要根据用户角色进行不同的登录成功处理。例如,管理员登录后跳转到管理页面,普通用户登录后跳转到个人中心页面。我们可以在自定义登录成功处理器中实现这一逻辑。如下:

package com.hxstrive.spring_security.customer;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import java.io.IOException;
import java.util.Collection;

public class RoleBasedAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        String username = authentication.getName();
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

        // 判断用户是否拥有 ROLE_ADMIN 角色
        // 如果有,表示是管理员
        boolean isAdmin = authorities.stream()
            .anyMatch(a -> "ROLE_ADMIN".equals(a.getAuthority()));

        if (isAdmin) {
            // 管理员
            response.sendRedirect(request.getContextPath() + "/admin/dashboard");
        } else {
            // 非管理员
            response.sendRedirect(request.getContextPath() + "/user/profile");
        }
        // 记录日志信息
        System.out.println("用户 " + username + " 登录成功,重定向到对应页面");
    }
}

在上述代码中,通过 authentication.getAuthorities() 获取用户的权限集合,然后判断用户是否具有 ROLE_ADMIN 角色,根据角色不同将用户重定向到不同的页面。

源码分析

转发源码分析

使用 successForwardUrl() 时,表示成功后转发请求到地址。内部是通过 successHandler() 方法控制成功后交给哪个类进行处理,如下图:

Spring Security6 自定义登录成功处理

源码中,ForwardAuthenticationSuccessHandler 类到内部就是最简单的请求转发。注意,由于使用的是请求转发,当遇到需要跳转到站外或在前后端分离的项目中就无法使用了。源码如下:

Spring Security6 自定义登录成功处理

如果你之前使用过 Servlet,是不是很熟悉。

重定向源码分析

使用 defaultSuccessUrl() 时,表示成功后重定向到地址。内部也是通过 successHandler() 方法控制成功后交给哪个类进行处理,如下图:

Spring Security6 自定义登录成功处理

SavedRequestAwareAuthenticationSuccessHandler 类的部分源码如下:

Spring Security6 自定义登录成功处理

到这里,源码分析完了,下一章将介绍 Spring Security 自定义登陆失败处理……

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号