Java Spring中的Websocket身份验证和授权
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/45405332/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Websocket Authentication and Authorization in Spring
提问by Anthony Raymond
I've been struggling a lot to properly implement Stomp (websocket) Authenticationand Authorizationwith Spring-Security. For posterity i'll answer my own question to provide a guide.
我一直在努力使用 Spring-Security正确实现 Stomp (websocket)身份验证和授权。对于后代,我将回答我自己的问题以提供指导。
The Problem
问题
Spring WebSocket documentation (for Authentication) looks unclear ATM (IMHO). And i couldn't understand how to properly handle Authenticationand Authorization.
Spring WebSocket 文档(用于身份验证)看起来不清楚 ATM(恕我直言)。我无法理解如何正确处理Authentication和Authorization。
What i want
我想要的是
- Authenticate users with login/password.
- Prevent anonymous users to CONNECT though WebSocket.
- Add authorization layer (user, admin, ...).
- Having
Principal
available in controllers.
- 使用登录名/密码验证用户。
- 防止匿名用户通过 WebSocket 连接。
- 添加授权层(用户、管理员、...)。
- 有
Principal
在控制器可用。
What i don't want
我不想要的
- Authenticate on HTTP negotiation endpoints (since most of JavaScript libraries don't sends authentication headers along with the HTTP negotiation call).
- 在 HTTP 协商端点上进行身份验证(因为大多数 JavaScript 库不随 HTTP 协商调用一起发送身份验证标头)。
采纳答案by Anthony Raymond
As stated above the documentation (ATM) is unclear, until Spring provide some clear documentation, here is a boilerplate to save you from spending two days trying to understand what the security chain is doing.
如上所述,文档 (ATM) 不清楚,直到 Spring 提供一些清晰的文档,这里有一个样板,可以让您免于花两天时间试图了解安全链正在做什么。
A really nice attempt was made by Rob-Leggettbut, he was forking some Springs classand i don't feel comfortable doing that.
Rob-Leggett进行了一次非常好的尝试,但是,他分叉了一些 Springs 课程,我觉得这样做不太舒服。
Things to know:
须知:
- Security chainand Security configfor http and WebSocket are completely independent.
- Spring
AuthenticationProvider
take not part at all in Websocket authentication. - The authentication won't happend on HTTP negotiation endpoint because none of the JavaScripts STOMP (websocket) sends the authentication headres along with the HTTP request.
- Once set on CONNECT request, the user(
simpUser
) will be stored in the websocket session and no more authentication will be required on further messages.
- http 和 WebSocket 的安全链和安全配置是完全独立的。
- Spring
AuthenticationProvider
根本不参与 Websocket 身份验证。 - 身份验证不会在 HTTP 协商端点上发生,因为 JavaScripts STOMP (websocket) 都不会随 HTTP 请求一起发送身份验证标头。
- 一旦在 CONNECT 请求上设置,用户(
simpUser
) 将存储在 websocket 会话中,并且不再需要对进一步消息进行身份验证。
Maven deps
Maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-messaging</artifactId>
</dependency>
WebSocket configuration
WebSocket 配置
The below config register a simple message broker (Note that it has nothing to do with authentication nor authorization).
下面的配置注册了一个简单的消息代理(请注意,它与身份验证或授权无关)。
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(final MessageBrokerRegistry config) {
// These are endpoints the client can subscribes to.
config.enableSimpleBroker("/queue/topic");
// Message received with one of those below destinationPrefixes will be automatically router to controllers @MessageMapping
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
// Handshake endpoint
registry.addEndpoint("stomp"); // If you want to you can chain setAllowedOrigins("*")
}
}
Spring security config
Spring安全配置
Since the Stomp protocol rely on a first HTTP Request, we'll need to authorize HTTP call to our stomp handshake endpoint.
由于 Stomp 协议依赖于第一个 HTTP 请求,因此我们需要授权对我们的 stomp 握手端点的 HTTP 调用。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(final HttpSecurity http) throws Exception {
// This is not for websocket authorization, and this should most likely not be altered.
http
.httpBasic().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().antMatchers("/stomp").permitAll()
.anyRequest().denyAll();
}
}
Then we'll create a service responsible for authenticating users.
然后我们将创建一个负责对用户进行身份验证的服务。
@Component
public class WebSocketAuthenticatorService {
// This method MUST return a UsernamePasswordAuthenticationToken instance, the spring security chain is testing it with 'instanceof' later on. So don't use a subclass of it or any other class
public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final String username, final String password) throws AuthenticationException {
if (username == null || username.trim().isEmpty()) {
throw new AuthenticationCredentialsNotFoundException("Username was null or empty.");
}
if (password == null || password.trim().isEmpty()) {
throw new AuthenticationCredentialsNotFoundException("Password was null or empty.");
}
// Add your own logic for retrieving user in fetchUserFromDb()
if (fetchUserFromDb(username, password) == null) {
throw new BadCredentialsException("Bad credentials for user " + username);
}
// null credentials, we do not pass the password along
return new UsernamePasswordAuthenticationToken(
username,
null,
Collections.singleton((GrantedAuthority) () -> "USER") // MUST provide at least one role
);
}
}
Note that: UsernamePasswordAuthenticationToken
MUSThave GrantedAuthorities, if you use another constructor, Spring will auto-set isAuthenticated = false
.
注意:UsernamePasswordAuthenticationToken
必须有 GrantedAuthorities,如果你使用另一个构造函数,Spring 会自动设置isAuthenticated = false
。
Almost there, now we need to create an Interceptor that will set the simpUser
header or throw AuthenticationException
on CONNECT messages.
差不多了,现在我们需要创建一个拦截器,它将设置simpUser
标题或抛出AuthenticationException
CONNECT 消息。
@Component
public class AuthChannelInterceptorAdapter extends ChannelInterceptor {
private static final String USERNAME_HEADER = "login";
private static final String PASSWORD_HEADER = "passcode";
private final WebSocketAuthenticatorService webSocketAuthenticatorService;
@Inject
public AuthChannelInterceptorAdapter(final WebSocketAuthenticatorService webSocketAuthenticatorService) {
this.webSocketAuthenticatorService = webSocketAuthenticatorService;
}
@Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {
final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT == accessor.getCommand()) {
final String username = accessor.getFirstNativeHeader(USERNAME_HEADER);
final String password = accessor.getFirstNativeHeader(PASSWORD_HEADER);
final UsernamePasswordAuthenticationToken user = webSocketAuthenticatorService.getAuthenticatedOrFail(username, password);
accessor.setUser(user);
}
return message;
}
}
Note that: preSend()
MUSTreturn a UsernamePasswordAuthenticationToken
, another element in the spring security chain test this.
Note that: If your UsernamePasswordAuthenticationToken
was built without passing GrantedAuthority
, the authentication will fail, because the constructor without granted authorities auto set authenticated = false
THIS IS AN IMPORTANT DETAIL which is not documented in spring-security.
请注意:preSend()
必须返回 a UsernamePasswordAuthenticationToken
,spring 安全链中的另一个元素对此进行测试。请注意:如果您UsernamePasswordAuthenticationToken
是在未通过GrantedAuthority
的情况下构建的,则身份验证将失败,因为没有授予权限的构造函数会自动设置authenticated = false
这是一个重要的细节,在 spring-security 中没有记录。
Finally create two more class to handle respectively Authorization and Authentication.
最后再创建两个类来分别处理授权和认证。
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketAuthenticationSecurityConfig extends WebSocketMessageBrokerConfigurer {
@Inject
private AuthChannelInterceptorAdapter authChannelInterceptorAdapter;
@Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
// Endpoints are already registered on WebSocketConfig, no need to add more.
}
@Override
public void configureClientInboundChannel(final ChannelRegistration registration) {
registration.setInterceptors(authChannelInterceptorAdapter);
}
}
Note that: The @Order
is CRUCIALdon't forget it, it allows our interceptor to be registered first in the security chain.
需要注意的是:本@Order
是至关重要的,不要忘记它,它让我们的拦截器在安全链中第一个进行注册。
@Configuration
public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) {
// You can customize your authorization mapping here.
messages.anyMessage().authenticated();
}
// TODO: For test purpose (and simplicity) i disabled CSRF, but you should re-enable this and provide a CRSF endpoint.
@Override
protected boolean sameOriginDisabled() {
return true;
}
}
回答by tbw
for java client side use this tested example:
对于 Java 客户端,请使用此经过测试的示例:
StompHeaders connectHeaders = new StompHeaders();
connectHeaders.add("login", "test1");
connectHeaders.add("passcode", "test");
stompClient.connect(WS_HOST_PORT, new WebSocketHttpHeaders(), connectHeaders, new MySessionHandler());