Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 7 years ago.
Improve this question
In a Spring MVC app using Spring Security, I want to use a custom AuthenticationProvider to check n-number of additional fields beyond the default username and password. For example, if a user wants to authenticate, I want her to have to supplement her username and password with a pin code she receives via email, a pincode she receives via text, and n number of other credentials. However, to keep this question narrow, let's just focus on adding one additional pin to the login, but let's set it up in a way that enables us to add n-other credentials easily afterwards.
I want to use Java configuration.
I have created a custom AuthenticationProvider, a custom AuthenticationFilter, custom UserDetailsService, and a few other changes.
But the app is granting access when a user tries to log in whether or not the user has valid credentials, as shown in a screen shot in the instructions for reproducing the problem below. What specific changes need to be made to the code that I am sharing so that the custom n-factor authentication can function properly?
The structure of my test project is shown in the following screen shots:
Here is the Java code structure in eclipse project explorer:
{ Image host not available }
The XML config files can be located by scrolling down in project explorer to show the following:
{ Image host not available }
The view code can be found by scrolling a little further down in project explorer as follows:
{ Image host not available }
You can download and explore all this code in a working Eclipse project:
{ File now deleted }
CustomAuthenticationProvider.java is:
package my.app.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
public class CustomAuthenticationProvider implements AuthenticationProvider{
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
List<GrantedAuthority> grantedAuths = new ArrayList<>();
if (name.equals("admin") && password.equals("system")) {
grantedAuths.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
if(pincodeEntered(name)){
grantedAuths.add(new SimpleGrantedAuthority("registered"));
}
Authentication auth = new UsernamePasswordAuthenticationToken(name, password, grantedAuths);
return auth;
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
private boolean pincodeEntered(String userName){
// do your check here
return true;
}
}
MessageSecurityWebApplicationInitializer.java is:
package my.app.config;
import org.springframework.core.annotation.Order;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
#Order(2)
public class MessageSecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
}
TwoFactorAuthenticationFilter.java is:
package my.app.config;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
public class TwoFactorAuthenticationFilter extends UsernamePasswordAuthenticationFilter
{
private String extraParameter = "extra";
private String delimiter = ":";
/**
* Given an {#link HttpServletRequest}, this method extracts the username and the extra input
* values and returns a combined username string of those values separated by the delimiter
* string.
*
* #param request The {#link HttpServletRequest} containing the HTTP request variables from
* which the username client domain values can be extracted
*/
#Override
protected String obtainUsername(HttpServletRequest request){
String username = request.getParameter(getUsernameParameter());
String extraInput = request.getParameter(getExtraParameter());
String combinedUsername = username + getDelimiter() + extraInput;
System.out.println("Combined username = " + combinedUsername);
return combinedUsername;
}
/**
* #return The parameter name which will be used to obtain the extra input from the login request
*/
public String getExtraParameter(){
return this.extraParameter;
}
/**
* #param extraParameter The parameter name which will be used to obtain the extra input from the login request
*/
public void setExtraParameter(String extraParameter){
this.extraParameter = extraParameter;
}
/**
* #return The delimiter string used to separate the username and extra input values in the
* string returned by <code>obtainUsername()</code>
*/
public String getDelimiter(){
return this.delimiter;
}
/**
* #param delimiter The delimiter string used to separate the username and extra input values in the
* string returned by <code>obtainUsername()</code>
*/
public void setDelimiter(String delimiter){
this.delimiter = delimiter;
}
}
SecurityConfig.java is:
package my.app.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
#Configuration
#EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
public void registerGlobalAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider());
}
#Bean
AuthenticationProvider customAuthenticationProvider() {
CustomAuthenticationProvider impl = new CustomAuthenticationProvider();
return impl ;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/secure-home")
.usernameParameter("j_username")
.passwordParameter("j_password")
.loginProcessingUrl("/j_spring_security_check")
.failureUrl("/login")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.and()
.authorizeRequests()
.antMatchers("/secure-home").hasAuthority("registered")
.antMatchers("/j_spring_security_check").permitAll()
.and()
.userDetailsService(userDetailsService());
}
}
User.java is:
package my.app.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
#Entity
#Table(name="users")
public class User implements UserDetails{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue
private Integer id;
#Column(name= "email", unique=true, nullable=false)
private String login;//must be a valid email address
#Column(name = "password")
private String password;
#Column(name = "phone")
private String phone;
#Column(name = "pin")
private String pin;
#Column(name = "sessionid")
private String sessionId;
#ManyToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinTable(name="user_roles",
joinColumns = {#JoinColumn(name="user_id", referencedColumnName="id")},
inverseJoinColumns = {#JoinColumn(name="role_id", referencedColumnName="id")}
)
private Set<Role> roles;
public Integer getId() {return id;}
public void setId(Integer id) { this.id = id;}
public String getPhone(){return phone;}
public void setPhone(String pn){phone = pn;}
public String getPin(){return pin;}
public void setPin(String pi){pin = pi;}
public String getSessionId(){return sessionId;}
public void setSessionId(String sd){sessionId = sd;}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
//roles methods
public void addRole(Role alg) {roles.add(alg);}
public Set<Role> getRoles(){
if(this.roles==null){this.roles = new HashSet<Role>();}
return this.roles;
}
public void setRoles(Set<Role> alg){this.roles = alg;}
public boolean isInRoles(int aid){
ArrayList<Role> mylgs = new ArrayList<Role>();
mylgs.addAll(this.roles);
for(int a=0;a<mylgs.size();a++){if(mylgs.get(a).getId()==aid){return true;}}
return false;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// TODO Auto-generated method stub
return null;
}
#Override
public String getUsername() {
// TODO Auto-generated method stub
return null;
}
#Override
public boolean isAccountNonExpired() {
// TODO Auto-generated method stub
return false;
}
#Override
public boolean isAccountNonLocked() {
// TODO Auto-generated method stub
return false;
}
#Override
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return false;
}
#Override
public boolean isEnabled() {
// TODO Auto-generated method stub
return false;
}
}
The xml config is in business-config.xml and is:
<beans profile="default,spring-data-jpa">
<!-- lots of other stuff -->
<bean class="my.app.config.SecurityConfig"></bean>
</beans>
<!-- lots of unrelated stuff -->
In addition, mvc-core-config.xml contains the following:
<!-- lots of other stuff -->
<mvc:view-controller path="/" view-name="welcome" />
<mvc:view-controller path="/login" view-name="login" />
And login.jsp looks like this:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<%#taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%# page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Custom Login page</title>
<style>.error {color: red;}</style>
</head>
<body>
<div class="container">
<h1>Custom Login page</h1>
<p>
<c:if test="${error == true}">
<b class="error">Invalid login or password or pin.</b>
</c:if>
</p>
<form method="post" action="<c:url value='j_spring_security_check'/>" >
<table>
<tbody>
<tr>
<td>Login:</td>
<td><input type="text" name="j_username" id="j_username"size="30" maxlength="40" /></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" name="j_password" id="j_password" size="30" maxlength="32" /></td>
</tr>
<tr>
<td>Pin:</td>
<td><input type="text" name="pin" id="pin"size="30" maxlength="40" /></td>
</tr>
<tr>
<td colspan=2>
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="Login" /></td>
</tr>
</tbody>
</table>
</form>
</div>
</body>
</html>
The spring security dependencies in 'pom.xml' are:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>3.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>3.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>3.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>3.2.2.RELEASE</version>
</dependency>
Download and reproduce on your machine
I have also uploaded a working Eclipse project which contains the bare minimum code required to reproduce the problem on your local devbox. You can download the Eclipse project here:
{ File now deleted }
Once you have downloaded the zipped project, you can reproduce the problem on your machine by following these steps:
1.) Unzip the zip file to a new folder
2.) In Eclipse, do File > Import > Existing Maven Projects
3.) Click Next. Browse to folder of unzipped project. Complete wizard to import project.
4.) Right click on project name in eclipse and do Maven > Download sources
5.) Right click on project name again in eclipse and do Maven > Update project
6.) Open MySQL and create an empty new database called somedb
7.) In the Eclipse project, open data-access.properties as shown in the following picture, and change someusername and somepassword to your real username and password for your MySQL.
{ Image host not available }
8.) In Eclipse, right click the project and chose Run As .. Run on server.. . This should launch the app so that you see the following in your browser at the http://localhost:8080/n_factor_auth/ url:
{ Image host not available }
9.) Change the URL to http://localhost:8080/n_factor_auth/secure-home to see that you were redirected to http://localhost:8080/n_factor_auth/login which serves the sample custom login page, which requires a pin in addition to the username and password. Note that the result needs to accommodate n-factors and not simply adding a single pin code:
{ Image host not available }
10.) Insert test credentials into the MySQL database by running the following SQL commands, which you could put in a .sql file and run from the MySQL command line using the source command. Note that the database objects will be deleted and recreated empty every time the app starts because hbm2ddl is enabled to simplify this example. Thus, the following SQL commands will need to be re-run every time you reload the app in Eclipse.
SET FOREIGN_KEY_CHECKS=0;
INSERT INTO `roles` VALUES (100,'registered');
INSERT INTO `user_roles` VALUES (100,100);
INSERT INTO `users` (id, email,password, phone, pin) VALUES (100,'me#mydomain.com','somepassword','xxxxxxxxxx', 'yyyy');
SET FOREIGN_KEY_CHECKS=1;
11.) Try to login using any credentials (valid or invalid), and get the following successful login screen (Note that the user gets logged in whether or not they give valid credentials):
{ Image host not available }
That's it. You now have the problem recreated on your machine, including all the code shown above, but in a working minimalist eclipse project. So now how do you answer the OP above? What changes do you make to the code above, and what else do you do in order to get the custom authenticator to engage upon login?
I am interested to learn what specific changes need to be made to the minimalist download app in order to enable n-factor authentication. I will validate by checking your suggestions in the sample app on my machine.
Thanks to various people (including M.Deinum) who have suggested deleting redundant XML config to create the current version shown in this posting.
First, some explanation about the interfaces you are working with and the role they play in the authentication process:
Authentication - represents the result of authenticating a user. Holds the authorities granted to that user and any additional details that may be needed about the user. As there is no way for the framework to know, what details are going to be needed, the authentication object has a getDetails method that can return any object
AuthenticationProvider - object that can create an Authentication object in some way. To make them more reusable, some (or most) of AuthenticationProviders refrain from setting the user details on the Authentication object, as each application may need specific user details. Instead they delegate the process of resolving the user details to a settable UserDetailsService
UserDetailsService - a strategy for retrieving the user details required in your application.
So, if you are creating a custom AuthenticationProvider you may not even need to implement it in a way that requires a UserDetailsService. The decission is up to you and depends, on whether you plan on reusing your implementation in other projects.
As for the compilation problems in your code, you are mixing two ways of providing the UserDetailsService. In the CustomAuthenticationProvider you have annotated the userService field with the #Inject annotation .This means, that the container (Spring application context in your case) is to find a suitable implementation and inject it into that field at runtime using reflection. The process of setting this field by the context is called dependency injection. In the SecurityConfig class you are trying to provide the implementation yourself by setting the field through the setUserDetailsService method that does not exist in your class.
To resolve this problem you need to decide to use one of the ways to provide the UserDetails service and either:
remove the #Inject annotation and create the setUserDetailsService method, or
remove the line when you are calling the non-existant method and declare your implementation of the UserDetailsService as a bean
As for which of the ways should you choose, the dependecy injection way may by better if you can find a way of making your SecurityConfig class reusable in other projects. In that case you could just import it (by using the #Import annotaion) and declare a different UserDetailsSerice implementation as a bean in your next application and have it working.
Usually, classes like the SecurityConfig are not really reusable, so creating the setter and removing the dependency injection would probably be my first choice.
EDIT
A working, albeit a simplistic implementation (based heavily on this blog entry) would be:
public class CustomAuthenticationProvider implements AuthenticationProvider{
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
List<GrantedAuthority> grantedAuths = new ArrayList<>();
if (name.equals("admin") && password.equals("system")) {
grantedAuths.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
if(pincodeEntered(name)){
grantedAuths.add(new SimpleGrantedAuthority("ROLE_PINCODE_USER"));
}
Authentication auth = new UsernamePasswordAuthenticationToken(name, password, grantedAuths);
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
private boolean pincodeEntered(String userName){
// do your check here
return true;
}
}
Then in your config class change the following method:
#Bean
AuthenticationProvider customAuthenticationProvider() {
return new CustomAuthenticationProvider();
}
The first thing we need to do is extend the UsernamePasswordAuthenticationFilter class so that it can handle a second input field.
public class TwoFactorAuthenticationFilter extends UsernamePasswordAuthenticationFilter
{
private String extraParameter = "extra";
private String delimiter = ":";
//getters and setters
#Override
protected String obtainUsername(HttpServletRequest request)
{
String username = request.getParameter(getUsernameParameter());
String extraInput = request.getParameter(getExtraParameter());
String combinedUsername = username + getDelimiter() + extraInput;
return combinedUsername;
}
}
obtainUsername() This method is to retrieve the username and “extra” input field from the HttpServletRequest object that’s passed in.
It then concatenates these two values into one string, separating them by the delimiter string (a colon, by default).
It then returns this combined string. The parameter from which the “extra” input field is read is extra by default.
UserDetailsService should look like this:
#Override
public UserDetails loadUserByUsername(String input) throws UsernameNotFoundException, DataAccessException
{
String[] split = input.split(":");
if(split.length < 2)
{
throw new UsernameNotFoundException("Must specify both username and corporate domain");
}
String username = split[0];
String domain = split[1];
User user = userDao.findByUsernameAndDomain(username, domain);
if(user == null)
{
throw new UsernameNotFoundException("Invalid username or corporate domain");
}
return user;
}
Split the given username into its two components: the username and the extra field. In this example, the extra field is the user’s corporate domain.
Once we have the username and the domain, we can use our DAO to find the matching user.
Last Puzzle:
TwoFactorAuthenticationFilter:
<http use-expressions="true" auto-config="false" entry-point-ref="loginUrlAuthenticationEntryPoint">
<intercept-url pattern="/secured" access="isAuthenticated()" />
<intercept-url pattern="/**" access="permitAll" />
<custom-filter position="FORM_LOGIN_FILTER" ref="twoFactorAuthenticationFilter" />
<logout logout-url="/logout" />
</http>
<authentication-manager alias="authenticationManager">
<authentication-provider ref="authenticationProvider" />
</authentication-manager>
<beans:bean id="authenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<beans:property name="passwordEncoder">
<beans:bean class="org.springframework.security.authentication.encoding.ShaPasswordEncoder" />
</beans:property>
<beans:property name="userDetailsService" ref="userService" />
</beans:bean>
<beans:bean id="userService" class="com.awnry.springexample.UserDetailsServiceImpl" />
<beans:bean id="loginUrlAuthenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<beans:property name="loginFormUrl" value="/login" />
</beans:bean>
<beans:bean id="twoFactorAuthenticationFilter" class="com.awnry.springexample.TwoFactorAuthenticationFilter">
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="authenticationFailureHandler" ref="failureHandler" />
<beans:property name="authenticationSuccessHandler" ref="successHandler" />
<beans:property name="filterProcessesUrl" value="/processLogin" />
<beans:property name="postOnly" value="true" />
<beans:property name="extraParameter" value="domain" />
</beans:bean>
<beans:bean id="successHandler" class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
<beans:property name="defaultTargetUrl" value="/login" />
</beans:bean>
<beans:bean id="failureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<beans:property name="defaultFailureUrl" value="/login?login_error=true" />
</beans:bean>
In twoFactorAuthenticationFilter bean definition, we set the extraParameter property to “domain” which is the name of the input field to use in our login form.
EDIT:
Have a look into constructors of User class.
If you don't know what a granted authority get into a look over this below link:
http://docs.spring.io/autorepo/docs/spring-security/3.2.1.RELEASE/apidocs/org/springframework/security/core/GrantedAuthority.html
Your coding gives a different mode applicable only for normal username and password. My code works for n factor authentication. Try switch over to my code if any problem keep on persists.
I'm very concious that this post has undergone 28 edits, so I may have missed some context. I'm also concious that you've amalgamated some code from the other answers into your question and that the problem has been somewhat "turned on its head" from "why won't a valid user authenticate?" to "why does every user authenticate?".
Current problem.
However, as written, your CustomAuthenticationProvider.authenticate() method will always return an Authentication object that returns auth.isAuthenticated() == true because you instantiate using this method which warns you about that very thing. Even if the collection you passed in as the third argument were empty, this would be the case. In fact, the collection always contains a GrantedAuthority for "registered", because pincodeEntered(name) always returns true. So, you need to correct your logic in those methods. authenticate() should return null if authentication is not successful.
Next steps
You've indicated in comments that what you want is a reference implementation of multi factor authentication. This is problematic - there is not necessarily agreement on what would constitute such a thing. For instance some would argue that multi factor should include a possession factor, rather than n knowledge factors on a single login page. It's also not really suited to an SO answer as it would need a blog post (or a series) - however generous the bounty.
There are working examples of multi factor authentication in spring on the web, here and here, for example. The latter I think you must have discovered as you appear to be using some of the code from there.
Making your CustomAuthenticationProvider work might take hours. Debugging might take even longer, as you have a mixture of methods in your example - it is not minimal. In particular, the TwoFactorAuthenticationFilter class is supposed to be used to intercept input on a request from the login page and concatenate the username and pin. In the example from the blog, this is set up in XML - you could add the security namespace to your business-config.xml and add those beans there for instance.
However, the SecurityConfig class and CustomAuthenticationProvider is a different method again.
Next, your project code references a j_security_check url, but that URL is not handled by anything. I am not sure of the intent behind that, or where it comes from. Finally, the MVC config for URL routing adds another element to the mix - one which I'm not familiar with.
I've played with your example for a while. There are too many mixed methods and too much complexity for me to fix quickly - maybe others can.
I strongly suggest that you start from the example in the blog exactly, then add the mvc config you want to over the top of that.
N.B. Setup for others trying to get the example to work
There were a couple of wrinkles in setting the project - it had an un-needed and unsatisfied dependency on javax.mail, you need to publish the maven dependencies to the server (in project->properties->deployment assembly) and you need to download and install adapters for the tomcat server if you don't already have it.
You also need to create the tables and columns in your database.
The easiest way to use java config for n-factor authentication is to start with a working example of single-factor authentication (username and password) that uses java config. Then you only have to make a few very minor changes: Assuming that you have a working single factor authentication app using java configuration, the steps are simply:
First, define layered roles, with one role for each factor. If you only have two factor authentication, keep your existing one role in the database, but then create a second role with full-access that you only assign at runtime. Thus, when the user logs in, they are logged in to the minimal role stored in the database, and that minimal role is only given access to one view, which is a form allowing them to enter a pin code that your controller just sent them via text or email or some other method. These layered roles get defined in SecurityConfig.java, as follows:
#Configuration
#EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/getpin")
.usernameParameter("j_username")
.passwordParameter("j_password")
.loginProcessingUrl("/j_spring_security_check")
.failureUrl("/login")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.and()
.authorizeRequests()
.antMatchers("/getpin").hasAuthority("get_pin")
.antMatchers("/securemain/**").hasAuthority("full_access")
.antMatchers("/j_spring_security_check").permitAll()
.and()
.userDetailsService(userDetailsService);
}
}
Second, add code that upgrades the user's role to full-access upon successful entry of the correct pin code to the controller code that handles the pin code entry form POST. The code to manually assign full access in the controller is:
Role rl2 = new Role();rl2.setRole("full-access");//Don't save this one because we will manually assign it on login.
Set<Role> rls = new HashSet<Role>();
rls.add(rl2);
CustomUserDetailsService user = new CustomUserDetailsService(appService);
Authentication authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities(rls));
SecurityContextHolder.getContext().setAuthentication(authentication);
return "redirect:/securemain";
You can add as many layers as you want to after /getpin. You can also support multiple authorization roles and make it as complicated as you want to. But this answer gives the simplest way to get it running with java config.
Related
I need your help on resolution for the following Exception:
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sun Apr 19 09:45:23 CEST 2020
There was an unexpected error (type=Internal Server Error, status=500).
No plugin found for delimiter text/html;charset=UTF-8! Registered plugins: [org.springframework.hateoas.mediatype.hal.HalLinkDiscoverer#665cd8b1].
java.lang.IllegalArgumentException: No plugin found for delimiter text/html;charset=UTF-8! Registered plugins: [org.springframework.hateoas.mediatype.hal.HalLinkDiscoverer#665cd8b1].
at org.springframework.plugin.core.SimplePluginRegistry.lambda$getRequiredPluginFor$2(SimplePluginRegistry.java:140)
...
I implemented two WebServices using SpringBoot:
a Data Service called ProductMicroService and
an Interface Service called ProductMicroServiceClient for consumers at front-ends.
The ProductMicroService is implemented on basis of JPA and uses as persistency back-end a SQL database (in my sample a MariaDB).
The Controller provides RESTful API end points in JSON with media support (HATEOAS).
The ProductMicroServiceClient consumes the API end points from ProductMicroService and provides a RESTful API for front-ends also with media support (HATEOAS).
In my samples the client is a WebBrowser running some simple Thymleaf Templates.
When running the pure ProductMicroService and ProductMicroServiceClient implementations on my local machine everything goes well.
Also after introducing security based on JDBC for the ProductMicroServiceClient everything runs fine including access restrictions on the API end-points.
The User and Authority tables are persisted at the same MariaDB as for the Data Service.
But after introducing SecurityService for the ProductMicroService I receive the Exception above after successful authentication (standard login page from SpringBoot).
I am using OpenJDK.
I couldn’t find any direction for a solution when searching in internet.
Some relevant code
for ProductMicroServiceClient:
———— pom.xml ———————————————————————————————————
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>eu.mydomain</groupId>
<artifactId>ProductMicroServiceClient</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ProductMicroServiceClient</name>
<description>Learn Spring Boot Configuration for SpringData</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
——— eu.mydomain.product.security.EncodingFilter.java ————————————————
package eu.mydomain.product.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.web.filter.GenericFilterBean;
public class EncodingFilter extends GenericFilterBean {
#Override
public void doFilter( ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
chain.doFilter( request, response);
}
}
——— eu.mydomain.product.security.WebSecurityConfig.java —————————————
package eu.mydomain.product.security;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure( HttpSecurity http) throws Exception {
http.addFilterBefore( new EncodingFilter(), ChannelProcessingFilter.class);
http
.httpBasic()
.and()
.authorizeRequests()
.antMatchers("/product/**","/products", "/products/**").hasRole("USER")
.antMatchers("/create-products", "/products4create", "/products4edit/**","/update-products/**","/products4edit/**","/delete-products/**","/products4delete/**").hasRole("ADMIN")
// .antMatchers("/","/**").permitAll()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
// .and().httpBasic()
;
#Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder(16);
}
#Autowired DataSource dataSource;
public void configure( AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.passwordEncoder( encoder() )
.usersByUsernameQuery( "SELECT username, password, enabled FROM users WHERE username = ?")
.authoritiesByUsernameQuery( "SELECT username, authority FROM authorities WHERE username = ?")
.dataSource( dataSource);
}
}
As said until now everything works as expected.
For the ProductMicroService
I introduced a database View V_PRODUCT_USERS that provides relevant user authorities from the users and authorities tables and implemented a ProductUser Entity, IProductUserRepository and UserDetailService.
——— eu.mydomain.product.domain.ProductUser.java —————————————
package eu.mydomain.product.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
#Entity
#Table( name="v_product_users")
public class ProductUser {
/*
* create view if not exists v_product_users as select u.is id, u.username username, u.'password' 'password', a.authority rolefrom users u, authorities a where u.username = a.username;
* commit;
*/
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(nullable = false, updatable = false)
private Long id;
#Column(nullable = false, unique = true, updatable = false)
private String username;
#Column(nullable = false, updatable = false)
private String password;
#Column(nullable = false, updatable = false)
private String role;
public ProductUser()
{}
public ProductUser(Long id, String username, String password, String role)
{
super();
this.id = id;
this.username = username;
this.password = password;
this.role = role;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
——— eu.mydomain.product.repository.IProductUserRepository.java ——————————
package eu.mydomain.product.repository;
import org.springframework.data.repository.CrudRepository;
import eu.mydomain.product.domain.ProductUser;
public interface IProductUserRepository extends CrudRepository< ProductUser, Long> {
ProductUser findByUsername( String username);
}
——— eu.mydomain.product.service.UserDetailServiceImpl.java ——————————
package eu.mydomain.product.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import eu.mydomain.product.domain.ProductUser;
import eu.mydomain.product.repository.IProductUserRepository;
#Service
public class UserDetailsServiceImpl implements UserDetailsService {
#Autowired
private IProductUserRepository userRepository;
#Override
public UserDetails loadUserByUsername( String username) throws UsernameNotFoundException {
ProductUser productUser = userRepository.findByUsername( username);
return new User(
username,
productUser.getPassword(),
AuthorityUtils.createAuthorityList( productUser.getRole()) // #TODO: comma separated list of all roles granted
);
}
}
Finally, I introduced for security an EncodingFilter and a WebSecurityConfig:
——— eu.mydomain.product.security.EncodingFilter.java ————————————————
package eu.mydomain.product.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.web.filter.GenericFilterBean;
public class EncodingFilter extends GenericFilterBean {
#Override
public void doFilter( ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
chain.doFilter( request, response);
}
}
——— eu.mydomain.product.security.WebSecurityConfig.java ————————————————
package eu.mydomain.product.security;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import eu.nydomain.product.service.UserDetailsServiceImpl;
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsServiceImpl userDetailsServiceImpl;
#Override
protected void configure( HttpSecurity http) throws Exception {
/*
* For Tyhmleaf Templates add <meta>-tag to the HTML-Header for the CSRF Token
*
* <meta name="_csrf" th:content="${_csrf.token}" />
* <meta name="_csrf_header" th:content="${_csrf.headerName}" />
*/
http.addFilterBefore( new EncodingFilter(), ChannelProcessingFilter.class);
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic()
;
}
#Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder(16);
}
#Autowired
#Override
public void configure( AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService( userDetailsServiceImpl)
.passwordEncoder( encoder() );
}
}
Now, after introducing Security to the Data Service I get the Exception after successful authentication by SpringBoot Security and while before loading the following page.
<!DOCTYPE html5>
<html>
<head>
<title>Spring Boot Introduction Sample - Products</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="_csrf" th:content="${_csrf.token}" />
<meta name="_csrf_header" th:content="${_csrf.headerName}" />
<!-- <link rel="stylesheet" type="text/css" media="all" href="../css/my.css" data-th-href="#{/css/my.css}" /> -->
</head>
<body>
<p>This is a <i>Product</i> database - as a Client for the Spring Boot Test Sample for RESTful Product services</p>
<table>
<tr>
<td>
<form action="#" th:action="#{/products}" th:object="${product}" method="get">
<input type="submit" value="Show All Products" />
</form>
</td>
<td>
<form action="#" th:action="#{/products4create}" th:object="${product}" method="get">
<input type="submit" value="Create a new Product" />
</form>
</td>
</tr>
</table>
<hr/>
<p>All Products:</p>
<table>
<thead>
<tr>
<th>Id</th>
<th>Product id</th>
<th>Product Name</th>
<th>Product Type</th>
<th>Description</th>
<th>Brand</th>
<th colspan="2">Action</th>
</tr>
</thead>
<tbody>
<!-- <tr th:each="product, rowStat: ${products}" th:style="${rowStat.odd} ? 'color: gray' : 'color: blue;'"> -->
<tr th:each="product : ${products}">
<td th:text="${product.content.id}">1</td>
<td th:text="${product.content.prodId}">Prod Id</td>
<td th:text="${product.content.name}">Name</td>
<td th:text="${product.content.type}">Type</td>
<td th:text="${product.content.description}">Description</td>
<td th:text="${product.content.brand}">Brand</td>
<td><a th:href="#{|/products4edit/${product.content.id}|}">Edit</a></td>
<td><a th:href="#{|/products4delete/${product.content.id}|}">Delete</a></td>
</tr>
</tbody>
</table>
</body>
</html>
the mention Exception at the top of this question.
What I tried already:
Put the different UTF-8 configurations to files, pom.xml etc.
Changed database fields to CHARSET utf8 COLLATE utf8_bin and also CHARSET utf8mb4 COLLATE utf8mb4_bin
I implemented my personal login page (and related handling)
I identified that the ProductMicroServiceClient after authentication works until calling the ProductMicroService API end-point via:
...
CollectionModel<EntityModel<Product>> productResources = myTraverson
.follow( "/products") // JSON element
.toObject(new ParameterizedTypeReference<CollectionModel<EntityModel<Product>>>() {});
...
which doesn't enter the API end-point:
#GetMapping(value = "/products", produces = "application/hal+json")
public CollectionModel<ProductRepresentationModel> findAll() {
List<Product> products = new ArrayList<>();
productRepository.findAll().forEach( (p -> products.add(p)));
CollectionModel<ProductRepresentationModel> productsModelList = new ProductRepresentationAssembler().toCollectionModel(products);
productsModelList.add( WebMvcLinkBuilder.linkTo( WebMvcLinkBuilder.methodOn(ProductController.class).findAll()).withRel("/products"));
return productsModelList;
}
I introduced at the ProductMicroService an access denied handler
#Override
protected void configure( HttpSecurity http) throws Exception {
/*
* For Tyhmleaf Templates add <meta>-tag to the HTML-Header for the CSRF Token
*
* <meta name="_csrf" th:content="${_csrf.token}" />
* <meta name="_csrf_header" th:content="${_csrf.headerName}" />
*/
http.addFilterBefore( new EncodingFilter(), ChannelProcessingFilter.class);
http
// .httpBasic()
// .and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.and()
.httpBasic()
;
}
I switched for debugging to call the the API end-points of the ProductMicrosService using Postman App with Basic-Authentication.
While debugging I identified that
(a) the correct user, (encrypted) password and role(s) get used for authentication
(b) no access denied handler gets called
(c) Method Call for the API end point ( findAll() method ) doesn't get entered
(d) Response Header contains "HttP 401 - Unauthorized"
(e) Response in Postman is empty
I assume now that the Exception above gets thrown as result of receiving an empty response and HttP 401 Unauthorizedfrom the API call.
Question to me now:
Is there something missing or wrong with the security configuration?
Why don't I get an Unauthorized Exception?
The issue with the reported Exception could be resolved now.
After certain changes in configuration of the Security the Exception has been resolved.
Calling the Data Service API End Point (ProductMicroService) from Postman App works now and provides as response the expected JSON Object.
Calling the Data Service API End Point (ProductMicroService) from a Interface Service (ProdctMicroServiceClient) throws another Exception:
401 : [{"timestamp":"2020-04-24T19:22:48.851+0000","status":401,"error":"Unauthorized","message":"Unauthorized","path":"/my-products/products"}]
org.springframework.web.client.HttpClientErrorException$Unauthorized: 401 : [{"timestamp":"2020-04-24T19:22:48.851+0000","status":401,"error":"Unauthorized","message":"Unauthorized","path":"/my-products/products"}]
I am now looking after JWT implementation (works well at the ProductMicroService) and OAuth2 (learning in progress).
What I want to do is read a http-parameter on my login page, e.g. login.html?param=value, and then pass value to my AuthenticationProvider. My idea was to somehow save value in a hidden parameter, but I still don't know how to pass it on.
Is this possible? How do I go about to do it?
Edit: Following Sanath's advice and after doing some reading I was finally able to solve the problem -- look below if you're interested in how I did it.
I did the following and finally it worked like a charm.
In my bean configuration file I had to enter:
<http auto-config="true>
...
<form-login ... authentication-details-source-ref="myWebAuthDetSource"/>
...
</http>
<beans:bean id="myWebAuthDetSource" class="com.my.pack.age.MyWebAuthDetSource"/>
...
then I've set up my WebAuthenticationDetailsSource implementation like this:
public class MyWebAuthDetSource implements AuthenticationDetailsSource<HttpServletRequest, MyWebAuthDets> {
public MyWebAuthDetSource buildDetails (HttpServletRequest context) {
return new MyWebAuthDets(context);
}
}
then I've got my AuthenticationDetails implementation like:
public class MyWebAuthDets extends WebAuthenticationDetails {
private final String parameter;
public MyWebAuthDets(HttpServletRequest request) {
super(request);
this.parameter = request.getParameter("paramId");
}
}
And then I overlook the most simple fact, that the authentication process processes only what it gets on submit from the login form, so I simply had to add the following in login.jsp:
...
<form id="j_spring_security_check" ... >
<input type="hidden" name="paramID" value="${param['paramID']}" />
...
</form>
...
And that was all. Now I can add an authenticationProvider and get my parameter with .getDetails() from the authenticationToken.
Thanks again #Sanath.
We secure our REST services (for server to server communication, no user involved) with Spring Security OAuth2. However when one tries to access a protected resource in a browser, it will show:
<oauth>
<error_description>
An Authentication object was not found in the SecurityContext
</error_description>
<error>unauthorized</error>
</oauth>
We want this to be a custom page of our own choosing. Is there a way?
Setting the access-denied-page won't do. For one it requires the definition of a login page which we don't have as this is a pure server to server communication. For another this attribute is supposedly deprecated since Spring 3.0.. or something.
Anyway.. Debugged my way into the OAuth Error Handling. And found that the response seems to somehow get enriched with the information I see on the error page. Apparently no page rendering at all is done so it looks like there is no error page to replace..?!
At least we want to hide the fact that we use OAuth and just display a basic "Denied" text if we can't have a "real" page.. So maybe I'll have to extend the spring security handler or add a custom filter to modify the response?!
Maybe a redirect to our error page?
Thanks!
Edit
For our current setup check my other SO question here
I had to remove the oauth detail too and my solution was to implement my own OAuth2ExceptionRenderer
package org.some.nice.code;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.oauth2.provider.error.OAuth2ExceptionRenderer;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
public class HeaderOnlyOAuth2ExceptionRenderer implements OAuth2ExceptionRenderer
{
private final Log logger = LogFactory
.getLog(MyOAuth2ExceptionRenderer.class);
public void handleHttpEntityResponse(HttpEntity<?> responseEntity,
ServletWebRequest webRequest) throws Exception
{
if (responseEntity == null)
{
return;
}
HttpInputMessage inputMessage = createHttpInputMessage(webRequest);
HttpOutputMessage outputMessage = createHttpOutputMessage(webRequest);
logger.info("filtering headers only...");
if (responseEntity instanceof ResponseEntity
&& outputMessage instanceof ServerHttpResponse)
{
((ServerHttpResponse) outputMessage)
.setStatusCode(((ResponseEntity<?>) responseEntity)
.getStatusCode());
}
HttpHeaders entityHeaders = responseEntity.getHeaders();
if (!entityHeaders.isEmpty())
{
outputMessage.getHeaders().putAll(entityHeaders);
}
}
private HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest)
throws Exception
{
HttpServletRequest servletRequest = webRequest
.getNativeRequest(HttpServletRequest.class);
return new ServletServerHttpRequest(servletRequest);
}
private HttpOutputMessage createHttpOutputMessage(
NativeWebRequest webRequest) throws Exception
{
HttpServletResponse servletResponse = (HttpServletResponse) webRequest
.getNativeResponse();
return new ServletServerHttpResponse(servletResponse);
}
}
Then you will have to reference it within your spring context
<bean id="oauthAuthenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
<property name="realmName" value="theRealm" />
<property name="exceptionRenderer" ref="headerOnlyExceptionRender" />
</bean>
<bean id="clientAuthenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
<property name="realmName" value="theRealm/client" />
<property name="typeName" value="Basic" />
<property name="exceptionRenderer" ref="headerOnlyExceptionRender" />
</bean>
<bean id="oauthAccessDeniedHandler" class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler">
<property name="exceptionRenderer" ref="headerOnlyExceptionRender" />
</bean>
<bean id="headerOnlyExceptionRender" class="org.some.nice.code.HeaderOnlyOAuth2ExceptionRenderer"/>
Hope it helps.
This is a cross post. I've also posted the same question to spring forums. http://forum.springsource.org/showthread.php?128579-Database-driven-Controller-Mapping
Hi I'm trying to do database driven controller mappings so that they can change at runtime.
So far what I have is as follows.
Custom Handler Adaptor which can always be optimized later.
#Component
public class DatabasePageUrlHandlerMapping extends AbstractUrlHandlerMapping implements PriorityOrdered {
#Override
protected Object getHandlerInternal(HttpServletRequest request)
throws Exception {
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
List<Page> pages = Page.findAllPages();
for (Page page : pages) {
if (lookupPath.equals(page.getSeoPath())) {
Object handler = getApplicationContext().getBean("_pageViewController");
return new HandlerExecutionChain(handler);
}
}
return super.getHandlerInternal(request);
}
}
my webmvc-config looks as follows (the relevant part)
Code:
<context:component-scan base-package="com.artiststogether"
use-default-filters="false">
<context:include-filter expression="org.springframework.stereotype.Controller"
type="annotation" />
</context:component-scan>
<!-- If I don't put an order into this it doesn't fail over to the implementation why? -->
<bean class="com.artiststogether.web.DatabasePageUrlHandlerMapping" p:order="-1" />
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>
This appears to be picking up the correct controller. However I recieve an error when going to a database defined path (such as "/a")
java.lang.NullPointerException
at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter$ServletHandlerMethodResolver.useTypeLevelMapping(AnnotationMethodHandlerAdapter.java:675)
at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter$ServletHandlerMethodResolver.resolveHandlerMethod(AnnotationMethodHandlerAdapter.java:585)
at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.invokeHandlerMethod(AnnotationMethodHandlerAdapter.java:431)
at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.handle(AnnotationMethodHandlerAdapter.java:424)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:900)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:827)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:882)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:778)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:621)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:722)
....
Do I need to define a custom annotation handler?
To be honest this whole process seems more difficult than it should. I want 1 controller to handle all requests to an externally defined url path is this the correct way of going arround it.
I'd also like to pass in the object which matched into the controller if this is possible rather than doing a fresh lookup in the controller. This will basically form my model for the view.
Any advise on how to get this working?
EDIT
For the record the NPE is here
private boolean useTypeLevelMapping(HttpServletRequest request) {
if (!hasTypeLevelMapping() || ObjectUtils.isEmpty(getTypeLevelMapping().value())) {
return false;
}
return (Boolean) request.getAttribute(HandlerMapping.INTROSPECT_TYPE_LEVEL_MAPPING);
}
Another Edit
version numbers from the pom.xml
<properties>
<aspectj.version>1.6.12</aspectj.version>
<java.version>6</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<roo.version>1.2.1.RELEASE</roo.version>
<slf4j.version>1.6.4</slf4j.version>
<spring.version>3.1.0.RELEASE</spring.version>
<spring-security.version>3.1.0.RELEASE</spring-security.version>
</properties>
I've answered the question myself below but I'm still intrested in people weighing in on the correct way to do this.
Apparently from the lack of answers to the contrary here and on the spring forums it appears that there is no simpler way to do this within the spring framework.
I have however managed to get it working and I've shared a project at github that can be built with maven that add 4 classes to ease with the process of dynamically adding class. This project can be found at https://github.com/Athas1980/MvcBackingBean. I'll also share another project to prove that it works.
Thanks to Marten Deinum, and Rossen Stoyanchev
For those interested in how to achieve this yourselves you need to do the following
Implement an instance of HandlerMapper This gives you the mapping between a controller class and the url that you are mapping to.
// Copyright 2012 Wesley Acheson
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.wesley_acheson.spring;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.PriorityOrdered;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
/**
* A Handler mapper that delegates to a {#link UrlBackingBeanMapper} to know
* whether it should match a url. If it does match a url then it adds the bean
* which matches the url to the request.
*
* #author Wesley Acheson
*
*/
public class BackingBeanUrlHandlerMapper extends AbstractUrlHandlerMapping
implements PriorityOrdered {
private UrlBackingBeanMapper<?> urlMapper;
/**
*
* #param urlMapper
* The bean which matches urls with other beans.
*/
public void setUrlMapper(UrlBackingBeanMapper<?> urlMapper) {
this.urlMapper = urlMapper;
}
protected UrlBackingBeanMapper<?> getUrlMapper() {
return urlMapper;
}
public static final String BACKING_BEAN_ATTRIBUTE = BackingBeanUrlHandlerMapper.class
.getName() + ".backingBean";
/**
* The controller which control will be passed to if there is any beans
* matching in #{link {#link #setUrlMapper(UrlBackingBeanMapper)}.
*/
public Object controller;
/**
* #param controller
* <p>
* The controller which control will be passed to if there is any
* beans matching in #{link
* {#link #setUrlMapper(UrlBackingBeanMapper)}.
*/
public void setController(Object controller) {
this.controller = controller;
}
/*
* (non-Javadoc)
*
* #see org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#
* lookupHandler(java.lang.String, javax.servlet.http.HttpServletRequest)
*/
#Override
protected Object lookupHandler(String urlPath, HttpServletRequest request)
throws Exception {
if (urlMapper.isPathMapped(urlPath)) {
Object bean = urlMapper.retrieveBackingBean(urlPath);
return buildChain(bean, urlPath);
}
return super.lookupHandler(urlPath, request);
}
/**
* Builds a handler execution chain that contains both a path exposing
* handler and a backing bean exposing handler.
*
* #param bean
* The object to be wrapped in the handler execution chain.
* #param urlPath
* The path which matched. In this case the full path.
* #return The handler execution chain that contains the backing bean.
*
* #see {#link AbstractUrlHandlerMapping#buildPathExposingHandler(Object, String, String, java.util.Map)}
*
*/
protected HandlerExecutionChain buildChain(Object bean, String urlPath) {
// I don't know why but the super class declares object but actually
// returns handlerExecution chain.
HandlerExecutionChain chain = (HandlerExecutionChain) buildPathExposingHandler(
controller, urlPath, urlPath, null);
addBackingBeanInteceptor(chain, bean);
return chain;
}
/**
* Adds an inteceptor which adds the backing bean into the request to an
* existing HandlerExecutionChain.
*
* #param chain
* The chain which the backing bean is being added to.
* #param bean
* The object to pass through to the controller.
*/
protected void addBackingBeanInteceptor(HandlerExecutionChain chain,
Object bean) {
chain.addInterceptor(new BackingBeanExposingInteceptor(bean));
}
/**
* An Interceptor which adds a bean to a request for later consumption by a
* controller.
*
* #author Wesley Acheson
*
*/
protected class BackingBeanExposingInteceptor extends
HandlerInterceptorAdapter {
private Object backingBean;
/**
* #param backingBean
* the bean which is passed through to the controller.
*/
public BackingBeanExposingInteceptor(Object backingBean) {
this.backingBean = backingBean;
}
#Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
request.setAttribute(BACKING_BEAN_ATTRIBUTE, backingBean);
return true;
}
}
}
Implement a HandlerMethodArgumentResolver to fetch the value out of the session. (assuming you are intrested in setting in the session)
// Copyright 2012 Wesley Acheson
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.wesley_acheson.spring;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Resolves method parameters which are annotated with {#link BackingBean}.
*
* <b>Note:</b> Only works for Http requests.
*
* #author Wesley Acheson
*
*/
public class BackingBeanValueResolver implements HandlerMethodArgumentResolver {
/**
* Constructor.
*/
public BackingBeanValueResolver() {
}
/**
* Implementation of
* {#link HandlerMethodArgumentResolver#supportsParameter(MethodParameter)}
* that returns true if the method parameter is annotatated with
* {#link BackingBean}.
*/
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(BackingBean.class);
}
#Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
return webRequest.getNativeRequest(HttpServletRequest.class)
.getAttribute(
BackingBeanUrlHandlerMapper.BACKING_BEAN_ATTRIBUTE);
}
}
Implement a custom WebArgumentResolver to fetch the instance of the
Bean passed. Set this as a property to an instance of
AnnotationMethodHandler.
/**
*
*/
package com.wesley_acheson.spring;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.context.request.NativeWebRequest;
/**
* #author Wesley Acheson
*
*/
public class BackingBeanArgumentResolver implements WebArgumentResolver {
/* (non-Javadoc)
* #see org.springframework.web.bind.support.WebArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.context.request.NativeWebRequest)
*/
#Override
public Object resolveArgument(MethodParameter methodParameter,
NativeWebRequest webRequest) throws Exception {
if (methodParameter.hasParameterAnnotation(BackingBean.class))
{
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
Object parameter = request.getAttribute(BackingBeanUrlHandlerMapper.BACKING_BEAN_ATTRIBUTE);
if (parameter == null)
{
return UNRESOLVED;
}
if (methodParameter.getParameterType().isAssignableFrom(parameter.getClass()))
{
return parameter;
}
}
return UNRESOLVED;
}
}
I also created a BackingBean annotation and an interface to pass to my handler addapters as I felt they were easier.
Create your controller. If you use my code you will want to inject the argument using the #BackingBean annotation. The request mapping on the controller itself must not match any good urls (This is because we bypass this step with our handler adapter and we don't want the default annotation handler to pick it up.
Wire up everything in spring. Here's an example file from my working example project.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
<!-- The controllers are autodetected POJOs labeled with the #Controller
annotation. -->
<context:component-scan base-package="com.wesley_acheson"
use-default-filters="false">
<context:include-filter expression="org.springframework.stereotype.Controller"
type="annotation" />
</context:component-scan>
<bean class="com.wesley_acheson.spring.BackingBeanUrlHandlerMapper"
p:order="-1">
<property name="controller">
<!-- A simple example controller. -->
<bean class="com.wesley_acheson.example.PageController" />
</property>
<!-- A simple example mapper. -->
<property name="urlMapper">
<bean class="com.wesley_acheson.example.PageBeanUrlMapper" />
</property>
</bean>
<util:map id="pages">
<entry key="/testPage1">
<bean class="com.wesley_acheson.example.Page">
<property name="title" value="Test Page 1 title" />
<property name="contents"
value="This is the first test page.<br /> It's only purpose is to check
if <b>BackingBeans</b> work." />
</bean>
</entry>
<entry key="/test/nested">
<bean class="com.wesley_acheson.example.Page">
<property name="title" value="Nested Path" />
<property name="contents"
value="This is another test page its purpose is to ensure nested pages work." />
</bean>
</entry>
</util:map>
<bean
class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="customArgumentResolver">
<bean class="com.wesley_acheson.spring.BackingBeanArgumentResolver" />
</property>
</bean>
<!-- Turns on support for mapping requests to Spring MVC #Controller methods
Also registers default Formatters and Validators for use across all #Controllers -->
<mvc:annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving
up static resources -->
<mvc:resources location="/, classpath:/META-INF/web-resources/"
mapping="/resources/**" />
<!-- Allows for mapping the DispatcherServlet to "/" by forwarding static
resource requests to the container's default Servlet -->
<mvc:default-servlet-handler />
</beans>
Just to get over this specific issue, let me recommend one way out for now -
Create your own handlerAdapter internally composing the AnnotationMethodHandlerAdapter:
public DBAnnotationMethodHandlerAdapter implements HandlerAdapter,{
private AnnotationHandlerAdapter target;
#Override
public boolean supports(Object handler) {
return this.target.supports(handler);
}
#Override
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
request.setAttribute(HandlerMapping.INTROSPECT_TYPE_LEVEL_MAPPING, true);
return this.target.handle(request, response, handler);
}
public void setTarget(AnnotationHandlerAdapter target){
this.target = target;
}
}
<bean class="mypkg.DBAnnotationMethodHandlerAdapter">
<property name="target">
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>
</property>
</bean>
This should solve the current issue, but you will likely encounter other issues
I succesfully created a project using seam credentials for authentication, but right now some requirements has change (as usual) and i need authenticate the user automatic. See example bellow:
user call the page: http://server:port/project/index.jsp?parameter=xyz. This page has to call a seam component and get the parameter to populate credentials.username, but it's not working as expected.
my pages.xml:
<page view-id="*" action="#{authenticator.authenticate}" >
<rewrite pattern="/index/{cpf}" />
<rewrite pattern="/home/{cpf}" />
<param name="#{credentials.username}" value="123456" />
<param name="authenticator.teste" value="#{param['cpf']}" />
<param name="cpf" value="#{param['cpf']}" />
<param name="token" value="#{param['cpf']}" />
<navigation >
<rule if-outcome="home">
<redirect view-id="/home.xhtml" />
</rule>
<rule if-outcome="index">
<redirect view-id="#{authenticator.authenticate}" include-page-params="true" />
</rule>
</navigation>
</page>
my authenticatorBean (there is a lot of tests here, i tried everything):
#Stateless
#Name("authenticator")
public class AuthenticatorBean implements Authenticator {
#Out String token;
#Out String cpf;
#Out String xyz;
#Out String teste;
#Logger private Log log;
#In EntityManager entityManager;
#In Identity identity;
#In Credentials credentials;
public boolean authenticate() {
System.out.println(credentials.getUsername());
System.out.println(cpf);
System.out.println(xyz);
System.out.println(teste);
#SuppressWarnings("unused")
FacesContext fcx = FacesContext.getCurrentInstance();
String cpf = fcx.getExternalContext().getRequestContextPath();
String cpf2 = fcx.getExternalContext().getRequestParameterMap().get("token");
String cpf21 = fcx.getExternalContext().getRequestParameterMap().get("cpf");
String cpf22 = fcx.getExternalContext().getRequestParameterMap().get("xyz");
String cpf23 = fcx.getExternalContext().getRequestParameterMap().get("teste");
String cpf3 = fcx.getExternalContext().getInitParameter("cpf");
String cpf4 = fcx.getExternalContext().getRequestPathInfo();
String cpf5 = fcx.getExternalContext().getRequestServletPath();
Object cpf6 = fcx.getExternalContext().getRequest();
Object cpf7 = fcx.getExternalContext().getContext();
Object cpf8 = fcx.getExternalContext().getRequestMap();
Object cpf9 = fcx.getExternalContext().getRequestParameterNames();
log.info("authenticating {0}", credentials.getUsername());
Usuario usuario = (Usuario) entityManager.createQuery("select u from Usuario u where u.cpf = :cpf")
.setParameter("cpf", credentials.getUsername())
.getSingleResult();
log.info("authenticating {0}", credentials.getUsername());
if (usuario != null) {
identity.addRole("admin");
return Boolean.TRUE;
}
return false;
}
}
can someone help me ? i can't get the parameter in authenticatorBean
Thanks!
it is quite simply. See bellow:
PAGES.XML
<page view-id="/home.xhtml">
<action execute="#{identity.login}" if="#{not identity.loggedIn}" />
<param name="username" />
<navigation from-action="#{identity.login}">
<rule if="#{identity.loggedIn}">
<redirect view-id="/home.xhtml"/>
</rule>
<rule if="#{not identity.loggedIn}">
<redirect view-id="/error.xhtml"/>
</rule>
</navigation>
</page>
As you can see, i declare the parameter username and when the client input the url "http://host:port/project/home.seam?username=xyz" the parameter is gotten by username variable in seam component:
#Logger private Log log;
#In(required = true)
private String username;
#In(required=false, scope=ScopeType.SESSION)
Usuario usuario;
#In #Out
Identity identity;
#In #Out
Credentials credentials;
public boolean authenticate(){
log.info("authenticating {0}", username);
try {
UsuarioBean usuarioBean = (UsuarioBean) Component.getInstance(UsuarioBean.class, true);
usuario = usuarioBean.validaUsuario(username);
if (usuario == null){
throw new AuthorizationException("login failed");
}else{
credentials.setUsername(usuario.getNome());
return Boolean.TRUE;
}
}catch(Exception e){
log.error("falha na autenticação do usuario {0} : {1}", username, e.getCause().toString());
throw new AuthorizationException("login failed");
}
}
I hope this information be usefull for you. Good luck!
This Seam FAQ shows you how to get access to the HttpServletRequest object.
The HttpServletRequest object has a method named getRemoteUser().
This open source library, http://spnego.sourceforge.net, will allow your app server to perform integrated windows authentication/sso.
It is installed as a servlet filter.
In you web.xml file, define the filter so that it runs before any of the Seam stuff.
I'm facing a similar problem, though in my case there is no page:
Seam: login using external SSO application
I see you took a different route.
What I'm considering now is to make a JSF page that would take my parameter, save to form values through JS and autosubmit. it's a but ugly, but it should work.