Kerberos and Spnego should not be confused. Though Spnego is often used for Kerberos authentication, Spnego does not always mean Kerberos, or even a preference for Kerberos.
Spnego is a protocol that allows client and server to negotiate a mutually acceptable mech type (if available). 
That may or may not be Kerberos depending on the sub-mechanisms requested by the client and server during the negotiation process. 
The Negotiation process may take several handshake attempts.
Using human languages as an example. If I speak English, Latin and Zulu, in that order of preference, and you speak Eskimau and Zulu, then we will end up speaking Zulu.
In the setup that I am currently testing, with Internet Explorer as a client, and a custom Java Application Server using JAAS + GSS as the Server I observe similar behavour to that in your comment:
- Browser sends an unauthenticated request 
 
- Server replies with HTTP 401 Unauthorized, WWW-Authenticate: Negotiate header. 
 
- Browser either responds with Negotiate + NTLM token (bad!).
 
In my case the game does not end there, it continues as follows:
- Server replies with HTTP 401 Unauthorized, WWW-Authenticate: Negotiate + GSS response token
 
- Browser responds with Negotiate + Spnego NegoTokenTarg wrapping a Kerberos Token.
 
- Server unwraps the Kerberos Token; decodes, and authenticates the client; responds with HTTP 200, WWW-Authenticate: Negotiate + GSS response token
 
i.e. I don't prevent the browser sending an NTLM token, my Server just continues negotiation for another round until it gets a Kerberos Token.
As a side issue: the token provided by Internet Explorer 11 at step 3. above is not properly Spnego compliant, it is neither a NegTokenInit, nor a NetTokenTarg, and at 127 bytes long is clearly much too short to be or wrap a Kerberos token.
You are using Spring Security Kerberos, but in a comment you indicate an interest in other libraries, so below is my JGSS based Spnego authentication code. 
For brevity I leave out the JAAS setup, but all this takes place in a JAAS Subject.doAs() privileged context.
public static final String NEGOTIATE =    "Negotiate ";
public static final String AUTHORIZATION = "Authorization";
public static final String WWWAUTHENTICATE = "WWW-Authenticate";
public static final int HTTP_OK = 200;
public static final int HTTP_GOAWAY = 401; //Unauthorized
public static final String SPNEGOOID = "1.3.6.1.5.5.2";
public static final String KRB5OID = "1.2.840.113554.1.2.2";
public void spnegoAuthenticate(Request req, Response resp, Service http) {
    GSSContext gssContext = null;
    String kerberosUser = null; 
    String auth =req.headers("Authorization");
    if ( auth != null && auth.startsWith(NEGOTIATE )) {
        //smells like an SPNEGO request, so get the token from the http headers
        String authBody = auth.substring(NEGOTIATE.length());
        int offset =0;
        // As GSS cannot directly process Spnego NegTokenInit and NegTokenTarg, preprocess and extract native Kerberos token.
        authBody = preProcessToken(authBody);
        try {     
            byte gssapiData[] = Base64.getDecoder().decode(authBody);
            gssContext = initGSSContext(SPNEGOOID, KRB5OID);
            byte token[] = gssContext.acceptSecContext(gssapiData, offset, gssapiData.length);
            if (gssapiData.length > 128) {
                //extract the Kerberos User. The Execute/Login service will compare this with the user in the message body.
                kerberosUser = gssContext.getSrcName().toString();
                resp.status(HTTP_OK);
            } else {
                //Is too short to be a kerberos token (or to wrap one), so don't try and extract the user.
                //This could be a first pass from an SPNEGO enabled Web-browser. Maybe NTLM?
                resp.status(HTTP_GOAWAY);
            }
            String responseToken = Base64.getEncoder().encodeToString(token);
            if (responseToken != null && responseToken.length() > 0) {
                resp.header(WWWAUTHENTICATE, NEGOTIATE + responseToken);    
            }         
        } catch (GSSException e) {
            // Something went wrong fishing the token from the http headers
            http.halt(401, "Go Away! This is a privileged route, and you ain't privileged!"+"\r\n");    
        } finally {
            try {
                gssContext.dispose();
            } catch (GSSException e) {
                //error handling here
            }
        }
    } else {
        //This is either not a SPNEGO request, or is the first pass without token 
        resp.header(WWWAUTHENTICATE, NEGOTIATE.trim()); //set header to suggest negotiation
        http.halt(HTTP_GOAWAY, "Go Away! This is a privileged route, and you ain't privileged! Only come back when you are."+"\r\n");
    }
}
private String preProcessToken(String authBody) {
    String tag = getTokenType(authBody); 
    if (tag.equals("60")) {
        // is a standard "application constructed" token. Kerberos tokens seem to start with "YI.."
    } else if (tag.equals("A0")) {
        // is a Spnego NegTokenInit, starting with "oA.." to "oP.."
        authBody=extractKerberosToken(authBody);
    } else if (tag.equals("A1")) {
        // is a Spnego NegTokenTarg, starting with "oQ.." to "oZ.."
        authBody=extractKerberosToken(authBody);
    } else {
        // some other unexpected token.
        // TODO: generate error
    }
    return authBody;
}
private String extractKerberosToken(String authBody) {
    return authBody.substring(authBody.indexOf("YI", 2));
}
private String getTokenType(String authBody) {
    return String.format("%02X",    Base64.getDecoder().decode(authBody.substring(0,2))[0]);
}
Note this code is presented "as-is", as an example. It is work-in-progress and has a number of flaws:
1) getTokenType() uses the decoded token, but extractKerberosToken works on the encoded token, both should use byte operations on the decoded token.
2) Token rejection based on length is a little too simple. I plan to add better NTLM token identification....
3) I don't have a true GSS context loop. If I don't like what the client presents, I reject and close the context. 
For any following handshake attempts from the client I open a new GSS context.