Ste*_*ord 44 spring spring-security websocket jwt sockjs
我正在使用包含STOMP/SockJS WebSocket的Spring Boot(1.3.0.BUILD-SNAPSHOT)设置RESTful Web应用程序,我打算从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);
}
}
Run Code Online (Sandbox Code Playgroud)
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();
}
}
Run Code Online (Sandbox Code Playgroud)
我还有一个子类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;
}
}
Run Code Online (Sandbox Code Playgroud)
还有一些带@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是否适合这一点.虽然文档非常详细,但似乎没有一个简单的(也就是白痴)指南来指导各个组件如何相互配合/相互作用.
如何通过提供JSON Web令牌来保护SockJS WebSocket,最好是在握手点(甚至可能)?
Ram*_*man 42
更新2016-12-13:下面引用的问题现在已标记为已修复,因此不再需要以下版本的Spring 4.3.5或更高版本.请参阅https://github.com/spring-projects/spring-framework/blob/master/src/docs/asciidoc/web/web-websocket.adoc#token-based-authentication.
目前(2016年9月),除了@ rossen-stoyanchev所回答的查询参数之外,Spring不支持这一点,后者写了很多(全部?)Spring WebSocket支持.我不喜欢查询参数方法,因为潜在的HTTP引用漏洞和服务器日志中令牌的存储.此外,如果安全性后果不打扰您,请注意我发现此方法适用于真正的WebSocket连接,但如果您使用SockJS与其他机制的回退,determineUser则永远不会调用该方法进行回退.请参阅基于Spring 4.x令牌的WebSocket SockJS后备身份验证.
我已经创建了一个Spring问题来改进对基于令牌的WebSocket身份验证的支持:https://jira.spring.io/browse/SPR-14690
与此同时,我发现了一个在测试中运行良好的黑客攻击.绕过内置的Spring连接级Spring auth机器.相反,通过在客户端的Stomp头中发送身份验证令牌,将其设置在消息级别(这很好地反映了您已经在使用常规HTTP XHR调用执行的操作),例如:
stompClient.connect({'X-Authorization': 'token'}, ...);
stompClient.subscribe(..., {'X-Authorization': 'token'});
stompClient.send("/wherever", {'X-Authorization': 'token'}, ...);
Run Code Online (Sandbox Code Playgroud)
在服务器端,使用a从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)
}
})
Run Code Online (Sandbox Code Playgroud)
这很简单,让我们有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);
}
})
}
}
Run Code Online (Sandbox Code Playgroud)
现在,Spring完全了解身份验证,即它将注入Principal到需要它的任何控制器方法中,将其公开给Spring Security 4.x的上下文,并将用户与WebSocket会话相关联,以便向特定用户/会话发送消息.
最后,如果您使用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 = [...];
Run Code Online (Sandbox Code Playgroud)
这几乎不在问题的范围内,因为它不是特定于Stomp的,但我会稍微扩展它,因为它与使用Spring的令牌相关.使用基于令牌的身份验证时,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
}
Run Code Online (Sandbox Code Playgroud)
现在你需要一个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)
}
Run Code Online (Sandbox Code Playgroud)
注入TokenHandler抽象的JWT令牌解析,但应使用像jjwt这样的通用JWT令牌库.注入AuthService是您的抽象,实际上UserEntity基于令牌中的声明创建您的抽象,并且可以与您的用户数据库或其他后端系统进行通信.
现在,回到我们开始的那一行,它可能看起来像这样,它authenticationManager是AuthenticationManager由Spring注入我们的适配器,并且是CustomTokenAuthenticationManager我们在上面定义的实例:
Principal yourAuth = token == null ? null : authenticationManager.authenticate(new JwtAuthentication(token));
Run Code Online (Sandbox Code Playgroud)
然后将该主体附加到如上所述的消息上.HTH!
小智 8
使用最新的SockJS 1.0.3,您可以将查询参数作为连接URL的一部分传递.因此,您可以发送一些JWT令牌来授权会话.
var socket = new SockJS('http://localhost/ws?token=AAA');
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
stompClient.subscribe('/topic/echo', function(data) {
// topic handler
});
}
}, function(err) {
// connection error
});
Run Code Online (Sandbox Code Playgroud)
现在所有与websocket相关的请求都有参数"?token = AAA"
HTTP://本地主机/ WS /信息标记= AAA&T = 1446482506843
HTTP://本地主机/ WS/515/z45wjz24/WebSocket的标记= AAA
然后使用Spring,您可以设置一些过滤器,该过滤器将使用提供的令牌识别会话.
到目前为止,可以将身份验证令牌添加为请求参数并在握手时处理它,或者将其添加为连接到 stomp 端点的标头,并CONNECT在拦截器中的命令上处理它。
最好的办法是使用标头,但问题是您无法在握手步骤访问本机标头,因此您将无法在那里处理身份验证。
让我举一些示例代码:
配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-test")
.setHandshakeHandler(new SecDefaultHandshakeHandler())
.addInterceptors(new HttpHandshakeInterceptor())
.withSockJS()
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new JwtChannelInterceptor())
}
}
Run Code Online (Sandbox Code Playgroud)
握手拦截器:
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Map<String, Object> attributes) {
attributes.put("token", request.getServletRequest().getParameter("auth_token")
return true
}
}
Run Code Online (Sandbox Code Playgroud)
握手处理程序:
public class SecDefaultHandshakeHandler extends DefaultHandshakeHandler {
@Override
public Principal determineUser(ServerHttpRequest request, WebSocketHandler handler, Map<String, Object> attributes) {
Object token = attributes.get("token")
//handle authorization here
}
}
Run Code Online (Sandbox Code Playgroud)
通道拦截器:
public class JwtChannelInterceptor implements ChannelInterceptor {
@Override
public void postSend(Message message, MessageChannel channel, Boolean sent) {
MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class)
if (StompCommand.DISCONNECT == accessor.getCommand()) {
//retrieve Principal here via accessor.getUser()
//or get auth header from the accessor and handle authorization
}
}
}
Run Code Online (Sandbox Code Playgroud)
对于可能的编译错误,我很抱歉,我是从 Kotlin 代码手动转换的 =)
正如您提到的,您的 WebSocket 既有 Web 客户端,也有移动客户端,请注意,为所有客户端维护相同的代码库存在一些困难。请参阅我的主题:Spring Websocket ChannelInterceptor not fire CONNECT event
| 归档时间: |
|
| 查看次数: |
26846 次 |
| 最近记录: |