我有一个Spring应用程序,它使用Security来使用JWT来保护端点,这对于标准的REST控制器都是正确的。该应用程序还具有WebSocket连接,我已经成功地使用相同的JWT保护了这些连接。
问题
如果我试图将主体注入到WebSocket控制器方法中,则在默认情况下主体为空。我已经找到了许多通过重写DefaultHandshakeHandler来设置主体的解决方案,但是这些解决方案中的大多数都会生成一个随机的主体名称。我宁愿使用现有的登录用户名来生成主体名称,这样我就可以看到哪个用户正在向WebSocket发送数据并相应地处理响应。
这里有一个答复,我认为它可以通过添加使我更接近,但是当将逻辑应用到我的preSend方法时,即使我将它作为StompJS连接方法的一个参数添加,也会得到一个空用户名:WebSocket Stomp over SockJS - http custom headers
我还发现奇怪的是,在拦截器本身中,我能够使用正确的用户名访问主体,但一旦到达控制器,它要么为null,要么是preSend握手方法中定义的任何内容。
下面是连接到WebSocket的方法:
_connect() {
console.log("Initialize WebSocket Connection");
if (this.authenticationService.isUserLoggedIn()) {
let ws = new SockJS(this.webSocketEndPoint);
this.stompClient = Stomp.over(ws);
const _this = this;
_this.stompClient.connect({
'Authorization': "Bearer " + this.authenticationService.getLoggedInUserToken(),
'username': this.authenticationService.getLoggedInUserName()
}, function (frame) {
_this.stompClient.subscribe(_this.board, function (data) {
_this.onDataReceived(data);
});
}, this.errorCallBack);
} else {
console.log("Unable to connect: must be logged in.");
}
}
这是我当前的握手代码,有注释结果:
public class MyHandshakeHandler extends DefaultHandshakeHandler {
@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler,
Map<String, Object> attributes) {
Principal principal = SecurityContextHolder.getContext().getAuthentication();
System.out.println("PRINCIPAL HANDSHAKE START:" + principal); //prints annonymous user
final String ATTR_PRINCIPAL = "__principal__";
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpServletRequest httpServletRequest = servletRequest.getServletRequest();
String username = httpServletRequest.getParameter("username");
//System.out.println("Username is: " + username); // null
final String name;
if (!attributes.containsKey(ATTR_PRINCIPAL)) {
name = "My made up name"; //Would like this to be the logged in user, but is currently null
attributes.put(ATTR_PRINCIPAL, name);
} else {
name = (String) attributes.get(ATTR_PRINCIPAL);
}
return new Principal() {
@Override
public String getName() {
return name;
}
};
}
}
最后,我的拦截器授权WebSocket请求并显示正确的登录用户:
@Configuration
@EnableWebSocketMessageBroker
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketAuthenticationConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserService userService;
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
Principal principal = SecurityContextHolder.getContext().getAuthentication();
System.out.println("PRINCIPAL PRE-SEND START:" + principal); // shows annonymous user
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
List<String> tokenList = accessor.getNativeHeader("Authorization");
String jwt = null;
if (tokenList == null || tokenList.size() < 1) {
return message;
} else {
jwt = tokenList.get(0).substring(7);
if (jwt == null) {
return message;
}
}
String username = jwtUtil.extractUsername(jwt); //Decoder class that can retrieve username from token
UserDetails userDetails = userService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//Used only to see which principal is found at this stage
principal = SecurityContextHolder.getContext().getAuthentication();
System.out.println("PRINCIPAL PRE-SEND END:" + principal); // shows correct logged in user
accessor.setUser(authentication);
}
return message;
}
});
}
}
以下是当前正在对主体进行测试的Controller方法:
@MessageMapping("/test")
public void test(@Payload String message, Principal principal) throws Exception {
System.out.println("PRINCIPAL TEST INJECTED: " + principal.getName());
Principal retrievedPrincipal = SecurityContextHolder.getContext().getAuthentication();
System.out.println("PRINCIPAL TEST RETRIEVED: " + retrievedPrincipal); //null
webSocket.convertAndSend("/topic/test", new ResponseMessage("TestSuccess", "Thanks for the request " + principal.getName())); // shows "made up username" from handshake
}
下面是一个名为“示例”的登录用户连接到WebSocket并尝试访问/app/test端点时的控制台输出:
PRINCIPAL HANDSHAKE START:AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]]
PRINCIPAL PRE-SEND START:null
PRINCIPAL PRE-SEND END:UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=example, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]]
PRINCIPAL PRE-SEND START:null
PRINCIPAL PRE-SEND START:null
PRINCIPAL TEST INJECTED: My made up name
PRINCIPAL TEST RETRIEVED: SecurityContextImpl [Null authentication]
请让我知道,如果您需要任何进一步的代码,以明确。
发布于 2022-01-08 18:57:40
以下是我经过大量研究后所做的工作。我到达这个位置(https://coderedirect.com/questions/310917/secure-spring-webscoket-using-spring-security-and-access-principal-from-websocke)是从搜索google @MessageMapping Principal is null
作为查询,以查看与我们的实现有什么不同,而不是
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
当我尝试
StompHeaderAccessor accessor = MessageHeaderAccessor
.getAccessor(message, StompHeaderAccessor.class);
控制器内的Principal
现在不是空的。我不确定spring-messaging
和MessageHeaderAccessor
的深度
从我搜索的初始线程中得到的更多的参考是JSON Web Token (JWT) with Spring based SockJS / STOMP Web Socket,它最终找到了主要答案的真相来源。
https://stackoverflow.com/questions/65919245
复制相似问题