Spring-websockets : Spring 安全授权在 websockets 中不起作用
Spring-websockets : Spring security authorization not working inside websockets
我正在开发一个 Spring-MVC 应用程序,其中我们有 Spring- 身份验证和授权安全性。我们正在努力迁移到 Spring websockets,但我们在让经过身份验证的用户进入 websocket 连接时遇到了问题。安全上下文根本不存在于 websocket 连接中,但可以正常使用常规 HTTP。我们做错了什么?
网络套接字配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/app").withSockJS();
}
}
在下面的控制器中,我们正在尝试获取当前经过身份验证的用户,但它始终为 null
@Controller
public class OnlineStatusController extends MasterController{
@MessageMapping("/onlinestatus")
public void onlineStatus(String status) {
Person user = this.personService.getCurrentlyAuthenticatedUser();
if(user!=null){
this.chatService.setOnlineStatus(status, user.getId());
}
}
}
安全-applicationContext.xml :
<security:http pattern="/resources/**" security="none"/>
<security:http pattern="/org/**" security="none"/>
<security:http pattern="/jquery/**" security="none"/>
<security:http create-session="ifRequired" use-expressions="true" auto-config="false" disable-url-rewriting="true">
<security:form-login login-page="/login" username-parameter="j_username" password-parameter="j_password"
login-processing-url="/j_spring_security_check" default-target-url="/canvaslisting"
always-use-default-target="false" authentication-failure-url="/login?error=auth"/>
<security:remember-me key="_spring_security_remember_me" user-service-ref="userDetailsService"
token-validity-seconds="1209600" data-source-ref="dataSource"/>
<security:logout delete-cookies="JSESSIONID" invalidate-session="true" logout-url="/j_spring_security_logout"/>
<security:csrf disabled="true"/>
<security:intercept-url pattern="/cometd/**" access="permitAll" />
<security:intercept-url pattern="/app/**" access="hasAnyRole('ROLE_ADMIN','ROLE_USER')" />
<!-- <security:intercept-url pattern="/**" requires-channel="https"/>-->
<security:port-mappings>
<security:port-mapping http="80" https="443"/>
</security:port-mappings>
<security:logout logout-url="/logout" logout-success-url="/" success-handler-ref="myLogoutHandler"/>
<security:session-management session-fixation-protection="newSession">
<security:concurrency-control session-registry-ref="sessionReg" max-sessions="5" expired-url="/login"/>
</security:session-management>
</security:http>
我记得在我从事的项目中遇到过同样的问题。由于我无法使用 Spring 文档找到解决方案 - Stack Overflow 上的其他答案对我不起作用 - 我最终创建了一个解决方法。
技巧本质上是强制应用程序对 WebSocket 连接请求的用户进行身份验证。为此,您需要一个 class 来拦截此类事件,然后一旦您控制了它,就可以调用您的身份验证逻辑。
创建一个 class 来实现 Spring 的 ChannelInterceptorAdapter
。在此 class 中,您可以注入执行实际身份验证所需的任何 bean。我的示例使用基本身份验证:
@Component
public class WebSocketAuthInterceptorAdapter extends ChannelInterceptorAdapter {
@Autowired
private DaoAuthenticationProvider userAuthenticationProvider;
@Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {
final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
StompCommand cmd = accessor.getCommand();
if (StompCommand.CONNECT == cmd || StompCommand.SEND == cmd) {
Authentication authenticatedUser = null;
String authorization = accessor.getFirstNativeHeader("Authorization:");
String credentialsToDecode = authorization.split("\s")[1];
String credentialsDecoded = StringUtils.newStringUtf8(Base64.decodeBase64(credentialsToDecode));
String[] credentialsDecodedSplit = credentialsDecoded.split(":");
final String username = credentialsDecodedSplit[0];
final String password = credentialsDecodedSplit[1];
authenticatedUser = userAuthenticationProvider.authenticate(new UsernamePasswordAuthenticationToken(username, password));
if (authenticatedUser == null) {
throw new AccessDeniedException();
}
SecurityContextHolder.getContext().setAuthentication(authenticatedUser);
accessor.setUser(authenticatedUser);
}
return message;
}
}
然后,在您的 WebSocketConfig
class 中,您需要注册您的拦截器。将上面的class添加为bean并注册。完成这些更改后,您的 class 将如下所示:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Autowired
private WebSocketAuthInterceptorAdapter authInterceptorAdapter;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/app").withSockJS();
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(authInterceptorAdapter);
super.configureClientInboundChannel(registration);
}
}
显然,身份验证逻辑的细节由您决定。您可以调用 JWT 服务或任何您正在使用的服务。
如果您使用的是 SockJS + Stomp 并正确配置了您的安全性,您应该能够通过常规 username/pw 身份验证器(如 @AlgorithmFromHell)进行连接并执行
accessor.setUser(authentication.getPrincipal()) // stomp header accessor
您也可以通过 http://{END_POINT}/access_token={ACCESS_TOKEN} 连接。 Spring 安全应该能够选择它并通过 ResourceServerTokenServices 执行 loadAuthentication(access_token)。完成后,您可以通过将其添加到 AbstractSessionWebSocketMessageBrokerConfigurer 或 WebSocketMessageBrokerConfigurer 的实现中来获取您的委托人。执行此操作时,出于某种原因,加载的 Pricipal 保存在“simpUser”header 中。
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
if (message.getHeaders().get("simpUser") != null && message.getHeaders().get("simpUser") instanceof OAuth2Authentication) { // or Authentication depending on your impl of security
OAuth2Authentication authentication = (OAuth2Authentication) message.getHeaders().get("simpUser");
accessor.setUser(authentication != null ? (UserDetails) authentication.getPrincipal() : null);
}
}
return message;
}
});
}
我正在开发一个 Spring-MVC 应用程序,其中我们有 Spring- 身份验证和授权安全性。我们正在努力迁移到 Spring websockets,但我们在让经过身份验证的用户进入 websocket 连接时遇到了问题。安全上下文根本不存在于 websocket 连接中,但可以正常使用常规 HTTP。我们做错了什么?
网络套接字配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/app").withSockJS();
}
}
在下面的控制器中,我们正在尝试获取当前经过身份验证的用户,但它始终为 null
@Controller
public class OnlineStatusController extends MasterController{
@MessageMapping("/onlinestatus")
public void onlineStatus(String status) {
Person user = this.personService.getCurrentlyAuthenticatedUser();
if(user!=null){
this.chatService.setOnlineStatus(status, user.getId());
}
}
}
安全-applicationContext.xml :
<security:http pattern="/resources/**" security="none"/>
<security:http pattern="/org/**" security="none"/>
<security:http pattern="/jquery/**" security="none"/>
<security:http create-session="ifRequired" use-expressions="true" auto-config="false" disable-url-rewriting="true">
<security:form-login login-page="/login" username-parameter="j_username" password-parameter="j_password"
login-processing-url="/j_spring_security_check" default-target-url="/canvaslisting"
always-use-default-target="false" authentication-failure-url="/login?error=auth"/>
<security:remember-me key="_spring_security_remember_me" user-service-ref="userDetailsService"
token-validity-seconds="1209600" data-source-ref="dataSource"/>
<security:logout delete-cookies="JSESSIONID" invalidate-session="true" logout-url="/j_spring_security_logout"/>
<security:csrf disabled="true"/>
<security:intercept-url pattern="/cometd/**" access="permitAll" />
<security:intercept-url pattern="/app/**" access="hasAnyRole('ROLE_ADMIN','ROLE_USER')" />
<!-- <security:intercept-url pattern="/**" requires-channel="https"/>-->
<security:port-mappings>
<security:port-mapping http="80" https="443"/>
</security:port-mappings>
<security:logout logout-url="/logout" logout-success-url="/" success-handler-ref="myLogoutHandler"/>
<security:session-management session-fixation-protection="newSession">
<security:concurrency-control session-registry-ref="sessionReg" max-sessions="5" expired-url="/login"/>
</security:session-management>
</security:http>
我记得在我从事的项目中遇到过同样的问题。由于我无法使用 Spring 文档找到解决方案 - Stack Overflow 上的其他答案对我不起作用 - 我最终创建了一个解决方法。
技巧本质上是强制应用程序对 WebSocket 连接请求的用户进行身份验证。为此,您需要一个 class 来拦截此类事件,然后一旦您控制了它,就可以调用您的身份验证逻辑。
创建一个 class 来实现 Spring 的 ChannelInterceptorAdapter
。在此 class 中,您可以注入执行实际身份验证所需的任何 bean。我的示例使用基本身份验证:
@Component
public class WebSocketAuthInterceptorAdapter extends ChannelInterceptorAdapter {
@Autowired
private DaoAuthenticationProvider userAuthenticationProvider;
@Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {
final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
StompCommand cmd = accessor.getCommand();
if (StompCommand.CONNECT == cmd || StompCommand.SEND == cmd) {
Authentication authenticatedUser = null;
String authorization = accessor.getFirstNativeHeader("Authorization:");
String credentialsToDecode = authorization.split("\s")[1];
String credentialsDecoded = StringUtils.newStringUtf8(Base64.decodeBase64(credentialsToDecode));
String[] credentialsDecodedSplit = credentialsDecoded.split(":");
final String username = credentialsDecodedSplit[0];
final String password = credentialsDecodedSplit[1];
authenticatedUser = userAuthenticationProvider.authenticate(new UsernamePasswordAuthenticationToken(username, password));
if (authenticatedUser == null) {
throw new AccessDeniedException();
}
SecurityContextHolder.getContext().setAuthentication(authenticatedUser);
accessor.setUser(authenticatedUser);
}
return message;
}
}
然后,在您的 WebSocketConfig
class 中,您需要注册您的拦截器。将上面的class添加为bean并注册。完成这些更改后,您的 class 将如下所示:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Autowired
private WebSocketAuthInterceptorAdapter authInterceptorAdapter;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/app").withSockJS();
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(authInterceptorAdapter);
super.configureClientInboundChannel(registration);
}
}
显然,身份验证逻辑的细节由您决定。您可以调用 JWT 服务或任何您正在使用的服务。
如果您使用的是 SockJS + Stomp 并正确配置了您的安全性,您应该能够通过常规 username/pw 身份验证器(如 @AlgorithmFromHell)进行连接并执行
accessor.setUser(authentication.getPrincipal()) // stomp header accessor
您也可以通过 http://{END_POINT}/access_token={ACCESS_TOKEN} 连接。 Spring 安全应该能够选择它并通过 ResourceServerTokenServices 执行 loadAuthentication(access_token)。完成后,您可以通过将其添加到 AbstractSessionWebSocketMessageBrokerConfigurer 或 WebSocketMessageBrokerConfigurer 的实现中来获取您的委托人。执行此操作时,出于某种原因,加载的 Pricipal 保存在“simpUser”header 中。
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
if (message.getHeaders().get("simpUser") != null && message.getHeaders().get("simpUser") instanceof OAuth2Authentication) { // or Authentication depending on your impl of security
OAuth2Authentication authentication = (OAuth2Authentication) message.getHeaders().get("simpUser");
accessor.setUser(authentication != null ? (UserDetails) authentication.getPrincipal() : null);
}
}
return message;
}
});
}