带有JWT令牌的Spring安全性和Websocket
我有一个SpringBoot项目(2.0.0.RELEASE)。我使用基于JWT令牌的身份验证和授权来保护REST。我还想将Websocket与SockJS结合使用,并使用令牌进行套接字身份验证。我试图实现,但是我没有成功 首先,我的安全配置如下带有JWT令牌的Spring安全性和Websocket,spring,spring-boot,spring-security,websocket,spring-websocket,Spring,Spring Boot,Spring Security,Websocket,Spring Websocket,我有一个SpringBoot项目(2.0.0.RELEASE)。我使用基于JWT令牌的身份验证和授权来保护REST。我还想将Websocket与SockJS结合使用,并使用令牌进行套接字身份验证。我试图实现,但是我没有成功 首先,我的安全配置如下 @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSe
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final TokenAuthenticationService tokenAuthenticationService;
private final ObjectMapper mapper;
@Autowired
protected SecurityConfig(final TokenAuthenticationService tokenAuthenticationService, ObjectMapper mapper) {
super();
this.tokenAuthenticationService = tokenAuthenticationService;
this.mapper = mapper;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers()
.frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/api/v3/auth").permitAll()
.antMatchers("/api/v3/signup").permitAll()
.antMatchers("/api/v3/websocket/**").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(new RestAuthenticationEntryPoint(mapper))
.and()
.addFilterBefore(new AuthenticationTokenFilter(tokenAuthenticationService), UsernamePasswordAuthenticationFilter.class)
.cors()
.and()
.csrf().disable();
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("GET");
config.addAllowedMethod("POST");
config.addAllowedMethod("PUT");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
在应用程序中,要获取令牌,应该向/api/v3/auth
发出POST请求/api/v3/websocket
是websocket端点。下面是我的Websocket配置类
@Configuration
@EnableWebSocketMessageBroker
@Order(Ordered.HIGHEST_PRECEDENCE + 50)
public class SocketBrokerConfig implements WebSocketMessageBrokerConfigurer {
private static final Logger log = LoggerFactory.getLogger(SocketBrokerConfig.class);
private final TokenAuthenticationService authenticationService;
@Autowired
public SocketBrokerConfig(TokenAuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
log.info("registering websockets");
registry
.addEndpoint("/api/v3/websocket")
.setAllowedOrigins("*")
.withSockJS()
.setClientLibraryUrl("https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js")
.setWebSocketEnabled(false)
.setSessionCookieNeeded(false);
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptorAdapter() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
log.info("in override " + accessor.getCommand());
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// String name = auth.getName(); //get logged in username
// System.out.println("Authenticated User : " + name);
String authToken = accessor.getFirstNativeHeader("x-auth-token");
log.info("Header auth token: " + authToken);
Principal principal = authenticationService.getUserFromToken(authToken);
if (Objects.isNull(principal))
return null;
accessor.setUser(principal);
} else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (Objects.nonNull(authentication))
log.info("Disconnected Auth : " + authentication.getName());
else
log.info("Disconnected Sess : " + accessor.getSessionId());
}
return message;
}
@Override
public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(message);
// ignore non-STOMP messages like heartbeat messages
if (sha.getCommand() == null) {
log.warn("postSend null command");
return;
}
String sessionId = sha.getSessionId();
switch (sha.getCommand()) {
case CONNECT:
log.info("STOMP Connect [sessionId: " + sessionId + "]");
break;
case CONNECTED:
log.info("STOMP Connected [sessionId: " + sessionId + "]");
break;
case DISCONNECT:
log.info("STOMP Disconnect [sessionId: " + sessionId + "]");
break;
default:
break;
}
}
});
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Test</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"/>
</head>
<body>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/test/ws">Test Client</a>
</div>
</div>
</nav>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script>
<script type="application/javascript">
var endpoint = "http://192.168.0.58:8080/api/v3/auth";
var login = {username: "admin", password: "password"};
$.ajax({
type: "POST",
url: endpoint,
data: JSON.stringify(login),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
success: function (data) {
var socket = new SockJS("http://192.168.0.58:8080/api/v3/websocket/");
var stompClient = Stomp.over(socket);
var headers = {
'client-id': 'my-client-id',
'x-auth-token': data.data.token
};
stompClient.connect(headers, function (frame) {
console.log("Connected ?!");
console.log(frame);
stompClient.subscribe(
"/user/queue/admin",
function (message) {
console.log("Message arrived");
console.log(message);
}
);
});
}
});
</script>
</body>
</html>
最后,下面是我的测试JS文件
@Configuration
@EnableWebSocketMessageBroker
@Order(Ordered.HIGHEST_PRECEDENCE + 50)
public class SocketBrokerConfig implements WebSocketMessageBrokerConfigurer {
private static final Logger log = LoggerFactory.getLogger(SocketBrokerConfig.class);
private final TokenAuthenticationService authenticationService;
@Autowired
public SocketBrokerConfig(TokenAuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
log.info("registering websockets");
registry
.addEndpoint("/api/v3/websocket")
.setAllowedOrigins("*")
.withSockJS()
.setClientLibraryUrl("https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js")
.setWebSocketEnabled(false)
.setSessionCookieNeeded(false);
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptorAdapter() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
log.info("in override " + accessor.getCommand());
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// String name = auth.getName(); //get logged in username
// System.out.println("Authenticated User : " + name);
String authToken = accessor.getFirstNativeHeader("x-auth-token");
log.info("Header auth token: " + authToken);
Principal principal = authenticationService.getUserFromToken(authToken);
if (Objects.isNull(principal))
return null;
accessor.setUser(principal);
} else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (Objects.nonNull(authentication))
log.info("Disconnected Auth : " + authentication.getName());
else
log.info("Disconnected Sess : " + accessor.getSessionId());
}
return message;
}
@Override
public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(message);
// ignore non-STOMP messages like heartbeat messages
if (sha.getCommand() == null) {
log.warn("postSend null command");
return;
}
String sessionId = sha.getSessionId();
switch (sha.getCommand()) {
case CONNECT:
log.info("STOMP Connect [sessionId: " + sessionId + "]");
break;
case CONNECTED:
log.info("STOMP Connected [sessionId: " + sessionId + "]");
break;
case DISCONNECT:
log.info("STOMP Disconnect [sessionId: " + sessionId + "]");
break;
default:
break;
}
}
});
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Test</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"/>
</head>
<body>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/test/ws">Test Client</a>
</div>
</div>
</nav>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script>
<script type="application/javascript">
var endpoint = "http://192.168.0.58:8080/api/v3/auth";
var login = {username: "admin", password: "password"};
$.ajax({
type: "POST",
url: endpoint,
data: JSON.stringify(login),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
success: function (data) {
var socket = new SockJS("http://192.168.0.58:8080/api/v3/websocket/");
var stompClient = Stomp.over(socket);
var headers = {
'client-id': 'my-client-id',
'x-auth-token': data.data.token
};
stompClient.connect(headers, function (frame) {
console.log("Connected ?!");
console.log(frame);
stompClient.subscribe(
"/user/queue/admin",
function (message) {
console.log("Message arrived");
console.log(message);
}
);
});
}
});
</script>
</body>
</html>
我在互联网上尝试了很多不同的代码,但都没有成功
我的问题是,为什么我的客户机的CONNECT命令落入被覆盖的拦截器,而DISCONNECT命令落入
**编辑1**
我在下面添加我的TokenAuthenticationService实现。我注意到,若我在authenticate方法中放置一个调试点,然后等待几秒钟,然后继续,websocket客户机connect和connect命令将落入preSend方法
当我停止验证方法几秒钟时会发生什么?为什么
@Service
public class JsonWebTokenAuthenticationService implements TokenAuthenticationService {
private static final Logger log = LoggerFactory.getLogger(JsonWebTokenAuthenticationService.class);
@Value("security.token.secret.key")
private String secretKey;
private final UserRepository userRepository;
@Autowired
public JsonWebTokenAuthenticationService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public Authentication authenticate(HttpServletRequest request) {
final String token = request.getHeader("x-auth-token");
final Jws<Claims> tokenData = parseToken(token);
if (Objects.nonNull(tokenData)) {
User user = getUserFromToken(tokenData);
if (Objects.nonNull(user) && user.isEnabled()) {
return new UserAuthentication(user);
}
}
return null;
}
public Authentication getUserFromToken(String token) {
if (Objects.isNull(token))
return null;
final Jws<Claims> tokenData = parseToken(token);
if (Objects.nonNull(tokenData)) {
User user = getUserFromToken(tokenData);
if (Objects.nonNull(user) && user.isEnabled()) {
return new UserAuthentication(user);
}
}
return null;
}
private Jws<Claims> parseToken(final String token) {
if (Objects.nonNull(token)) {
try {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException
| SignatureException | IllegalArgumentException e) {
log.warn("Token parse failed", e);
return null;
}
}
return null;
}
private User getUserFromToken(final Jws<Claims> tokenData) {
try {
return userRepository.findByUsername(tokenData.getBody().get("username").toString());
} catch (UsernameNotFoundException e) {
log.warn("No user", e);
}
return null;
}
}
@服务
公共类JsonWebTokenAuthenticationService实现TokenAuthenticationService{
私有静态最终记录器log=LoggerFactory.getLogger(JsonWebTokenAuthenticationService.class);
@值(“security.token.secret.key”)
私钥;
私有最终用户存储库用户存储库;
@自动连线
公共JsonWebTokenAuthenticationService(UserRepository UserRepository){
this.userRepository=userRepository;
}
@凌驾
公共身份验证(HttpServletRequest){
最终字符串标记=request.getHeader(“x-auth-token”);
最终Jws-tokenData=parseToken(令牌);
if(Objects.nonNull(tokenData)){
用户用户=getUserFromToken(tokenData);
if(Objects.nonNull(user)&&user.isEnabled()){
返回新的UserAuthentication(用户);
}
}
返回null;
}
公共身份验证getUserFromToken(字符串令牌){
if(Objects.isNull(令牌))
返回null;
最终Jws-tokenData=parseToken(令牌);
if(Objects.nonNull(tokenData)){
用户用户=getUserFromToken(tokenData);
if(Objects.nonNull(user)&&user.isEnabled()){
返回新的UserAuthentication(用户);
}
}
返回null;
}
专用Jws parseToken(最终字符串标记){
if(Objects.nonNull(令牌)){
试一试{
返回Jwts.parser();
}捕获(过期JWTException |不支持JWTException |格式错误JWTException)
|签名例外|非法辩论例外e){
log.warn(“令牌解析失败”,e);
返回null;
}
}
返回null;
}
私有用户getUserFromToken(最终Jws tokenData){
试一试{
返回userRepository.findByUsername(tokenData.getBody().get(“username”).toString());
}catch(UsernameNotFounde异常){
日志警告(“无用户”,e);
}
返回null;
}
}
**编辑2**
我在
AuthenticationTokenFilter#doFilter
方法中添加了Thread.sleep(2000)
(我不应该这么做),JS客户端现在连接、订阅等。STOMP的CONNECT命令属于ChannelInterceptorAdapter#preSend
方法。我觉得有点奇怪 在编辑2之后,我面对着。结果发现所有的问题都是由路由器引起的。我仍然不明白发生了什么,但是它现在像预期的那样工作