如何在Java EE中保护WebSocket端点?

3
我遵循了这篇关于使用Java EE设置websocket端点的教程:https://technology.amis.nl/2013/06/22/java-ee-7-ejb-publishing-cdi-events-that-are-pushed-over-websocket-to-browser-client/

由于明显的原因,在安全方面还要做更多工作(例如没有SSL和访问限制/身份验证)。

所以我的目标是通过以下方式来改进websocket安全性:

  • 使用SSL(wss://而不是ws://)-完成
  • 设置用户身份验证(web.xml)-完成
  • 强制执行SSL通信(web.xml)-完成
  • 用令牌(有限生命周期)保护websocket连接

我的问题:我如何在ServerEndpoint中验证我在LoginBean中创建的令牌?

附加问题:我是否遗漏了在Java EE中保护websocket的重要部分?

这是我到目前为止的内容:

ServerEndpoint

import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/user/endpoint/{token}")
public class ThisIsTheSecuredEndpoint {

    @OnOpen
    public void onOpen(@PathParam("token") String incomingToken, 
    Session session) throws IOException {

        //How can i check if the token is valid?

    }      
}

LoginBean

@ManagedBean
@SessionScoped
public class LoginBean {

public String login() {

    FacesContext facesContext = FacesContext.getCurrentInstance();
    HttpServletRequest request = (HttpServletRequest)facesContext.getExternalContext().getRequest();

    try {
        request.login("userID", "password");

        HttpSession session = request.getSession();

        // here we put the token in the session
        session.setAttribute("token", "someVeeeeryLongRandomValue123hfgrtwpqllkiw");


    } catch (ServletException e) {
        facesContext.addMessage(null, new FacesMessage("Login failed."));
        return "error";
    }

    return "home";
}

}

Javascipt

这是我想使用的连接到 WebSocket 的代码:

// use SSL 
// retrive the token from session via EL-expression #{session.getAttribute("token")}
var wsUri = "wss://someHost.com/user/endpoint/#{session.getAttribute("token")}";
var websocket = new WebSocket(wsUri);

websocket.onerror = function(evt) { onError(evt) };

function onError(evt) {
    writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
}

// For testing purposes
var output = document.getElementById("output");
websocket.onopen = function(evt) { onOpen(evt) };

function writeToScreen(message) {
    output.innerHTML += message + "<br>";
}

function onOpen() {
    writeToScreen("Connected to " + wsUri);
}

web-xml:

使用登录验证和SSL加密通信来保护“/user/*”目录

<security-constraint>
    ...
    <web-resource-name>Secured Area</web-resource-name>
    <url-pattern>pathToSecuredDicrtoy</url-pattern>       
     ...       
    <user-data-constraint>
        <transport-guarantee>CONFIDENTIAL</transport-guarantee>
    </user-data-constraint>
    ...
</security-constraint>
<login-config>
    <auth-method>FORM</auth-method> ...           
</login-config>

注意: 我正在使用JSF

非常感谢您的任何反馈。


为了使JavaScript EL评估工作,我在web.xml中添加了以下内容: jsp *.js 对于我的用例来说,这是可以的,因为该页面的访问率非常低。 - Tobi Tiggers
1个回答

9

您可以使用Servlet Filter来进行身份验证。

以下是我一段时间前创建的保护聊天端点的示例过滤器。它从名为access-token的查询参数中提取访问令牌,并将令牌验证委托给名为Authenticator的bean。

您可以根据自己的需要轻松地对其进行调整:

/**
 * Access token filter for the chat websocket. Requests without a valid access token 
 * are refused with a <code>403</code>.
 *
 * @author cassiomolin
 */
@WebFilter("/chat/*")
public class AccessTokenFilter implements Filter {

    @Inject
    private Authenticator authenticator;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, 
            FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        // Extract access token from the request
        String token = request.getParameter("access-token");
        if (token == null || token.trim().isEmpty()) {
            returnForbiddenError(response, "An access token is required to connect");
            return;
        }

        // Validate the token and get the user who the token has been issued for
        Optional<String> optionalUsername = authenticator.getUsernameFromToken(token);
        if (optionalUsername.isPresent()) {
            filterChain.doFilter(
                    new AuthenticatedRequest(
                            request, optionalUsername.get()), servletResponse);
        } else {
            returnForbiddenError(response, "Invalid access token");
        }
    }

    private void returnForbiddenError(HttpServletResponse response, String message) 
            throws IOException {
        response.sendError(HttpServletResponse.SC_FORBIDDEN, message);
    }

    @Override
    public void destroy() {

    }

    /**
     * Wrapper for a {@link HttpServletRequest} which decorates a 
     * {@link HttpServletRequest} by adding a {@link Principal} to it.
     *
     * @author cassiomolin
     */
    private static class AuthenticatedRequest extends HttpServletRequestWrapper {

        private String username;

        public AuthenticatedRequest(HttpServletRequest request, String username) {
            super(request);
            this.username = username;
        }

        @Override
        public Principal getUserPrincipal() {
            return () -> username;
        }
    }
}

聊天端点的格式大致如下:
@ServerEndpoint("/chat")
public class ChatEndpoint {

    private static final Set<Session> sessions = 
            Collections.synchronizedSet(new HashSet<>());

    @OnOpen
    public void onOpen(Session session) {
        sessions.add(session);
        String username = session.getUserPrincipal().getName();
        welcomeUser(session, username);
    }

    ...

}

该应用程序可以在这里获取。


1
非常感谢您的回答。这正是我正在寻找的东西 :-). - Tobi Tiggers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接