我有一个 Spring Boot 应用程序,它使用 Spring Security 来使用 JWT 保护端点,这对于标准 REST 控制器来说都可以正常工作。 该应用程序还具有 WebSocket 连接,我已使用相同的 JWT 成功保护了该连接。
如果我尝试将Principal注入到WebSocket控制器方法中,则Principal默认为null。我发现了许多通过覆盖 DefaultHandshakeHandler 来设置主体的解决方案,但是大多数解决方案都会生成随机主体名称。我宁愿使用现有登录用户名来生成主体名称,以便我可以看到哪个用户正在向 WebSocket 发送数据并相应地处理响应。
这里有一个回复,我认为通过添加可以让我更接近,但是当将逻辑应用到我的 preSend 方法时,即使我将其添加为我的 StompJS 连接方法的参数,我也会得到一个空用户名: WebSocket Stomp over SockJS - http 自定义标头
我还觉得奇怪的是,在拦截器本身内,我能够使用正确的用户名访问主体,但是一旦它到达控制器,它要么为空,要么为 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;
}
});
}
}
这是当前正在为主体测试的控制器方法:
@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
}
以下是名为“example”的登录用户连接到 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]
为了清楚起见,如果您需要任何进一步的代码,请告诉我。
这是我经过大量研究后所做的。
我通过搜索 google @MessageMapping Principal is null
作为查询到达这个位置(
https://coderedirect.com/questions/310917/secure-spring-webscoket-using-spring-security-and-access-principal-from-websocke)看看我们的实现有什么不同,我发现而不是
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
当我尝试时
StompHeaderAccessor accessor = MessageHeaderAccessor
.getAccessor(message, StompHeaderAccessor.class);
控制器内的Principal
现在不为空。
我不确定spring-messaging
和MessageHeaderAccessor
的深度
我搜索的初始线程的更多参考是JSON Web Token(JWT)与基于Spring的SockJS / STOMP Web Socket最终到达主要答案的真相来源
你知道为什么我检索到的委托人总是为空,而注入的委托人却不是...... 好像在preSend方法中进入了4次。前2次, 主要预发送开始:不为空
最后两次为空 主要预发送开始:null