背景
我正在使用Spring Boot(1.3.0.BUILD-SNAPSHOT)设置一个RESTful Web应用程序,该应用程序包括一个STOMP / SockJS WebSocket,我打算从iOS应用程序和Web浏览器中使用它。我想使用JSON Web令牌(JWT)来保护REST请求和WebSocket接口,但后者遇到了困难。
该应用程序受Spring Security保护:
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
public WebSecurityConfiguration() {
super(true);
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("steve").password("steve").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling().and()
.anonymous().and()
.servletApi().and()
.headers().cacheControl().and().and()
// Relax CSRF on the WebSocket due to needing direct access from apps
.csrf().ignoringAntMatchers("/ws/**").and()
.authorizeRequests()
//allow anonymous resource requests
.antMatchers("/", "/index.html").permitAll()
.antMatchers("/resources/**").permitAll()
//allow anonymous POSTs to JWT
.antMatchers(HttpMethod.POST, "/rest/jwt/token").permitAll()
// Allow anonymous access to websocket
.antMatchers("/ws/**").permitAll()
//all other request need to be authenticated
.anyRequest().hasRole("USER").and()
// Custom authentication on requests to /rest/jwt/token
.addFilterBefore(new JWTLoginFilter("/rest/jwt/token", authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)
// Custom JWT based authentication
.addFilterBefore(new JWTTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
WebSocket配置是标准配置:
@Configuration
@EnableScheduling
@EnableWebSocketMessageBroker
public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
}
我还有一个子类AbstractSecurityWebSocketMessageBrokerConfigurer
来保护WebSocket:-
@Configuration
public class WebSocketSecurityConfiguration extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.anyMessage().hasRole("USER");
}
@Override
protected boolean sameOriginDisabled() {
// We need to access this directly from apps, so can't do cross-site checks
return true;
}
}
还有两个带@RestController
注释的类可以处理各种功能,并且可以通过JWTTokenFilter
在我的WebSecurityConfiguration
课程中注册的方式成功保护它们。
问题
但是我似乎无法用JWT保护WebSocket。我在浏览器中使用SockJS 1.1.0和STOMP 1.7.1,无法弄清楚如何传递令牌。它看来, SockJS不允许参数与最初的发送/info和/或握手请求。
在Spring Security进行的WebSockets文档指出的是,AbstractSecurityWebSocketMessageBrokerConfigurer
确保:
任何入站CONNECT消息均需要有效的CSRF令牌来实施“同源起点策略”
这似乎暗示着初始握手应该是不安全的,并且在接收到STOMP CONNECT消息时将调用身份验证。不幸的是,我似乎找不到任何有关执行此操作的信息。另外,此方法将需要其他逻辑来断开打开WebSocket连接且从不发送STOMP CONNECT的流氓客户端的连接。
作为Spring的(新手)我也不确定Spring Sessions是否适合于此。尽管文档非常详细,但似乎没有一个很好的,简单的(又称白痴)指南来说明各个组件如何组合在一起/如何相互影响。
我发现了一种在测试中效果很好的黑客程序。绕过内置的Spring连接级Spring auth机制。相反,可以通过在客户端的Stomp标头中发送身份验证令牌来在消息级别设置身份验证令牌(这很好地反映了常规HTTP XHR调用已在执行的操作),例如:
stompClient.connect({'X-Authorization': 'token'}, ...);
stompClient.subscribe(..., {'X-Authorization': 'token'});
stompClient.send("/wherever", {'X-Authorization': 'token'}, ...);
在服务器端,使用以下命令从Stomp消息中获取令牌: ChannelInterceptor
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
Message<*> preSend(Message<*> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
List tokenList = accessor.getNativeHeader("X-Authorization");
String token = null;
if(tokenList == null || tokenList.size < 1) {
return message;
} else {
token = tokenList.get(0);
if(token == null) {
return message;
}
}
// validate and convert to a Principal based on your own requirements e.g.
// authenticationManager.authenticate(JwtAuthentication(token))
Principal yourAuth = [...];
accessor.setUser(yourAuth);
// not documented anywhere but necessary otherwise NPE in StompSubProtocolHandler!
accessor.setLeaveMutable(true);
return MessageBuilder.createMessage(message.payload, accessor.messageHeaders)
}
})
这很简单,可以让我们达到85%的方式,但是,这种方法不支持向特定用户发送消息。这是因为Spring的将用户关联到会话的机制不受的结果影响ChannelInterceptor
。Spring WebSocket假定身份验证是在传输层而不是消息层完成的,因此忽略了消息级身份验证。
使这项工作反正砍,就是创造我们的情况DefaultSimpUserRegistry
和DefaultUserDestinationResolver
,揭露那些环境,然后用拦截器来更新这些仿佛Spring本身是这样做。换句话说,类似:
@Configuration
@EnableWebSocketMessageBroker
@Order(HIGHEST_PRECEDENCE + 50)
class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer() {
private DefaultSimpUserRegistry userRegistry = new DefaultSimpUserRegistry();
private DefaultUserDestinationResolver resolver = new DefaultUserDestinationResolver(userRegistry);
@Bean
@Primary
public SimpUserRegistry userRegistry() {
return userRegistry;
}
@Bean
@Primary
public UserDestinationResolver userDestinationResolver() {
return resolver;
}
@Override
public configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue", "/topic");
}
@Override
public registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/stomp")
.withSockJS()
.setWebSocketEnabled(false)
.setSessionCookieNeeded(false);
}
@Override public configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
Message<*> preSend(Message<*> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
List tokenList = accessor.getNativeHeader("X-Authorization");
accessor.removeNativeHeader("X-Authorization");
String token = null;
if(tokenList != null && tokenList.size > 0) {
token = tokenList.get(0);
}
// validate and convert to a Principal based on your own requirements e.g.
// authenticationManager.authenticate(JwtAuthentication(token))
Principal yourAuth = token == null ? null : [...];
if (accessor.messageType == SimpMessageType.CONNECT) {
userRegistry.onApplicationEvent(SessionConnectedEvent(this, message, yourAuth));
} else if (accessor.messageType == SimpMessageType.SUBSCRIBE) {
userRegistry.onApplicationEvent(SessionSubscribeEvent(this, message, yourAuth));
} else if (accessor.messageType == SimpMessageType.UNSUBSCRIBE) {
userRegistry.onApplicationEvent(SessionUnsubscribeEvent(this, message, yourAuth));
} else if (accessor.messageType == SimpMessageType.DISCONNECT) {
userRegistry.onApplicationEvent(SessionDisconnectEvent(this, message, accessor.sessionId, CloseStatus.NORMAL));
}
accessor.setUser(yourAuth);
// not documented anywhere but necessary otherwise NPE in StompSubProtocolHandler!
accessor.setLeaveMutable(true);
return MessageBuilder.createMessage(message.payload, accessor.messageHeaders);
}
})
}
}
现在,Spring完全意识到了身份验证,即它将注入Principal到需要它的任何控制器方法中,将其公开给Spring Security 4.x的上下文,并将用户与WebSocket会话相关联,以向特定的用户/会话发送消息。
Spring Security Messaging
最后,如果你使用Spring Security 4.x Messaging
支持,请确保将@Order你的AbstractWebSocketMessageBrokerConfigurer
设置为高于Spring Security
的值AbstractSecurityWebSocketMessageBrokerConfigurer
(Ordered.HIGHEST_PRECEDENCE + 50
将起作用,如上所示)。这样,你的拦截器将Principal在Spring Security
执行其检查之前设置并设置安全上下文。
上面的代码中的这一行似乎使很多人感到困惑:
// validate and convert to a Principal based on your own requirements e.g.
// authenticationManager.authenticate(JwtAuthentication(token))
Principal yourAuth = [...];
这不是问题的范围,因为它不是特定于Stomp的,但是无论如何我都会对其进行一些扩展,因为它与在Spring中使用auth令牌有关。使用基于令牌的身份验证时,Principal通常需要的JwtAuthentication
是扩展Spring Security的AbstractAuthenticationToken
类的自定义类。AbstractAuthenticationToken
实现Authentication
扩展该Principal
接口的接口,并包含将令牌与Spring Security
集成的大多数机制。
因此,在Kotlin代码中(很抱歉,我没有时间或意愿将其转换回Java),你JwtAuthentication
可能看起来像这样,这是一个简单的包装AbstractAuthenticationToken
:
import my.model.UserEntity
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.core.GrantedAuthority
class JwtAuthentication(
val token: String,
// UserEntity is your application's model for your user
val user: UserEntity? = null,
authorities: Collection<GrantedAuthority>? = null) : AbstractAuthenticationToken(authorities) {
override fun getCredentials(): Any? = token
override fun getName(): String? = user?.id
override fun getPrincipal(): Any? = user
}
现在你需要一个AuthenticationManager
知道如何处理它的人。在Kotlin中,这可能看起来像以下内容:
@Component
class CustomTokenAuthenticationManager @Inject constructor(
val tokenHandler: TokenHandler,
val authService: AuthService) : AuthenticationManager {
val log = logger()
override fun authenticate(authentication: Authentication?): Authentication? {
return when(authentication) {
// for login via username/password e.g. crash shell
is UsernamePasswordAuthenticationToken -> {
findUser(authentication).let {
//checkUser(it)
authentication.withGrantedAuthorities(it).also { setAuthenticated(true) }
}
}
// for token-based auth
is JwtAuthentication -> {
findUser(authentication).let {
val tokenTypeClaim = tokenHandler.parseToken(authentication.token)[CLAIM_TOKEN_TYPE]
when(tokenTypeClaim) {
TOKEN_TYPE_ACCESS -> {
//checkUser(it)
authentication.withGrantedAuthorities(it).also { setAuthenticated(true) }
}
TOKEN_TYPE_REFRESH -> {
//checkUser(it)
JwtAuthentication(authentication.token, it, listOf(SimpleGrantedAuthority(Authorities.REFRESH_TOKEN)))
}
else -> throw IllegalArgumentException("Unexpected token type claim $tokenTypeClaim.")
}
}
}
else -> null
}
}
private fun findUser(authentication: JwtAuthentication): UserEntity =
authService.login(authentication.token) ?:
throw BadCredentialsException("No user associated with token or token revoked.")
private fun findUser(authentication: UsernamePasswordAuthenticationToken): UserEntity =
authService.login(authentication.principal.toString(), authentication.credentials.toString()) ?:
throw BadCredentialsException("Invalid login.")
@Suppress("unused", "UNUSED_PARAMETER")
private fun checkUser(user: UserEntity) {
// TODO add these and lock account on x attempts
//if(!user.enabled) throw DisabledException("User is disabled.")
//if(user.accountLocked) throw LockedException("User account is locked.")
}
fun JwtAuthentication.withGrantedAuthorities(user: UserEntity): JwtAuthentication {
return JwtAuthentication(token, user, authoritiesOf(user))
}
fun UsernamePasswordAuthenticationToken.withGrantedAuthorities(user: UserEntity): UsernamePasswordAuthenticationToken {
return UsernamePasswordAuthenticationToken(principal, credentials, authoritiesOf(user))
}
private fun authoritiesOf(user: UserEntity) = user.authorities.map(::SimpleGrantedAuthority)
}
注入的内容将TokenHandler
抽象出JWT令牌解析,但应使用通用的JWT令牌库,如jjwt。注入的内容AuthService
是你的抽象,它实际上是UserEntity
根据令牌中的声明创建你的抽象的,并且可以与你的用户数据库或其他后端系统对话。
现在,回到我们开始与线,它可能是这个样子,哪里authenticationManager
是一个AuthenticationManager
注入由spring我们适配器,是一个实例CustomTokenAuthenticationManager
上面定义我们:
Principal yourAuth = token == null ? null : authenticationManager.authenticate(new JwtAuthentication(token));
然后,将此主体附加到消息中,如上所述。HTH!
我们正在将SpringWebSockets集成到我们的应用程序中,我运行了HelloWorld示例,令人惊讶的是,spring为我们连接了一切,以便将服务器端通知推送到客户端。 不过,我有一些简单的问题 1) 队列是如何创建的?我使用的是ActiveMQ,队列名称与我在目的地中指定的不同(例如,像greetings-user3n9\u jn3i)。 2)目标名称是否不同于队列? 3) 我正在使用A
问题内容: 老派:我正在使用Java SE 5(或Javav1.5)(请不要告诉我进行升级,因为对于我正在从事的工作[私有],我需要使用此版本的Java )。 我需要设置Web客户端/服务器应用程序的帮助。我在网上寻找的每个变体都缩小了使用websockets /sockets的范围,但是我使用的Java版本还没有套接字。 有没有人有不使用套接字来设置客户端/服务器模型应用程序的教程,或者一些示例
application.yml:
问题内容: 我有一个REST API,我正在使用Spring Security基本授权进行身份验证,客户端会为每个请求发送用户名和密码。现在,我想实现基于令牌的身份验证,当用户最初通过身份验证时,我将在响应标头中发送令牌。对于进一步的请求,客户端可以在令牌中包含该令牌,该令牌将用于对资源进行用户身份验证。我有两个身份验证提供程序tokenAuthenticationProvider和daoAuth
在daoAuthenticationProvider中,我设置了自定义userDetailsService并通过从数据库中获取用户登录详细信息对其进行身份验证(只要使用授权传递用户名和密码就可以正常工作:basic bgllqxbpvxnlcjogn21wxidmqjrdturtr04pag==作为头) 但是当我使用X-AUTH-TOKEN(即constants.auth_header_name)
问题内容: 有没有一种方法可以将WebSocket与SockJS客户端和Spring 4服务器一起使用,而不能与STOMP一起使用? 基于Spring网站上的本教程,我知道如何使用Stomp和Spring 4设置基于WebSocket的应用程序。在客户端,我们有: 在服务器端,控制器中包含以下内容: 现在,我了解可以确保如果将消息发送到目的地,则将调用该方法。并且由于已订阅,因此会将邮件发送回。