java - 如何在 Spring Security 中允许多重登录?

标签 java spring spring-mvc spring-security

我想为我的 spring security 应用程序启用多点登录,例如,如果我有两个电子邮件地址,我希望允许用户使用多个电子邮件地址登录,并且用户可以从一个电子邮件地址切换考虑到另一个,就像Gmail Multiple Sign-in .我如何使用 Spring 安全来做到这一点?

似乎只有一个 Principal 而不是 Spring 安全性中的委托(delegate)人列表。我能实现吗?

SecurityContextHolder.getContext().getAuthentication().getPrincipal();

提前致谢。希望您尽快回复。

最佳答案

在某种程度上Spring Security支持用户切换。它更像是Linux下的一个su。 不过,您可以重用 SwitchUserFilter 中的一些代码创建您自己的用户开关。

首先,你需要创建

  • 自定义 Spring 安全 UserDetails其中包含允许切换的用户名列表
  • 自定义 UserDetailsService填充您的自定义 UserDetails
  • 基于Spring的SwitchUserFilter自定义UserSwitchFilter

自定义 UserDetailsUserDetailsS​​ervice 只是示例,可能与您自己的实现不同。这个想法是在 UserDetails 中保存一个用户名列表,以便稍后在自定义 UserSwitchFilter 中进行处理。

自定义用户详细信息:

public class CustomUserDetails extends User {

    private final Set<String> linkedAccounts;

    public CustomUserDetails(String username, String password, Set<String> linkedAccounts, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
        this.linkedAccounts = linkedAccounts;
    }

    public Set<String> getLinkedAccounts() {
        return linkedAccounts;
    }
}

自定义用户详细信息服务:

public class CustomUserDetailsService implements UserDetailsService {

    private UserDao userDao = ...;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        BackendUser user = userDao.findUserByUsername(username);        
        return new CustomUserDetails(user.getHane(), ......);
    }
}

与 Spring Security UserSwitchFilter 的主要区别:

  • 添加方法 checkSwitchAllowed 检查是否允许从当前经过身份验证的用户切换到该特定用户
  • switch 基于查询参数而不是 url 以获得更好的用户体验(参见 requiresSwitchUser)。因此 不需要 switchUserUrltargetUrl
  • 自定义 UserSwitchFilter 没有 exitUserUrl 的概念。因此不需要 exitUserUrl
  • createSwitchUserToken 不修改用户权限

自定义切换用户过滤器:

public class CustomSwitchUserFilter extends GenericFilterBean implements  ApplicationEventPublisherAware, MessageSourceAware {

    public static final String SPRING_SECURITY_SWITCH_USERNAME_KEY = "j_switch_username";

    private ApplicationEventPublisher eventPublisher;
    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private String switchFailureUrl;
    private String usernameParameter = SPRING_SECURITY_SWITCH_USERNAME_KEY;
    private UserDetailsService userDetailsService;
    private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
    private AuthenticationFailureHandler failureHandler;

    @Override
    public void afterPropertiesSet() {
        Assert.notNull(userDetailsService, "userDetailsService must be specified");
        if (failureHandler == null) {
            failureHandler = switchFailureUrl == null ? new SimpleUrlAuthenticationFailureHandler() :
                    new SimpleUrlAuthenticationFailureHandler(switchFailureUrl);
        } else {
            Assert.isNull(switchFailureUrl, "You cannot set both a switchFailureUrl and a failureHandler");
        }
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // check for switch or exit request
        if (requiresSwitchUser(request)) {
            // if set, attempt switch and store original
            try {
                Authentication targetUser = attemptSwitchUser(request);

                // update the current context to the new target user
                SecurityContextHolder.getContext().setAuthentication(targetUser);
            } catch (AuthenticationException e) {
                logger.debug("Switch User failed", e);
                failureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }

        chain.doFilter(request, response);
    }

    protected Authentication attemptSwitchUser(HttpServletRequest request) throws AuthenticationException {
        UsernamePasswordAuthenticationToken targetUserRequest;

        String username = request.getParameter(usernameParameter);

        if (username == null) {
            username = "";
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Attempt to switch to user [" + username + "]");
        }

        UserDetails targetUser = userDetailsService.loadUserByUsername(username);
        userDetailsChecker.check(targetUser);

        checkSwitchAllowed(targetUser);

        // OK, create the switch user token
        targetUserRequest = createSwitchUserToken(request, targetUser);

        if (logger.isDebugEnabled()) {
            logger.debug("Switch User Token [" + targetUserRequest + "]");
        }

        // publish event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new AuthenticationSwitchUserEvent(SecurityContextHolder.getContext().getAuthentication(), targetUser));
        }

        return targetUserRequest;
    }

    private void checkSwitchAllowed(UserDetails targetUser) {
        CustomUserDetails details = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String targetUsername = targetUser.getUsername();

        //target username has to be in linked accounts otherwise this is an unauthorized switch
        if(!details.getLinkedAccounts().contains(targetUsername)) {
            throw new InsufficientAuthenticationException("user switch not allowed");
        }
    }

    private UsernamePasswordAuthenticationToken createSwitchUserToken(HttpServletRequest request, UserDetails targetUser) {
        UsernamePasswordAuthenticationToken targetUserRequest;

        // get the original authorities
        Collection<? extends GrantedAuthority> orig = targetUser.getAuthorities();

        // add the new switch user authority
        List<GrantedAuthority> newAuths = new ArrayList<GrantedAuthority>(orig);

        // create the new authentication token
        targetUserRequest = new UsernamePasswordAuthenticationToken(targetUser, targetUser.getPassword(), newAuths);

        // set details
        targetUserRequest.setDetails(authenticationDetailsSource.buildDetails(request));

        return targetUserRequest;
    }

    protected boolean requiresSwitchUser(HttpServletRequest request) {
        Map<String, String[]> parameterMap = request.getParameterMap();
        return parameterMap.containsKey(usernameParameter);
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher)
            throws BeansException {
        this.eventPublisher = eventPublisher;
    }

    public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest,?> authenticationDetailsSource) {
        Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
        this.authenticationDetailsSource = authenticationDetailsSource;
    }

    public void setMessageSource(MessageSource messageSource) {
        Assert.notNull(messageSource, "messageSource cannot be null");
        this.messages = new MessageSourceAccessor(messageSource);
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    public void setSwitchFailureUrl(String switchFailureUrl) {
        Assert.isTrue(StringUtils.hasText(usernameParameter) && UrlUtils.isValidRedirectUrl(switchFailureUrl),
                "switchFailureUrl cannot be empty and must be a valid redirect URL");
        this.switchFailureUrl = switchFailureUrl;
    }

    public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
        Assert.notNull(failureHandler, "failureHandler cannot be null");
        this.failureHandler = failureHandler;
    }

    public void setUserDetailsChecker(UserDetailsChecker userDetailsChecker) {
        this.userDetailsChecker = userDetailsChecker;
    }

    public void setUsernameParameter(String usernameParameter) {
        this.usernameParameter = usernameParameter;
    }
}

CustomSwitchUserFilter 添加到您的安全过滤器链。它必须放在 FILTER_SECURITY_INTERCEPTOR 之后。

<security:authentication-manager alias="authenticationManager">
    <security:authentication-provider user-service-ref="userDetailsService"/>
</security:authentication-manager>

<security:http use-expressions="true">
    <security:intercept-url pattern="/**" access="isFullyAuthenticated()" />
    <security:form-login login-page="/login.do" />
    <security:logout logout-success-url="/login.do" />
    <security:custom-filter ref="switchUserProcessingFilter" after="FILTER_SECURITY_INTERCEPTOR" />
</security:http>

<bean id="switchUserProcessingFilter" class="security.CustomSwitchUserFilter">
    <property name="userDetailsService" ref="userDetailsService" />
</bean>

您可以找到一个工作示例 here .

关于java - 如何在 Spring Security 中允许多重登录?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24577091/

相关文章:

java - twitter4j 可序列化正在保存但未加载

java - Velocity - 调用 init() 时的 NPE

java - 如何使用今天的日期传入文件名值属性

java - 如何修复 UUID 类型的方法参数“''缺少 URI 模板变量 ' " 'uuid'?

java - 带有 Spring MVC 表单标签的日期模式

java - DataOutputStream 不断给出空指针异常

java - Tomcat 构建错误 : java. lang.NoClassDefFoundError

java - Spring NamedParameterJdbcTemplate 查询的性能非常慢

java - 为什么我坚持了却没有任何反应?

java - 无法打开 Hibernate Session 进行交易;嵌套异常是 org.hibernate.exception.JDBCConnectionException : Could not open connection