ChatGPT解决这个技术问题 Extra ChatGPT

RESTful Authentication via Spring

Problem: We have a Spring MVC-based RESTful API which contains sensitive information. The API should be secured, however sending the user's credentials (user/pass combo) with each request is not desirable. Per REST guidelines (and internal business requirements), the server must remain stateless. The API will be consumed by another server in a mashup-style approach.

Requirements:

Client makes a request to .../authenticate (unprotected URL) with credentials; server returns a secure token which contains enough information for the server to validate future requests and remain stateless. This would likely consist of the same information as Spring Security's Remember-Me Token.

Client makes subsequent requests to various (protected) URLs, appending the previously obtained token as a query parameter (or, less desirably, an HTTP request header).

Client cannot be expected to store cookies.

Since we use Spring already, the solution should make use of Spring Security.

We've been banging our heads against the wall trying to make this work, so hopefully someone out there has already solved this problem.

Given the above scenario, how might you solve this particular need?

Hi Chris, I'm not sure passing that token in the query parameter is the best idea. That will show up in logs, regardless of HTTPS or HTTP. The headers are probably safer. Just FYI. Great question though. +1
What is your understanding of stateless? Your token requirement collides with my understanding of stateless. The Http authentication answer seems to me the only stateless implementation.
@MarkusMalkusch stateless refers to the server's knowledge of prior communications with a given client. HTTP is stateless by definition, and session cookies make it stateful. The lifetime (and source, for that matter) of the token are irrelevant; the server only cares that it's valid and can be tied back to a user (NOT a session). Passing an identifying token, therefore, does not interfere with statefulness.
@ChrisCashwell How do you ensure that the token is not being spoofed/generated by the client? Do you use a private key on the server-side to encrypt the token, provide it to the client, and then use the same key to decrypt it during future requests? Obviously Base64 or some other obfuscation would not be enough. Can you elaborate on techniques for the "validation" of these tokens?
Although this is dated and I haven't touched or updated the code in over 2 years, I have created a Gist to further expand on these concepts. gist.github.com/ccashwell/dfc05dd8bd1a75d189d1

N
Nimantha

We managed to get this working exactly as described in the OP, and hopefully someone else can make use of the solution. Here's what we did:

Set up the security context like so:

<security:http realm="Protected API" use-expressions="true" auto-config="false" create-session="stateless" entry-point-ref="CustomAuthenticationEntryPoint">
    <security:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" />
    <security:intercept-url pattern="/authenticate" access="permitAll"/>
    <security:intercept-url pattern="/**" access="isAuthenticated()" />
</security:http>

<bean id="CustomAuthenticationEntryPoint"
    class="com.demo.api.support.spring.CustomAuthenticationEntryPoint" />

<bean id="authenticationTokenProcessingFilter"
    class="com.demo.api.support.spring.AuthenticationTokenProcessingFilter" >
    <constructor-arg ref="authenticationManager" />
</bean>

As you can see, we've created a custom AuthenticationEntryPoint, which basically just returns a 401 Unauthorized if the request wasn't authenticated in the filter chain by our AuthenticationTokenProcessingFilter.

CustomAuthenticationEntryPoint:

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Authentication token was either missing or invalid." );
    }
}

AuthenticationTokenProcessingFilter:

public class AuthenticationTokenProcessingFilter extends GenericFilterBean {

    @Autowired UserService userService;
    @Autowired TokenUtils tokenUtils;
    AuthenticationManager authManager;
    
    public AuthenticationTokenProcessingFilter(AuthenticationManager authManager) {
        this.authManager = authManager;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        @SuppressWarnings("unchecked")
        Map<String, String[]> parms = request.getParameterMap();

        if(parms.containsKey("token")) {
            String token = parms.get("token")[0]; // grab the first "token" parameter
            
            // validate the token
            if (tokenUtils.validate(token)) {
                // determine the user based on the (already validated) token
                UserDetails userDetails = tokenUtils.getUserFromToken(token);
                // build an Authentication object with the user's info
                UsernamePasswordAuthenticationToken authentication = 
                        new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails((HttpServletRequest) request));
                // set the authentication into the SecurityContext
                SecurityContextHolder.getContext().setAuthentication(authManager.authenticate(authentication));         
            }
        }
        // continue thru the filter chain
        chain.doFilter(request, response);
    }
}

Obviously, TokenUtils contains some privy (and very case-specific) code and can't be readily shared. Here's its interface:

public interface TokenUtils {
    String getToken(UserDetails userDetails);
    String getToken(UserDetails userDetails, Long expiration);
    boolean validate(String token);
    UserDetails getUserFromToken(String token);
}

That ought to get you off to a good start.


Is it necessary to authenticate the token when the token is sending with the request. How about get the username info directly and set in the current context/request?
@Spring I don't store them anywhere... the whole idea of the token is that it needs to be passed with every request, and it can be deconstructed (partially) to determine its validity (hence the validate(...) method). This is important because I want the server to remain stateless. I would imagine you could use this approach without needing to use Spring.
If the client is a browser, how can the token be stored? or do you have to redo authentication for each request?
great tips. @ChrisCashwell - the part that I can't find is where do you validate the user credentials and send back a token ? I would guess it should be somewhere in the impl of the /authenticate end point. am I right ? If not what is the goal of /authenticate ?
what is inside the AuthenticationManager ?
C
Community

You might consider Digest Access Authentication. Essentially the protocol is as follows:

Request is made from client Server responds with a unique nonce string Client supplies a username and password (and some other values) md5 hashed with the nonce; this hash is known as HA1 Server is then able to verify client's identity and serve up the requested materials Communication with the nonce can continue until the server supplies a new nonce (a counter is used to eliminate replay attacks)

All of this communication is made through headers, which, as jmort253 points out, is generally more secure than communicating sensitive material in the url parameters.

Digest Access Authentication is supported by Spring Security. Notice that, although the docs say that you must have access to your client's plain-text password, you can successfully authenticate if you have the HA1 hash for your client.


While this is a possible approach, the several round trips that must be made in order to retrieve a token makes it a little undesirable.
If your client follows the HTTP Authentication specification those round trips happen only upon the first call and when 5. happens.
L
Leif John

Regarding tokens carrying information, JSON Web Tokens (http://jwt.io) is a brilliant technology. The main concept is to embed information elements (claims) into the token, and then signing the whole token so that the validating end can verify that the claims are indeed trustworthy.

I use this Java implementation: https://bitbucket.org/b_c/jose4j/wiki/Home

There is also a Spring module (spring-security-jwt), but I haven't looked into what it supports.


C
Community

Why don't you start using OAuth with JSON WebTokens

http://projects.spring.io/spring-security-oauth/

OAuth2 is an standardized authorization protocol/framework. As per Official OAuth2 Specification:

You can find more info here


The Spring Security OAuth project is deprecated. The latest OAuth 2.0 support is provided by Spring Security. See the OAuth 2.0 Migration Guide for further details.