1. 概述

Web应用中一个常见的需求是,用户登录后根据角色重定向到不同的页面。例如,普通用户跳转到/homepage.html,管理员跳转到/console.html

下面将演示如何使用Spring Security实现此功能。 本文涉及MVC相关知识,如果不熟悉可参考我们之前的Spring MVC教程

2. Spring Security 配置

Spring Security 提供了一个组件 —— AuthenticationSuccessHandler,从名字上我们也能看出,它负责处理登录成功后的逻辑操作。

2.1. 基本配置

首先定义我们的 Security 配置类

@Configuration
@EnableWebSecurity
public class SecSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            // ... endpoints
            .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/login")
                .defaultSuccessUrl("/homepage.html", true)
            // ... other configuration   
        return http.build();
    }
}

上面配置中我们需要重点关注defaultSuccessUrl() 这项配置。它表示用户登录成功后,将重定向到/homepage.html

此外,我们还需要配置用户和角色信息。我们将实现一个简单的UserDetailService,其中定义两个用户,每个用户对应一个角色。想了解更多关于此话题信息,请阅读Spring Security – 角色与权限

    @Service
    public class MyUserDetailsService implements UserDetailsService {
    
        private Map<String, User> roles = new HashMap<>();
    
        @PostConstruct
        public void init() {
            roles.put("admin2", new User("admin", "{noop}admin1", getAuthority("ROLE_ADMIN")));
            roles.put("user2", new User("user", "{noop}user1", getAuthority("ROLE_USER")));
        }
    
        @Override
        public UserDetails loadUserByUsername(String username) {
            return roles.get(username);
        }
    
        private List<GrantedAuthority> getAuthority(String role) {
            return Collections.singletonList(new SimpleGrantedAuthority(role));
        }
    }

注意,本示例中,我们没有使用PasswordEncoder密码编码器,所以需要在密码中添加{noop}前缀标识。

2.2. 添加自定义AuthenticationSuccessHandler

现在我们定义了2个不同的用户,user普通用户 和admin管理员。但目前登录成功后,都会跳到homepage.html。下面教你如何实现基于用户角色跳转到不同页面

首先,创建AuthenticationSuccessHandler bean(它的定义见下文):

@Bean
public AuthenticationSuccessHandler myAuthenticationSuccessHandler(){
    return new MySimpleUrlAuthenticationSuccessHandler();
}

然后,把defaultSuccessUrl替换为successHandler方法,并将自定义的AuthenticationSuccessHandler实例作为参数传进去:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        // endpoints
        .formLogin()
            .loginPage("/login.html")
            .loginProcessingUrl("/login")
            .successHandler(myAuthenticationSuccessHandler())
        // other configuration      
    return http.build();
}

2.3. XML 配置

等效的XML配置如下:

    <http use-expressions="true" >
        <!-- other configuration -->
        <form-login login-page='/login.html' 
          authentication-failure-url="/login.html?error=true"
          authentication-success-handler-ref="myAuthenticationSuccessHandler"/>
        <logout/>
    </http>
    
    <beans:bean id="myAuthenticationSuccessHandler"
      class="com.baeldung.security.MySimpleUrlAuthenticationSuccessHandler" />
    
    <authentication-manager>
        <authentication-provider>
            <user-service>
                <user name="user1" password="{noop}user1Pass" authorities="ROLE_USER" />
                <user name="admin1" password="{noop}admin1Pass" authorities="ROLE_ADMIN" />
            </user-service>
        </authentication-provider>
    </authentication-manager>

3. 自定义AuthenticationSuccessHandler的实现

Spring 内置了一个简单的跳转策略 - SimpleUrlAuthenticationSuccessHandler,它通过绑定URL、根据请求参数,或根据Referer进行跳转。

虽然比较灵活,但不能满足我们的需要。所以我们需要实现AuthenticationSuccessHandler接口,自定义登录成功处理器。

首先我们实现 onAuthenticationSuccess 方法:

    public class MySimpleUrlAuthenticationSuccessHandler
      implements AuthenticationSuccessHandler {
     
        protected Log logger = LogFactory.getLog(this.getClass());
    
        private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, 
          HttpServletResponse response, Authentication authentication)
          throws IOException {
     
            handle(request, response, authentication);
            clearAuthenticationAttributes(request);
        }

里面调用了2个helper方法:

    protected void handle(
            HttpServletRequest request,
            HttpServletResponse response, 
            Authentication authentication
    ) throws IOException {
    
        String targetUrl = determineTargetUrl(authentication);
    
        if (response.isCommitted()) {
            logger.debug(
                    "Response has already been committed. Unable to redirect to "
                            + targetUrl);
            return;
        }
    
        redirectStrategy.sendRedirect(request, response, targetUrl);
    }

determineTargetUrl 方法里面才是我们真正的实现逻辑,根据用户角色决定要重定向的URL:

    protected String determineTargetUrl(final Authentication authentication) {

        Map<String, String> roleTargetUrlMap = new HashMap<>();
        roleTargetUrlMap.put("ROLE_USER", "/homepage.html");
        roleTargetUrlMap.put("ROLE_ADMIN", "/console.html");
    
        final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        for (final GrantedAuthority grantedAuthority : authorities) {
            String authorityName = grantedAuthority.getAuthority();
            if(roleTargetUrlMap.containsKey(authorityName)) {
                return roleTargetUrlMap.get(authorityName);
            }
        }
    
        throw new IllegalStateException();
    }

注意 getAuthorities() 返回的是一个集合,所以如果用户有多个角色,那么只会返回第一个角色匹配到的URL。

    protected void clearAuthenticationAttributes(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return;
        }
        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    }

4. 总结

惯例,本文完整源代码可从GitHub上获取。这是一个基于Maven的项目,所以应该很容易导入和运行。