How to pass an additional parameter with spring security login page - spring

I am trying to set the database name as the request input parameter from the spring security login page. At present I am only getting username that's been retrieved using spring security SecurityContextHolder.getContext().getAuthentication().
How to access the additional field that's been set on the login page?

There's a number of ways to do this but the official way to do it is using a custom AuthenticationDetails and AuthenticationDetailsSource, subclassing Spring's WebAuthenticationDetails and WebAuthenticationDetailsSource, respectively. Add the extra field to the custom WebAuthenticationDetails and have the custom WebAuthenticationDetailsSource get the data from the request to populate the field.
In Spring Security 3.1 it's easy to configure by using the authentication-details-source-ref attribute of the <form-login> element.
In 3.0 you have to use a BeanPostProcessor. There is an example in the Spring Security FAQ on using a BeanPostProcessor to configure a custom WebAuthenticationDetailsSource.
Once this is done then you can call SecurityContextHolder.getContext().getAuthentication().getDetails() to get access to your extra field.

Elaborating on #Vacuum's comment
Here's a simple way (untested, but I believe this would work)
Create a new class ExUsernamePasswordAuthenticationFilter that will extend the default filter and grab the additional parameter and store it in the session. It will look something like this:
public class ExUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
final String dbValue = request.getParameter("dbParam");
request.getSession().setAttribute("dbValue", dbValue);
return super.attemptAuthentication(request, response);
}
}
In your UserDetailsService implementation, modify your implementation of:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException;
to grab the session variable that the filter from step 1) makes available.
in your <http /> security set-up, override the default filter with your custom one
<custom-filter ref="beanForYourCustomFilterFromStep1" position="FORM_LOGIN_FILTER"/>
Refer to this part of the documentation for more info about custom filters: http://static.springsource.org/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#ns-custom-filters

sourcedelica mentioned using AuthenticationDetailsSource and a custom AuthenticationDetails.
Here is an example.
Add authentication-details-source-ref attribute with the bean id customWebAuthenticationDetailsSource to form-login:
<security:http>
<security:intercept-url pattern="/**" access="..." />
<security:form-login authentication-details-source-ref="customWebAuthenticationDetailsSource" login-page="..." />
<security:logout logout-success-url="..." />
</security:http>
Create a new class CustomWebAuthenticationDetailsSource:
package security;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import javax.servlet.http.HttpServletRequest;
public class CustomWebAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
#Override
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new CustomWebAuthenticationDetails(context);
}
}
and the related CustomWebAuthenticationDetails:
package security;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import javax.servlet.http.HttpServletRequest;
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
private final String yourParameter;
public CustomWebAuthenticationDetails(HttpServletRequest request) {
super(request);
yourParameter = request.getParameter("yourParameter");
}
public String getyourParameter() {
return yourParameter;
}
//TODO override hashCode, equals and toString to include yourParameter
#Override
public int hashCode() { /* collapsed */ }
#Override
public boolean equals(Object obj) { /* collapsed */ }
#Override
public String toString() { /* collapsed */ }
}

There is an easier way if you are using custom AuthenticationProvider. You can just inject HttpServletRequest and retrieve your extra parameter:
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired(required = false)
private HttpServletRequest request;
#Autowired
private MyAccountService myAccountService;
#Override
public Authentication authenticate(Authentication authentication) {
System.out.println("request testing= " + request.getParameter("testing"));
.....
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}

#user1322340 does not provide implement detail to get session Attributes in loadUserByUsername function:
Step 1: Follow all the step provided by #user1322340
Step 2:
you need add one configuration in web.xml like this:
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
Step 3:
Use such code to get attributes:
RequestContextHolder.getRequestAttributes().getAttribute("yourAttributeName", RequestAttributes.SCOPE_SESSION);
Step 4: Register your filter in spring security config.
If you get a error "authenticationManager must be specified". after you register your filter in config. You need set a authenticationManagerBean for your extended filter and config it in that way:
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public ExUsernamePasswordAuthenticationFilter exUsernamePasswordAuthenticationFilter()
throws Exception {
ExUsernamePasswordAuthenticationFilter exUsernamePasswordAuthenticationFilter = new ExUsernamePasswordAuthenticationFilter();
exUsernamePasswordAuthenticationFilter
.setAuthenticationManager(authenticationManagerBean());
return exUsernamePasswordAuthenticationFilter;
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
RequestMatcher requestMatcher = new RequestMatcher() {
#Override
public boolean matches(HttpServletRequest httpServletRequest) {
if (httpServletRequest.getRequestURI().indexOf("/api", 0) >= 0) {
return true;
}
return false;
}
};
http
.addFilterBefore(exUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
...
}
}

For spring security 3.0 or above which uses java configuration, the following simple steps works well.
Add a your of filter before the
UserNameandPasswordAuthenticationFilter in HttpSecurity object in configure.
http.addFilterBefore(new YourFilter(), UsernamePasswordAuthenticationFilter.class);
Let the filter has a line like this to get the needed fields in your
request to session.
if(requestPath != null &&requestPath.equals("/login") ) {
session.setAttribute("yourParam",req.getParameter("yourParam"));
}
Later you may get the parameter value from the session in any class as:
String yourParam =(String)request.getSession().getAttribute("yourParam");

Simple way:
1) register RequestContextListener
#Bean
public RequestContextListener requestContextListener(){
return new RequestContextListener();
}
2) And to main class:
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.
currentRequestAttributes()).
getRequest();
3) After that we can take params in custom headers:
request.getHeader("OrganizationId")

Simplest way in only 2 steps:
Step 1.
Add the following listener in web.xml:
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</context-param>
Step 2.
Add the following in your class method where you want to get additional param:
RequestAttributes attribs = RequestContextHolder.getRequestAttributes();
if (RequestContextHolder.getRequestAttributes() != null) {
HttpServletRequest request = ((ServletRequestAttributes) attribs).getRequest();
}
Now you can get your additional parameter by the following, assuming the extra parameter is named "loginType":
request.getParameter("loginType")

Related

Camel REST and Spring Security Java Configuration

I'm trying to set up Camel REST to use basic auth with a simple username/password from my application.properties and can't for the life of me seem to configure Camel Spring Security to do that. I'm trying to follow the Spring Security component documentation which seems to be missing the example of configuring the required beans. I found the missing example here under 'Controlling access to Camel routes' but this only shows the xml configuration.
How do I set up the required SpringSecurityAuthorizationPolicy bean? It needs an AuthenticationManager and an AccessDecisionManager and it also seems to require that I set its SpringSecurityAccessPolicy which I have no idea how to do.
I haven't gotten to test these yet, because I can't get my beans set up, but my rest route looks like:
rest("/ingest")
.post("/json").consumes("application/json")
.route()
.process(authProcessor)
.policy(authPolicy) // this is the bean I don't know how to configure
.to("direct:ingest")
.endRest();
and my AuthProcessor (taken from the camel component doc) looks like:
#Component
public class AuthProcessor implements Processor {
public void process(Exchange exchange) {
String userpass = new String(Base64.decodeBase64(exchange.getIn().getHeader("Authorization", String.class)));
String[] tokens = userpass.split(":");
// create an Authentication object
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(tokens[0], tokens[1]);
// wrap it in a Subject
Subject subject = new Subject();
subject.getPrincipals().add(authToken);
// place the Subject in the In message
exchange.getIn().setHeader(Exchange.AUTHENTICATION, subject);
}
}
and here's my broken bean configuration for what it's worth:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public SpringSecurityAuthorizationPolicy springSecurityAuthorizationPolicy(
AuthenticationManager authenticationManager, AccessDecisionManager accessDecisionManager) {
SpringSecurityAuthorizationPolicy policy = new SpringSecurityAuthorizationPolicy();
SpringSecurityAccessPolicy springSecurityAccessPolicy = new SpringSecurityAccessPolicy();
policy.setAuthenticationManager(authenticationManager);
policy.setAccessDecisionManager(accessDecisionManager);
policy.setSpringSecurityAccessPolicy(????);
return policy;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("pass").roles("USER");
}
#Bean(name = BeanIds.AUTHENTICATION_MANAGER)
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public AccessDecisionManager accessDecisionManager() {
AffirmativeBased affirmativeBased = new AffirmativeBased(ImmutableList.of(
new RoleVoter()
));
affirmativeBased.setAllowIfAllAbstainDecisions(true);
return affirmativeBased;
}
}
I've been banging my head against the wall trying to understand this so an example of how to do this would be amazing. It looks like the xml configuration for what I want to do (in the second link) is simple enough but I can't seem to replicate it in Java configuration.
I know it's an old topic, but I ran into similar questions. I managed to get it working. Not by overriding the accessDecisionManager() method within the WebSecurityConfigurerAdapter class, but by constructing a new instance while building my SpringSecurityAuthorizationPolicy:
#Bean
public Policy adminPolicy(AuthenticationManager authenticationManager) {
RoleVoter roleVoter = new RoleVoter();
SpringSecurityAuthorizationPolicy policy = new SpringSecurityAuthorizationPolicy();
policy.setAuthenticationManager(authenticationManager);
policy.setAccessDecisionManager(new UnanimousBased(List.of(roleVoter)));
policy.setSpringSecurityAccessPolicy(new SpringSecurityAccessPolicy(roleVoter.getRolePrefix() + "<ROLE_NAME>");
return policy;
}

Sending message to specific user using spring

My Goal - To send message to single user if possible without using spring security
I want to input a username from user and set it as username in spring security so that I can use method convertAndSendToUser. I searched on the net and found two approaches
Using DefaultHandshakeHandler to set username but this way I am unable to retrieve user input from the page and use it in determineUser method
I have tried using following piece of code
Authentication request = new UsernamePasswordAuthenticationToken("xyz", null);
SecurityContextHolder.getContext().setAuthentication(request);
But it is not working as it is just changing the username for that method and then it resets the username.
If possible is there any approach with which I can send message to single user without using spring security. Thanks in advance
P.S. I am a newbee.
You can use your first approach to set the Username. First you need add the interceptor to your StompEndpointRegistry class and after that you can determine User from the attributes Map and return the Principal.
Below is the Code:
HttpSessionHandshakeInterceptor is Used for Intercepting the Http attributes and provide them in the DefaultHandshakeHandler class
#Configuration
#EnableWebSocketMessageBroker
#EnableWebMvc
#Controller
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app","/user");
}
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat")
//Method .addInterceptors for enabling interceptor
.addInterceptors(new HttpSessionHandshakeInterceptor())
.setHandshakeHandler(new MyHandler())
.withSockJS();
}
class MyHandler extends DefaultHandshakeHandler{
#Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler,
Map<String, Object> attributes) {
//Get the Username object which you have saved as session objects
String name = (String)attributes.get("name");
//Return the User
return new UsernamePasswordAuthenticationToken(name, null);
}
}
}

Spring MVC accessing Spring Security ConfigAttributes?

I want to produce HTTP Response Body with an error message referencing something like _"missing ... 'CUSTOM_AUTHORITY'"_ in addition to a 403 Forbidden HTTP Status code.
My application is Spring Boot with a Spring-Security-Secured #PreAuthorize method within a Spring-MVC-REST #Controller:
MyController
#Controller
#RequestMapping("/foo")
public FooController{
#PreAuthorize("hasAuthority('CUSTOM_AUTHORITY')")
public Object getSomething(){ ... }
}
GlobalExceptionHandlerResolver
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(AccessDeniedException.class)
#ResponseStatus(HttpStatus.FORBIDDEN)
public Object forbidden(AccessDeniedException exception){ ... }
}
What I want is to expose/inject Collection<ConfigAttribute>. The Spring Security docs reference it.
There doesn't seem to be a straightforward way of accomplishing this. The AccessDecisionManager (which is AffirmativeBased) throws the AccessDeniedException with none of the information you want. So if you want to "expose/inject" the Collection<ConfigAttribute>, you'll want to provide your own AccessDecisionManager that throws a custom exception that holds the ConfigAttributes.
The easiest way to do this could be to wrap the default AccessDecisionManager with your own and delegate method calls to it:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled=true)
CustomMethodSecurityConfig extends GlobalMethodSecurityConfiguration
#Override
protected AccessDecisionManager accessDecisionManager() {
AccessDecisionManager default = super.accessDecisionManager();
MyCustomDecisionManager custom = new CustomDecisionManager(default);
}
}
You could define your custom AccessDecisionManager as follows:
public class MyCustomDecisionManager implements AccessDecisionManager {
private AccessDecisionManager default;
public MyCustomDecisionManager(AccessDecisionManager acm) {
this.default = acm;
}
#Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException{
try {
default.decide(authentication, object, configAttributes)
} catch(AccessDeniedException ex) {
throw new CustomAccessDeniedException(ex.getMessage(), configAttributes);
}
}
// other methods delegate to default
}
Now whenever access is denied, you will get an exception that holds the Collection<ConfigAttribute>.
Your custom exception could look like this:
public class CustomAccessDeniedException extends AccessDeniedException {
private Collection<ConfigAttribute> attributes;
public CustomAccessDeniedException(String message, Collection<ConfigAttribute> attr) {
super(message);
this.attributes = attr;
}
public Collection<ConfigAttribute> getAttributes() {
return this.attributes;
}
}
Now your #ExceptionHandler could handle your CustomAccessDeniedException and have access to the ConfigAttributes.
HOWEVER...
I am not sure that will provide you with the error message you wanted. The ConfigAttribute interface only has one method:
String getAttribute();
And the javadoc states:
If the ConfigAttribute cannot be expressed with sufficient precision as a String, null should be returned.
Since we can't rely on the interface method, how you deal with each ConfigAttribute will be heavily dependent on the type of the particular object you're dealing with.
For example, the ConfigAttribute that corresponds to #PreAuthorize("hasAuthority('CUSTOM_AUTHORITY')") is PreInvocationExpressionAttribute, and to print something that resembles what you want, you could do:
PreInvocationExpressionAttribute attr = (PreInvocationExpressionAttribute)configAttribute;
String expressionString = attr.getAuthorizeExpression().getExpressionString();
System.out.println(expressionString); // "hasAuthority('CUSTOM_AUTHORITY')"
That's the major drawback. Also, you would get ALL the ConfigAttributes, not necessarily the ones that failed.

Multiple servlet mappings in Spring Boot

Is there any way to set via property 'context-path' many mappings for a same Spring Boot MVC application? My goal is to avoid creating many 'Dispatcherservlet' for the uri mapping.
For example:
servlet.context-path =/, /context1, context2
You can create #Bean annotated method which returns ServletRegistrationBean , and add multiple mappings there. This is more preferable way, as Spring Boot encourage Java configuration rather than config files:
#Bean
public ServletRegistrationBean myServletRegistration()
{
String urlMapping1 = "/mySuperApp/service1/*";
String urlMapping2 = "/mySuperApp/service2/*";
ServletRegistrationBean registration = new ServletRegistrationBean(new MyBeautifulServlet(), urlMapping1, urlMapping2);
//registration.set... other properties may be here
return registration;
}
On application startup you'll be able to see in logs:
INFO | localhost | org.springframework.boot.web.servlet.ServletRegistrationBean | Mapping servlet: 'MyBeautifulServlet' to [/mySuperApp/service1/*, /mySuperApp/service2/*]
You only need a single Dispatcherservlet with a root context path set to what you want (could be / or mySuperApp).
By declaring multiple #RequestMaping, you will be able to serve different URI with the same DispatcherServlet.
Here is an example. Setting the DispatcherServlet to /mySuperApp with #RequestMapping("/service1") and #RequestMapping("/service2") would exposed the following endpoints :
/mySuperApp/service1
/mySuperApp/service2
Having multiple context for a single servlet is not part of the Servlet specification. A single servlet cannot serve from multiple context.
What you can do is map multiple values to your requesting mappings.
#RequestMapping({"/context1/service1}", {"/context2/service1}")
I don't see any other way around it.
You can use 'server.contextPath' property placeholder to set context path for the entire spring boot application. (e.g. server.contextPath=/live/path1)
Also, you can set class level context path that will be applied to all the methods e.g.:
#RestController
#RequestMapping(value = "/testResource", produces = MediaType.APPLICATION_JSON_VALUE)
public class TestResource{
#RequestMapping(method = RequestMethod.POST, value="/test", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<TestDto> save(#RequestBody TestDto testDto) {
...
With this structure, you can use /live/path1/testResource/test to execute save method.
None of the answers to this sort of question seem to mention that you'd normally solve this problem by configuring a reverse proxy in front of the application (eg nginx/apache httpd) to rewrite the request.
However if you must do it in the application then this method works (with Spring Boot 2.6.2 at least) : https://www.broadleafcommerce.com/blog/configuring-a-dynamic-context-path-in-spring-boot.
It describes creating a filter, putting it early in the filter chain and basically re-writing the URL (like a reverse proxy might) so that requests all go to the same place (ie the actual servlet.context-path).
I've found an alternative to using a filter described in https://www.broadleafcommerce.com/blog/configuring-a-dynamic-context-path-in-spring-boot that requires less code.
This uses RewriteValve (https://tomcat.apache.org/tomcat-9.0-doc/rewrite.html) to rewrite urls outside of the context path e.g. if the real context path is "context1" then it will map /context2/* to /context1/*
#Component
public class LegacyUrlWebServerFactoryCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
private static final List<String> LEGACY_PATHS = List.of("context2", "context3");
#Override
public void customize(TomcatServletWebServerFactory factory) {
RewriteValve rewrite = new RewriteValve() {
#Override
protected void initInternal() throws LifecycleException {
super.initInternal();
try {
String config = LEGACY_PATHS.stream() //
.map(p -> String.format("RewriteRule ^/%s(/.*)$ %s$1", p, factory.getContextPath())) //
.collect(Collectors.joining("\n"));
setConfiguration(config);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
};
factory.addEngineValves(rewrite);
}
}
If you need to use HTTP redirects instead then there is a little bit more required (to avoid a NullPointerException in sendRedirect):
#Component
public class LegacyUrlWebServerFactoryCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
private static final List<String> LEGACY_PATHS = List.of("context2", "context3");
#Override
public void customize(TomcatServletWebServerFactory factory) {
RewriteValve rewrite = new RewriteValve() {
#Override
protected void initInternal() throws LifecycleException {
super.initInternal();
try {
String config = LEGACY_PATHS.stream() //
.map(p -> String.format("RewriteRule ^/%s(/.*)$ %s$1 R=permanent", p, factory.getContextPath())) //
.collect(Collectors.joining("\n"));
setConfiguration(config);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
#Override
public void invoke(Request request, Response response) throws IOException, ServletException {
if (request.getContext() == null) {
String[] s = request.getRequestURI().split("/");
if (s.length > 1 && LEGACY_PATHS.contains(s[1])) {
request.getMappingData().context = new FailedContext();
}
}
super.invoke(request, response);
}
};
factory.addEngineValves(rewrite);
}
}
I use this approach:
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
#Configuration
public class WebAppInitializer implements WebApplicationInitializer {
#Override
public void onStartup(ServletContext servletContext) {
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
rootContext.register(AppConfig.class);
rootContext.setServletContext(servletContext);
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(rootContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/mapping1/*");
dispatcher.addMapping("/mapping2/*");
servletContext.addListener(new ContextLoaderListener(rootContext));
}
}

Dynamic Spring Security using SQL Query

Hello I want to make an intercept url pattern and access dynamically by using sql query in spring security.
Generally we use this type of notation in XML and I want to take these values (/add-role and ROLE_ADMIN) from database.
<intercept-url pattern="/add-role*" access="ROLE_ADMIN" />
Is it possible to do this dynamically?
Disclaimer
As the Spring Security FAQ mentions, the first thing you should do is ask should I really do this? Security is complicated and the configuration should be tested extensively. Allowing the configuration to change dynamically only further complicates things making the application that much more vulnerable. If you really want to do this, the FAQ outlines a basic method to accomplish this. I have expanded upon the FAQ's answer below.
Implement Custom FilterInvocationSecurityMetadataSource
To obtain the security URL mappings dynamically you can implement your own FilterInvocationSecurityMetadataSource. An example implementation is given below.
NOTE: Keep in mind that getAttributes will be invoked for every request that Spring Security intercepts so you will most likely want some sort of caching.
public class JdbcFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
FilterInvocation fi = (FilterInvocation) object;
String url = fi.getRequestUrl();
HttpServletRequest request = fi.getHttpRequest();
// Instead of hard coding the roles lookup the roles from the database using the url and/or HttpServletRequest
// Do not forget to add caching of the lookup
String[] roles = new String[] { "ROLE_ADMIN", "ROLE_USER" };
return SecurityConfig.createList(roles);
}
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
Create a BeanPostProcessor
You cannot use the namespace to wire it up, so taking another tip from the FAQ you can use a BeanPostProcessor which might look like:
public class FilterInvocationSecurityMetadataSourcePostProcessor implements BeanPostProcessor, InitializingBean {
private FilterInvocationSecurityMetadataSource securityMetadataSource;
public Object postProcessAfterInitialization(Object bean, String name) {
if (bean instanceof FilterSecurityInterceptor) {
((FilterSecurityInterceptor)bean).setSecurityMetadataSource(securityMetadataSource);
}
return bean;
}
public Object postProcessBeforeInitialization(Object bean, String name) {
return bean;
}
public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource securityMetadataSource) {
this.securityMetadataSource = securityMetadataSource;
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(securityMetadataSource,"securityMetadataSource cannot be null");
}
}
XML Configuration
Then, assuming both of the above beans are in the package sample, you would add the following configuration
<bean class="sample.FilterInvocationSecurityMetadataSourcePostProcessor">
<property name="securityMetadataSource">
<bean class="sample.JdbcFilterInvocationSecurityMetadataSource"/>
</property>
</bean>
Possible Problems
If you end up getting a ClassCastException, you are likely running into SEC-1957 which was fixed in Spring Security 3.1.1+ Try updating to the latest version to resolve this.
You cant really get those values from the databse, but you can write a custom code called DecisionManager that evaluates if the resource is allowed to execute. With that code you can even read data from the database.
<bean id="MyDecisionManagerBean" class="org.springframework.security.vote.UnanimousBased">
<property name="decisionVoters">
<list>
<!-- <bean class="org.springframework.security.vote.RoleVoter"/> -->
<bean class="org.springframework.security.vote.RoleHierarchyVoter" >
<constructor-arg>
<bean class="org.springframework.security.userdetails.hierarchicalroles.RoleHierarchyImpl" factory-bean="roleHierarchyImplFactory" factory-method="createRoleHierarchyImpl"/>
</constructor-arg>
</bean>
<bean class="com.mycompany.RoleDenyVoter"/>
<bean class="com.mycompany.RoleAllowVoter"/>
</list>
</property>
</bean>
Your class will be like this :
public class RoleDenyVoter implements AccessDecisionVoter {
public int vote(Authentication authentication, Object object, ConfigAttributeDefinition config) {
//read from the DB and decide if access is granted
the process is documented here :
http://static.springsource.org/spring-security/site/docs/3.0.x/reference/authz-arch.html#authz-voting-based
I have created this entry for update purpose
Implement Custom FilterInvocationSecurityMetadataSource
This class only obtains the URL in every request and lookup their permissions from the database or third party applications
public class CommonFilterSecurityMetaDataSource implements FilterInvocationSecurityMetadataSource {
private final Map<String, UrlRequestModel> permissions;
#Autowired
private UrlRequestDao urlRequestDao;
public CommonFilterSecurityMetaDataSource() {
permissions = new Hashtable<>();
}
public List<ConfigAttribute> getAttributes(Object object) {
final FilterInvocation fi = (FilterInvocation) object;
final String url = fi.getRequestUrl();
final String httpMethod = fi.getRequest().getMethod();
final String key = String.format("%s %s", httpMethod, url);
final UrlRequestModel urlRequestModel;
List<ConfigAttribute> attributes = null;
// Lookup your database (or other source) using this information and populate the
// list of attributes
if(permissions.containsKey(key)) {
urlRequestModel= permissions.get(key);
} else {
urlRequestModel= catRequestDao.findByUrl(url);
if(catRequestMapModel != null) {
permissions.put(key, urlRequestModel);
}
}
if (catRequestMapModel != null) {
List<RoleModel> roles = ulrRequestModel.getRoleList();
if(!roles.isEmpty()) {
attributes = new ArrayList<>(roles.size());
for (RoleModel role : roles) {
attributes.add(new SecurityConfig(role.getDescription()));
}
}
}
return attributes;
}
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
Java configuration
For java configuration only add this to your class wich extends from WebSecurityConfigurerAdapter
#Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();
http.authorizeRequests().
antMatchers( "/javax.faces.resource/**").permitAll().
and()
.exceptionHandling().accessDeniedPage("/accessDenied.jsf").
and().formLogin().
loginPage("/login.jsf").
loginProcessingUrl("/loginAction").
usernameParameter("app_username").
passwordParameter("app_password").
defaultSuccessUrl("/secure/index.jsf").
and().logout().
logoutUrl("/appLogout").
logoutSuccessUrl("/login.jsf").logoutRequestMatcher(new AntPathRequestMatcher("/appLogout")).
and().addFilterAfter(filterSecurityInterceptor(), FilterSecurityInterceptor.class);
http.csrf().disable();
}
#Bean
public FilterSecurityInterceptor filterSecurityInterceptor() throws Exception {
FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
filterSecurityInterceptor.setSecurityMetadataSource(securityMetadataSource());
filterSecurityInterceptor.setAuthenticationManager(authenticationManager());
filterSecurityInterceptor.setAccessDecisionManager(accessDecisionManager());
filterSecurityInterceptor.setPublishAuthorizationSuccess(true);
return filterSecurityInterceptor;
}
#Bean
public AccessDecisionManager accessDecisionManager() {
AuthenticatedVoter authenticatedVoter = new AuthenticatedVoter();
RoleVoter roleVoter = new RoleVoter();
List<AccessDecisionVoter<? extends Object>> voters = new ArrayList<>();
voters.add(authenticatedVoter);
voters.add(roleVoter);
return new AffirmativeBased(voters);
}
#Bean
public FilterInvocationSecurityMetadataSource securityMetadataSource() {
return new CommonFilterSecurityMetaDataSource();
}
I tested it using Spring security 5.0.8

Resources