I'm trying to integrate two simple Spring Web Applications with Spring Integration, and I want to propagate the Spring SecurityContext between the two applications, but I've not yet found a working solution.
The two applications are very similar between each other, there are only a few differences in the configuration: one of them has to be the "caller", the other one has to be the "receiver".
In order to getting the expected result, I'm using Spring Security and I've configured both applications with the following Spring Security configuration:
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.1.xsd">
<http auto-config="true">
<intercept-url pattern="/" access="permitAll" />
<intercept-url pattern="/service/**" access="permitAll" />
<intercept-url pattern="/home" access="permitAll" />
<intercept-url pattern="/admin**" access="hasRole('ADMIN')" />
<intercept-url pattern="/dba**" access="hasRole('ADMIN') and hasRole('DBA')" />
<form-login authentication-failure-url="/accessDenied" />
</http>
<authentication-manager id="authenticationManager">
<authentication-provider>
<user-service>
<user name="user" password="user" authorities="ROLE_USER" />
<user name="admin" password="admin" authorities="ROLE_ADMIN" />
<user name="dba" password="dba" authorities="ROLE_ADMIN,ROLE_DBA" />
</user-service>
</authentication-provider>
</authentication-manager>
<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
<beans:constructor-arg>
<beans:list>
<beans:bean class="org.springframework.security.access.vote.RoleVoter" />
<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
</beans:list>
</beans:constructor-arg>
</beans:bean>
</beans:beans>
This is working: if I go to the url http://localhost:8080/caller/login, the login process is correctly managed by the Spring Security layer.
So I'm trying to configure the Spring Integration module in order to "integrate" the two applications; my idea (wrong?) is to use an http:outbound-gateway from the "caller", that invoke the "receiver" at a specific URL.
On the other side, there is an http:inbound-gateway that manage the requests.
Here is the configuration of the "caller" side:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-http="http://www.springframework.org/schema/integration/http"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:int-security="http://www.springframework.org/schema/integration/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration-4.3.xsd
http://www.springframework.org/schema/integration/http http://www.springframework.org/schema/integration/http/spring-integration-http-4.3.xsd
http://www.springframework.org/schema/integration/security http://www.springframework.org/schema/integration/security/spring-integration-security-4.2.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.3.xsd">
<int:channel id="requestChannel">
<int:dispatcher task-executor="executor"/>
</int:channel>
<int:channel-interceptor ref="requestChannelInterceptor" pattern="requestChannel">
</int:channel-interceptor>
<task:executor id="executor" pool-size="5"/>
<bean id="requestChannelInterceptor" class="org.springframework.integration.security.channel.SecurityContextPropagationChannelInterceptor"></bean>
<bean id="requestChannelBean" class="test.spring.webapp.client.MessagingChannel">
<property name="requestChannel" ref="requestChannel"></property>
</bean>
<int-http:outbound-gateway id="gateway" request-channel="requestChannel"
encode-uri="true" url="http://localhost:8080/receiver/service/{request}"
http-method="GET" >
<int-http:uri-variable name="request" expression="payload"/>
</int-http:outbound-gateway>
<int-security:secured-channels access-decision-manager="accessDecisionManager">
<int-security:access-policy pattern="requestChannel" send-access="ROLE_ADMIN,ROLE_USER,ROLE_DBA,ROLE_ANONYMOUS"/>
</int-security:secured-channels>
</beans>
Here is the configuration of the "receiver" side instead:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-http="http://www.springframework.org/schema/integration/http"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:int-security="http://www.springframework.org/schema/integration/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration-4.3.xsd
http://www.springframework.org/schema/integration/http http://www.springframework.org/schema/integration/http/spring-integration-http-4.3.xsd
http://www.springframework.org/schema/integration/security http://www.springframework.org/schema/integration/security/spring-integration-security-4.2.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.3.xsd">
<int:channel id="requestChannel">
<int:dispatcher task-executor="executor"/>
</int:channel>
<int:channel-interceptor ref="requestChannelInterceptor" pattern="requestChannel">
</int:channel-interceptor>
<task:executor id="executor" pool-size="5"/>
<bean id="requestChannelInterceptor" class="org.springframework.integration.security.channel.SecurityContextPropagationChannelInterceptor"></bean>
<int-http:inbound-gateway id="gateway" request-channel="requestChannel"
path="/service/**"
supported-methods="GET">
</int-http:inbound-gateway>
<int-security:secured-channels access-decision-manager="accessDecisionManager">
<int-security:access-policy pattern="requestChannel" send-access="ROLE_ADMIN,ROLE_USER,ROLE_DBA,ROLE_ANONYMOUS"/>
</int-security:secured-channels>
<bean id="channelService"
class="test.spring.webapp.server.ChannelService"/>
<int:service-activator id="channelServiceActivator"
ref="channelService"
input-channel="requestChannel"
method="manage"/>
</beans>
As you can see, I'm using the component SecurityContextPropagationChannelInterceptor of Spring Integration, that is intended to make the "propagation" of the security context.
I'm also using the int-security:secured-channels in the configuration, as illustrated in the documentation.
But this configuration doesn't work: when I call the URL http://localhost:8080/caller/service/hello (logged or not logged is the same), I'm getting the following Exception:
org.springframework.messaging.MessageHandlingException: HTTP request execution failed for URI [http://localhost:8080/receiver/service/hello];
nested exception is org.springframework.web.client.HttpServerErrorException: 500 Internal Server Error
Furthermore, the class org.springframework.integration.channel.interceptor.ThreadStatePropagationChannelInterceptor has the following method:
#Override
#SuppressWarnings("unchecked")
public final Message<?> postReceive(Message<?> message, MessageChannel channel) {
if (message instanceof MessageWithThreadState) {
MessageWithThreadState<S> messageWithThreadState = (MessageWithThreadState<S>) message;
Message<?> messageToHandle = messageWithThreadState.message;
populatePropagatedContext(messageWithThreadState.state, messageToHandle, channel);
return messageToHandle;
}
return message;
}
The message instance contains the logged Principal.
Debugging this method, I noticed that the logged Principal (for example "admin") is obtained only on the "caller" side, not in the "receiver" side (where there is always the "anonymousUser"): why the propagation of the SecurityContext is not working?
I believe that something is totally wrong or missing in my configuration...
It can't be propagated to the another process (application) by its premise.
See the implementation of SecurityContextPropagationChannelInterceptor. It is based on ThreadStatePropagationChannelInterceptor. One more time Thread State, not one process to another via network.
Since there is no one common solution for inter-process SecurityContext transfer, there is no anything as out-of-the-box.
As I answered you in another your question (Spring Integration: the SecurityContext propagation), you should transfer credential via HTTP only as headers. I even think that much better would be to do that using standard Apache HTTPClient CredentialProvider (or BasicAuthorizationInterceptor from Spring Framework 4.3.1) approach to send request with the Basic Authentication header. Really to achieve security via network with Base64 encoding for credentials.
On the receiver side your should do something similar what is in the https://github.com/spring-projects/spring-security/blob/master/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java#L224:
byte[] base64Token = header.substring(6).getBytes("UTF-8");
byte[] decoded;
try {
decoded = Base64.decode(base64Token);
}
catch (IllegalArgumentException e) {
throw new BadCredentialsException(
"Failed to decode basic authentication token");
}
String token = new String(decoded, getCredentialsCharset(request));
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
return new String[] { token.substring(0, delim), token.substring(delim + 1) };
Related
I am new into Spring REST, what I am trying to do is creating REST endpoints for a project. Also I am implementing JWT Spring Security into the service as per project requirement.
The first REST endpoint is /LOGIN, after user credentials are verified through this service a token is assigned to the client in the header. This token will hold the session for any further REST call authentication. This service is doing its work as expected.
The next REST service to be called is GET_CURRENT_USER, which does the work of validating the token and then further jobs. I am using the token in postman to call the GET_CURRENT_USER service, the service code works just fine and I am returning code 200 and the expected JSON through my service.
But at postman I am getting a 404 not found error. What I have tried:
Removed CORS filters
Added index.jsp to project, I get the index page in postman and not the 404 (but that doesn't help).
Tracing further calls after GET_CURRENT_USER, but that leads so many further calls and can't trace what exactly overrides my response with 404.
Tried to play with GET and POST methods in code and POSTMAN but no luck. I am using all the headers required and other stuff.
Can't figure out where could the problem located. I cannot share code, but can answer all questions related.
Any help will be appreciated.
The 404 error went away when I provided default index.jsp, which was missing in my project. But it didn't solved my problem as I needed Rest response(JSON) in result. What I learnt from long code-debugging of DispatcheServlet and Filter chains etc.. that in case we need JSON response after rest call, and we don't intend to send any view-response(like jsps), we should implement AuthenticationSuccessHandler and return void.
Initially my spring-secuyrity.xml was as follows:
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-4.0.xsd">
<global-method-security pre-post-annotations="enabled" />
<http pattern="/abc/login" security="none"/>
<http pattern="/abc/**" entry-point-ref="jwtAuthenticationEntryPoint" create-session="stateless">
<csrf disabled="true"/>
<custom-filter before="FORM_LOGIN_FILTER" ref="jwtAuthorizationTokenFilter"/>
</http>
<beans:bean id="jwtAuthorizationTokenFilter" class="com.abc.xyz.controller.JwtAuthorizationTokenFilter">
<beans:property name="authenticationManager" ref="authenticationManager" />
</beans:bean>
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="<MyUserDetailsService>">
<password-encoder ref="encoder" />
</authentication-provider>
</authentication-manager>
<beans:bean id="encoder"/>
<class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
<beans:constructor-arg name="strength" value="10" />
</beans:bean>
</beans:beans>
I changed it to as follows:
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-4.0.xsd">
<global-method-security pre-post-annotations="enabled" />
<http pattern="/abc/login" security="none"/>
<http pattern="/abc/**" entry-point-ref="jwtAuthenticationEntryPoint" create-session="stateless">
<csrf disabled="true"/>
<custom-filter before="FORM_LOGIN_FILTER" ref="jwtAuthorizationTokenFilter"/>
</http>
<beans:bean id="jwtAuthorizationTokenFilter" class="com.abc.xyz.controller.JwtAuthorizationTokenFilter">
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="authenticationSuccessHandler" ref="authSuccessHandler" />
</beans:bean>
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="<MyUserDetailsService>">
<password-encoder ref="encoder" />
</authentication-provider>
</authentication-manager>
<beans:bean id="encoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
<beans:constructor-arg name="strength" value="10" />
</beans:bean>
<!-- Auth Success handler -->
<beans:bean id="authSuccessHandler" class="com.abc.xyz.controller.JwtAuthenticationSuccessHandler" />
</beans:beans>
JWT:
#Component
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// Not to do anything
}
}
In the above code where we are setting 2 bean properties, that need not required to be in the JwtAuthorizationTokenFilter
The JwtAuthorizationTokenFilter extends to AbstractAuthenticationProcessingFilter
and I believe with reflection it sets authenticationManager, successHandler of the AbstractAuthenticationProcessingFilter
These are the following 2 methods I see in the AbstractAuthenticationProcessingFilter
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
public void setAuthenticationSuccessHandler(
AuthenticationSuccessHandler successHandler) {
Assert.notNull(successHandler, "successHandler cannot be null");
this.successHandler = successHandler;
}
My application is having embedded jetty integrated with spring security for authentication and authorization. Authentication and authorization works fine for REST api calls to the application, but method authorization using #PreAuthorize is not working.
My spring-security.xml file has <global-method-security pre-post-annotations="enabled" />
After some search I found that it is recommended to move global-method-security to servlet-context.xml under application context, but I cannot do it because we are not using "org.springframework.web.servlet.DispatcherServlet". We are already using "org.glassfish.jersey.servlet.ServletContainer" as dispatcher.
spring-security.xml
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<global-method-security pre-post-annotations="enabled" />
<http realm="Protected API" use-expressions="true"
create-session="stateless"
entry-point-ref="unauthorizedEntryPoint"
authentication-manager-ref="edbAuthenticationManager">
<custom-filter ref="edbAuthenticationFilter" position="FORM_LOGIN_FILTER"/>
<intercept-url pattern="/api/*/login" access="authenticated"/>
<intercept-url pattern="/api/*/clusters/*/datasupport/**" access="hasRole('data-scientist')"/>
<intercept-url pattern="/api/*/clusters/*/job/**" access="hasRole('data-scientist')"/>
<intercept-url pattern="/api/*/clusters/*/task/**" access="hasRole('data-scientist')"/>
<intercept-url pattern="/api/*/clusters/**" access="hasRole('cluster-admin')"/>
<http-basic />
</http>
<beans:bean id="unauthorizedEntryPoint" class="edbServer.server.security.EntryPoint"/>
<beans:bean id="userDetailService" class="edbServer.server.security.UserDetailsServiceImp"/>
<beans:bean name="bcryptEncoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<authentication-manager id="edbAuthenticationManager">
<authentication-provider user-service-ref="userDetailService">
<password-encoder ref="bcryptEncoder"/>
</authentication-provider>
</authentication-manager>
<beans:bean id="tokenManager" class="edbServer.server.security.TokenManager"/>
<beans:bean id="authenticationService" class="edbServer.server.security.EDBAuthenticationService"
c:authenticationManager-ref="edbAuthenticationManager" c:tokenManager-ref="tokenManager" c:inactivityTimeoutSeconds="1800" />
<beans:bean id="edbAuthenticationFilter" class="edbServer.server.security.EDBTokenAuthenticationFilter"
c:authenticationService-ref="authenticationService" c:loginLink="/api/v1/login" c:logoutLink="/api/v1/logout"/>
</beans:beans>
Relevant code to initialize jetty server
String SPRING_CONTEXT_LOCATION = "classpath:/webapp/WEB-INF/spring-security.xml";
ClassPathXmlApplicationContext parentSpringAppContext = new ClassPathXmlApplicationContext(contextLocations);
parentSpringAppContext.refresh();
ClassPathXmlApplicationContext springAppContext = new ClassPathXmlApplicationContext(
parentSpringAppContext);
springAppContext.refresh();
ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SECURITY
| ServletContextHandler.SESSIONS);
contextHandler.setContextPath(CONTEXT_PATH);
GenericWebApplicationContext springWebAppContext = new GenericWebApplicationContext();
springWebAppContext.setServletContext(contextHandler.getServletContext());
springWebAppContext.setParent(springAppContext);
contextHandler.getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
springWebAppContext);
DelegatingFilterProxy springSecurityFilter = new DelegatingFilterProxy();
springSecurityFilter.setTargetBeanName("springSecurityFilterChain");
contextHandler.addFilter(new FilterHolder(springSecurityFilter), "/api/*", EnumSet.of(DispatcherType.REQUEST));
ServletHolder sh = new ServletHolder(ServletContainer.class);
sh.setInitParameter("org.glassfish.jersey.server.ResourceConfig", "JacksonFeature.class");
sh.setInitParameter("jersey.config.server.provider.packages", "edbServer.server.api.services;");
sh.setInitParameter("com.sun.jersey.api.json.POJOMappingFeature", "true");
contextHandler.addServlet(sh, "/api/v1/*");
HandlerList handlerList = new HandlerList();
handlerList.addHandler(contextHandler);
server.setHandler(handlerList);
springAppContext.start();
server.start();
how can I set global-method-security in app context here?
Thanks,
Saurabh
First of all I kept the listener in web.xml
<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>
Then my springSecurity.xml goes like
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:security="http://www.springframework.org/schema/security"
xmlns:context="http://www.springframework.org/schema/context"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.2.xsd">
<security:http auto-config="true" use-expressions="true">
<security:intercept-url pattern="/*" access="permitAll" />
<security:session-management invalid-session-url="/" session-fixation-protection="newSession">
<security:concurrency-control max-sessions="1" error-if-maximum-exceeded="true" session-registry-alias="sessionRegistry"/>
</security:session-management>
<!-- access denied page -->
<security:access-denied-handler error-page="/loginerror" />
<security:form-login
login-page="/login?login_error=1"
default-target-url="/employee/listEmployee"
authentication-failure-url="/login/error"
/>
<security:logout invalidate-session="true" logout-success-url="/login" delete-cookies="JSESSIONID" />
<!-- enable csrf protection -->
<!-- <csrf/>-->
</security:http>
<!-- Select users and user_roles from database -->
<security:authentication-manager>
<security:authentication-provider ref="authenticationProvider"></security:authentication-provider>
</security:authentication-manager>
<bean id="authenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService">
<bean id="userAuthenticationService" class="com.elitenet.los.security.UserDetailsServiceImpl" />
</property>
<property name="passwordEncoder">
<bean class="org.springframework.security.authentication.encoding.ShaPasswordEncoder" />
</property>
</bean>
The controller goes like:I need the list of userNames which are logged in. But the sessionRegistry isn't working.
#Autowired
#Qualifier("sessionRegistry")
private SessionRegistry sessionRegistry;
#RequestMapping(value = "/showUserStatus",method = RequestMethod.GET)
public ModelAndView showUserStatus() {
List<String> usersNamesList = new ArrayList<String>();
List<User> userList = new ArrayList<User>();
try {
List<Object> principals =sessionRegistry.getAllPrincipals();//the principals here is empty
for (Object principal: principals) {
//import org.springframework.security.core.userdetails for User class
//User is a built in class of spring security core
if (principal instanceof User) {
getLog().info(((User) principal).getUserName());
getLog().info("going to list userNameList");
usersNamesList.add(((User) principal).getUserName());
}
}
getLog().info("going to list user");
userList = getUserService().getList();
} catch (Exception er) {
getLog().error("error while listing userList" + er);
}
return new ModelAndView("/user/showUserStatus", "userList", userList);
}
Can anyone help me what am I doing wrong
Please try mentioning in xml file
<bean id="sessionRegistry"
class="org.springframework.security.core.session.SessionRegistryImpl" />
#Controller class
Try injecting like below
#Resource(name="sessionRegistry")
private SessionRegistryImpl sessionRegistry;
I think you are almost there. The only thing you've probably missed is the use of session-registry-alias. By using that attribute on the concurrency-control element you expose the session registry, so that it can be injected to your own beans.
Now you have a reference to the session registry that will be populated by the ConcurrentSessionControlStrategy which is set up implicitly by the above configuration. To use it you would just inject it to your bean as normal:
<security:session-management>
<security:concurrency-control max-sessions="10" session-registry-ref="sessionRegistry"/>
</security:session-management>
<bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl"/>
or something like below
<bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy"
p:maximumSessions="1" >
<constructor-arg name="sessionRegistry" ref="sessionRegistry" />
</bean>
I want to cache LDAP user data locally to allow faster queries. Do the Spring LDAP offers such a functionality? How can I do this?
I am using Spring Security 3.1 and Spring LDAP 1.3.1 for authentication and authorization. It would be nice to have a cache for LDAP using built-in mechanism if exists..
Spring LDAP configuration:
applicationContext-ldap.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee.xsd
">
<!-- Ldap -->
<jee:jndi-lookup id="ldapUrl" jndi-name="appName/ldapUrl" expected-type="java.lang.String" />
<jee:jndi-lookup id="ldapUser" jndi-name="appName/ldapUser" expected-type="java.lang.String" />
<jee:jndi-lookup id="ldapPassword" jndi-name="appName/ldapPassword" expected-type="java.lang.String" />
<!-- for authentication and search purpose -->
<bean id="ldapContextSource" class="org.springframework.ldap.core.support.LdapContextSource">
<property name="url" ref="ldapUrl" />
<property name="userDn" ref="ldapUser" />
<property name="password" ref="ldapPassword" />
<property name="pooled" value="true" />
</bean>
<bean id="ldapTemplate" class="org.springframework.ldap.core.LdapTemplate">
<property name="contextSource" ref="ldapContextSource" />
</bean>
<!-- for pagination search purpose -->
<bean id="dirContext" factory-bean="ldapContextSource" factory-method="getReadOnlyContext" scope="session"/>
<bean id="singleLdapContextSource" class="org.springframework.ldap.core.support.SingleContextSource" scope="session">
<constructor-arg ref="dirContext"/>
</bean>
<bean id="singleLdapTemplate" class="org.springframework.ldap.core.LdapTemplate" scope="session">
<property name="contextSource" ref="singleLdapContextSource" />
</bean>
</beans>
Spring Security configuration:
spring-security.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<!-- This is where we configure Spring-Security -->
<security:http
auto-config="true"
use-expressions="true"
access-denied-page="/auth/denied"
>
<security:intercept-url pattern="/login" access="permitAll"/>
<security:intercept-url pattern="/app/admin" access="permitAll"/>
<security:intercept-url pattern="/app/common" access="hasRole('User')"/>
<security:intercept-url pattern="/viol/home" access="permitAll"/>
<security:intercept-url pattern="/app/users" access="permitAll"/>
<security:intercept-url pattern="/admin/edit/*" access="hasRole('Administrator')"/>
<security:form-login
login-page="/auth/login"
authentication-failure-url="/auth/loginFailure"
default-target-url="/auth/authorize"/>
<security:logout
invalidate-session="true"
logout-success-url="/auth/login"
logout-url="/logout"/>
</security:http>
<security:authentication-manager>
<security:ldap-authentication-provider
server-ref="ldapContextSource"
user-search-filter="(sAMAccountName={0})"
user-search-base="dc=myDomain,dc=com"
/>
</security:authentication-manager>
</beans>
Thank you very much for your help!
If you configure EhCacheBasedUserCache and use ldap-user-service then you can use cache as:
<authentication-manager>
<authentication-provider>
<ldap-user-service
user-search-filter="(sAMAccountName={0})" user-search-base="dc=myDomain,dc=com" cache-ref="userCache" />
</authentication-provider>
</authentication-manager>
I don't think Spring offers client side LDAP caching out of the box, as caching LDAP query results on the client would pose a security risk. The cache will certainly hold stale data at some point, which is not a huge problem if it's e.g. the email/home address of the user, but much worse when it comes to e.g. role assignments and other authentication/authorization related data. You will be much better off by scaling up the server side, so that it's able to handle the load.
That's being said, introducing caching is pretty easy since Spring 3.1, because it provides excellent support for it. In your case it would be enough to use a custom LdapContextSource like the following:
public class CachingLdapContextSource extends AbstractContextSource {
#Override
protected DirContext getDirContextInstance(Hashtable environment)
throws NamingException
{
InitialLdapContext context = new InitialLdapContext(environment, null);
return new CachingDirContextWrapper(context);
}
}
The wrapper class simply delegates all DirContext methods to the underlying implementation and decorates methods to be cached with #Cacheable.
class CachingDirContextWrapper implements DirContext {
private final DirContext delegate;
CachingDirContextWrapper(DirContext delegate) {
this.delegate = delegate;
}
#Override
#Cacheable(value = "search")
public NamingEnumeration<SearchResult> search(...)
{
return delegate.search(name, matchingAttributes, attributesToReturn);
}
...
}
Refer to the official documentation, and this tutorial on details about how to configure a cache storage to be used by Spring.
But once again, you'd better not do this, I think.
Trying to make some experiments with Spring MVC and Spring Security:
#Controller
#RequestMapping("/auth")
public class AuthController {
#Autowired
// #Qualifier("userDetailsService") - tried adding this
private MyUserDetailsService userDetailsService;
...
}
// #Scope("singleton") - tried adding this
#Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
...
}
Complete context.xml that I have:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.xsd">
<!-- ORIGINAL springmvc-servlet.xml -->
<mvc:annotation-driven />
<mvc:resources mapping="/static/**" location="/static/" />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsp/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
<context:annotation-config />
<context:component-scan base-package="com.xxxxxxxxx" />
<!-- end ORIGINAL springmvc-servlet.xml -->
<!-- FROM springmvc-security.xml -->
<security:global-method-security secured-annotations="enabled">
</security:global-method-security>
<security:http auto-config="true" access-denied-page="/auth/denied">
<security:intercept-url pattern="/admin/*" access="ROLE_ADMIN"/>
<security:intercept-url pattern="/user/*" access="ROLE_USER"/>
<security:intercept-url pattern="/auth/login" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<security:intercept-url pattern="/auth/register" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<security:intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<security:form-login login-page="/auth/login" authentication-failure-url="/auth/login?login_error=true" default-target-url="/user"/>
<security:logout logout-url="/auth/logout" logout-success-url="/" invalidate-session="true"/>
<security:openid-login authentication-failure-url="/login?login_error=t" user-service-ref="openIdUserDetailsService" />
</security:http>
<security:authentication-manager>
<security:authentication-provider user-service-ref="userDetailsService" />
</security:authentication-manager>
<!-- end FROM springmvc-security.xml -->
</beans>
For some reason, there are 2 instances of MyUserDetailsService created. The first one is used by Spring Security and the second one is injected to AuthController. What's the right approach in case I want to have a single instance of MyUserDetailsService?
You haven't shown enough configuration to be certain, but I'd bet money that you're confused about how Spring ApplicationContexts should be managed in a Spring MVC app. My answer to another question about the same problem is almost certainly what you need to read:
Declaring Spring Bean in Parent Context vs Child Context
You've most likely declared your service bean (either explicitly or with a component-scan) in both the root and child contexts of your app. Being a service bean, it should live only in the root context. You may also benefit from reading this answer:
Spring XML file configuration hierarchy help/explanation
this config works for me :
<security:authentication-manager>
<security:authentication-provider user-service-ref="userService">
and
<bean id="userService" class="com.mydomain.service.UserDetailsServiceImpl" />
A tutorial here.