我想为我的 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
自定义 UserDetails
和 UserDetailsService
只是示例,可能与您自己的实现不同。这个想法是在 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
)。因此不需要 switchUserUrl
和targetUrl
- 自定义 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/