Pre-Authentication without Authorization using Spr

2019-04-09 10:07发布

问题:

My requirement is:

In my application, the authentication is performed by customized third party API for the first time when the user logs in via login screen(not by using Spring Security). Now, we got few enhancements in which the rest service calls are used. Per the requirement, before making any rest call from our application, we need to reauthenticate the user against the database. Since the user is already validated when he logged in using login screen and those details are available in the request, I am planning to use spring security for reauthenticating the user(Pre-Authentication scenario).We don't have any roles defined for the users in our application. So no need to worry about the roles. I have read the reference manual, but I didn't get much info on how to proceed further. One thing I understood is we need to tell the spring context somehow about the user after he is authenticated by customized third party API. Even I have googled a bit around, but could not get a good example that suits my requirement. It would be great if anybody can guide me on how to start with an example.

I just need to tell the spring context something like "hey..! this user is already authenticated so he can be permitted to call the rest services after the user's credentials are verified by the customized third party API.

I am not supposed to change the existing initial authentication process. I should only use the authenticated user info and use the spring security further to reauthenticate the user.

My issue is some what similar to the issue mentioned in the spring reference manual http://docs.spring.io/spring-security/site/docs/3.0.x/reference/preauth.html

Please don't answer with single line (except if it has a proper external link).It would be great if you can show me an example or pseudo code.

Thanks in advance.

回答1:

I'd suggest to make a "bridge" from Spring Security to the 3rd party login page. If you work with Spring, I think this is the best way to work.

Meaning, you have a login handler that will redirect the user to the 3rd party login page. After logging-in, the user will be redirected back to the web-app.

Is this what you mean? Does it sound good? Does it make sense?

If so, you may use my article to get some help:

<security:http entry-point-ref="legacyEntryPoint">

Generally, it means that whenever a relevant http call tries to access your app, this is the entry-point that handles the request. In your case, legacyEntryPoint is a class that you will implement that will check if the user is authenticated; if not, it redirects the user to the 3rd party login system, otherwise it uses the known "token" to use your app.

Hope that helps!



回答2:

Did you try this?

SecurityContextHolder.getContext().setAuthenticated(true);.

http://docs.spring.io/autorepo/docs/spring-security/3.0.x/apidocs/org/springframework/security/core/Authentication.html



回答3:

For first authentication, I hope you have access to call the 3rd party API with some input values like username/password and it returns true/false. If so you could write your own AuthenticationProvider as below. And invoke you 3rd party authentication as below. Spring security framework would automatically set SecurityContextHolder.getContext().setAuthenticated(true or false); accordingly. You need not have to set this.

        public class MyAuthenticationProvider implements AuthenticationProvider {

            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException {

                String user = (String) authentication.getPrincipal();
                String password = (String) authentication.getCredentials();

                List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
                authorities.add(new SimpleGrantedAuthority("ROLE_ONE"));
                authorities.add(new SimpleGrantedAuthority("ROLE_TWO"));

                UsernamePasswordAuthenticationToken authenticationToken = null;



                if (<your 3rd party authentication result == true>)) {
                    authenticationToken = new UsernamePasswordAuthenticationToken(user, password, authorities);
                } else {
                    throw new BadCredentialsException("Invalid credentials supplied. Please try again.");
                }
                return authenticationToken;
            }


            @Override
            public boolean supports(Class<?> authentication) {
                boolean supports = authentication.isAssignableFrom(UsernamePasswordAuthenticationToken.class);
                return supports;
            }

        }

Before making further REST API calls, in its Spring controller you can check if the user is valid or not using code SecurityContextHolder.getContext().getAuthentication().isAuthenticated();

You could also get lot of other User information with code like below.

                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                System.out.println("getAuthorities : " + authentication.getAuthorities());
                System.out.println("getName : " + authentication.getName());
                System.out.println("getCredentials : " + authentication.getCredentials());
                System.out.println("getDetails : " + authentication.getDetails());
                System.out.println("getPrincipal : " + authentication.getPrincipal());

                if (authentication.getPrincipal() instanceof User) {
                    User user = (User) authentication.getPrincipal();
                    System.out.println(user.getUsername());
                    System.out.println(user.getPassword());
                    System.out.println(user.getAuthorities());
                }


回答4:

I'm doing something very similar. I'm doing authentication for a stateless REST backend, so I want the user to authenticate once, then for each subsequent request, the authentication must be transparent. I'm using tokens for this. On login, the user-supplied credentials are used to authenticate and generate a token (although ultimately, we want to use an outside service for obtaining a token). The token is returned as a header. Then the angularjs frontend sends the token on each subsequent REST call. The backend checks the validity of the token and if it's good, then marks 'authenticated' to be true.

Here's my security-context.xml:

<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="
  http://www.springframework.org/schema/security
  http://www.springframework.org/schema/security/spring-security-3.2.xsd
  http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">

<http use-expressions="true" 
      entry-point-ref="restAuthenticationEntryPoint"
      create-session="stateless">
    <intercept-url pattern="/secured/extreme/**" access="hasRole('ROLE_SUPERVISOR')"/>
    <intercept-url pattern="/secured/**" access="isAuthenticated()" />
    <intercept-url pattern="/j_spring_security_check" requires-channel="https" access="permitAll"/>
    <intercept-url pattern="/logon.jsp" requires-channel="https" access="permitAll"/>
    <sec:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" />
</http>

<beans:bean id="restAuthenticationEntryPoint" class="com.company.project.authentication.security.RestAuthenticationEntryPoint" />

<beans:bean id="authenticationTokenProcessingFilter" class="com.company.project.authentication.security.AuthenticationTokenProcessingFilter" >
    <beans:property name="authenticationManager" ref="authenticationManager" />
    <beans:property name="userDetailsServices"> 
                <beans:list>
                    <beans:ref bean="inMemoryUserDetailsService" />
                    <beans:ref bean="tmpUserDetailsService" />
                </beans:list>
    </beans:property>

</beans:bean>

<beans:bean id="tmpUserDetailsService" class="com.company.project.authentication.security.TokenUserDetailsServiceImpl" />

<user-service id="inMemoryUserDetailsService">
            <user name="temporary" password="temporary" authorities="ROLE_SUPERVISOR" />
            <user name="user" password="userPass" authorities="ROLE_USER" />
</user-service>

<authentication-manager alias="authenticationManager">
    <!-- Use some hard-coded values for development -->
    <authentication-provider user-service-ref="inMemoryUserDetailsService" />
    <authentication-provider ref='companyLdapProvider' />
</authentication-manager>

For the authentication filter, I subclass UsernamePasswordAuthenticationFilter. When it's a login request, then authentication with the authentication provider happens, and then a token is generated. If a token is read from the header, then the token is examined for authentication. Here is my authentication filter (which is still not production-ready, but it works to give you an idea of what you can do):

public class AuthenticationTokenProcessingFilter extends UsernamePasswordAuthenticationFilter {
//~ Static fields/initializers =====================================================================================

private static final String HEADER_AUTH_TOKEN = "X-Auth-Token";    
private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationTokenProcessingFilter.class);

private List<UserDetailsService> userDetailsServices = new ArrayList<UserDetailsService>();
//~ Constructors ===================================================================================================

public AuthenticationTokenProcessingFilter() {
    super();
}

//~ Methods ========================================================================================================
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
                ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;

        String authToken = this.extractAuthTokenFromRequest(request);
        if (authToken == null) {
            super.doFilter(request,  res,  chain);
            return;
        }
        String userName = TokenUtils.getUserNameFromToken(authToken);

        if (userName != null) {

                UserDetails userDetails = loadUserByUsername(userName);

                if (TokenUtils.validateToken(authToken, userDetails)) {

                        UsernamePasswordAuthenticationToken authentication =
                                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                }
        }

        chain.doFilter(request, response);
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    if (!request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }

    UsernamePasswordAuthenticationToken authRequest = authenticateWithForm(request, response);
    // Allow subclasses to set the "details" property
    setDetails(request, authRequest);

    Authentication authentication = this.getAuthenticationManager().authenticate(authRequest);

    if (authentication.isAuthenticated()) {
        try {
            String authToken = TokenUtils.createToken(obtainUsername(request), obtainPassword(request));
            LOGGER.info("Setting HTTP header {} = {}", HEADER_AUTH_TOKEN, authToken);
            response.addHeader(HEADER_AUTH_TOKEN, authToken);
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();                
            LOGGER.info("authorities = {}", authorities);
            // Now we should make an in-memory table of the token and userdetails for later use 
        } catch(Exception e) {
            LOGGER.warn("Error creating token for authentication. Authorization token head cannot be created.", e);
        }

    }

    return authentication;
}

protected UsernamePasswordAuthenticationToken authenticateWithForm(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    String username = obtainUsername(request);
    String password = obtainPassword(request);

    if (username == null) {
        username = "";
    }

    if (password == null) {
        password = "";
    }

    username = username.trim();

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);

    return authRequest;
}

private String extractAuthTokenFromRequest(HttpServletRequest httpRequest) {
        /* Get token from header */
        String authToken = httpRequest.getHeader(HEADER_AUTH_TOKEN);

        /* If token not found get it from request parameter */
        if (authToken == null) {
                authToken = httpRequest.getParameter("token");
        }

        return authToken;
}

public List<UserDetailsService> getUserDetailsServices() {
    return userDetailsServices;
}

public void setUserDetailsService(UserDetailsService userDetailsService) {
    this.userDetailsServices.add(userDetailsService);
}

public void setUserDetailsServices(List<UserDetailsService> users) {
    if (users != null) {
        this.userDetailsServices.clear();
        this.userDetailsServices.addAll(users);
    }
}
private UserDetails loadUserByUsername(String username) {
    UserDetails user = null;
    List<Exception> exceptions = new ArrayList<Exception>();
    for (UserDetailsService service: userDetailsServices) {
        try {
            user = service.loadUserByUsername(username);
            break;
        } catch (Exception e) {
            LOGGER.warn("Could not load user by username {} with service {}", username, service.getClass().getName());
            LOGGER.info("Exception is: ",e);
            exceptions.add(e);
        }
    }
    if (user == null && !exceptions.isEmpty()) {
        throw new AuthenticationException(exceptions.get(0));
    }
    return user;
}
}

I'm still working on getting the UserDetailsService refined though. Usually, you can use the authentication provider to get the UserDetails, but since I have a stateless app, when I want to authenticate a token, I have to figure which UserDetailsService to use. I'm doing this with custom code for the moment.