This setup only works assuming you have on your main configuration class. By default Spring Security will provide a login form at /login but you can implement your own. I’ve also setup two users to test it
The Web MVC auto configuration feature of Spring Boot sets you up with a ViewResolver with blank suffix and prefix, hence you can place jsp in src/main/webapp/WEB-INF/test.jsp and have your controller return path to it
("/test")
public class TestController {
(method = GET)
public String test() {
return "/WEB-INF/test.jsp";
}
}
Spring Security comes handy when you need to secure your RESTful web service. Let’s give this a go! In this example I’ll create a REST service exposing a Cat entity. It simply have name and colour field
{
"name" : "Tom",
"colour" : "Black"
};
Creating the Service
Setup a new maven project with following pom.xml. We’ll leverage Spring Boot to simplify the work.
Setup a main configuration class. This substitutes the old-fashioned spring context xml. Spring Boot will do a lot of under-the-hood work to setup various bits and pieces using auto configuration
guration
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
Now let’s create our Cat JPA entity
public class Cat {
@Id
private long id;
private String name = "";
private String colour = "";
/* .. getters & setters omitted .. */
}
Since we included dependency to spring-boot-starter-data-jpa, Spring Boot will automatically set us up with JPA with Hibernate implementation. Also note that on pom.xml we declared a dependency to hsqldb which will automatically give us datasource to an embedded HSQL database.
Next, let’s create a Spring Data repository for Cat.
esource
public interface CatRepository extends PagingAndSortingRepository {
}
Again, spring-boot-starter-data-jpa will auto setup spring-data-jpa for me, and it will automatically provide an implementation of the repository interface at runtime.
Take a note at the esource annotation. This annotation tells Spring Data REST to expose the repository as REST service as well.
Testing the Service
Now you’re ready to run the app using the embedded tomcat container. Run following maven command
Next, configure Spring Security so /cats/** path are protected to users with ROLE_USER only. We also setup two in-memory users: bob with ROLE_USER and admin with ROLE_USER and ROLE_ADMIN
Don’t forget to recompile and restart the app. Now you can test querying the cats repository will be forbidden for non-authenticated (anonymous) users:
The -H option passes a HTTP header as part of the request. Also note that the username and password joined by colon (:) is encoded into Base64 resulting in the string Ym9iOmJvYjEyMw==. This is the string “bob:bob123″ in plain text. You can use an to try it yourself.
Also note that similar to web browser the server gave us a JSESSIONID Cookie:
Different Security Permission for Read / Update Operations
What if you want to give ROLE_USER read-only access and full read/write to ROLE_ADMIN?
This can be achieved by using Spring Data Repository event handler class. You can invoke custom code prior / after certain operations executed on the repository. Let’s see how this works.
First enable method security on our SecurityConfig class. Add the hodSecurity(securedEnabled = true) annotation to SecurityConfig class:
ty
hodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
}
Create a new CatEventHandler class annotated with Handler(Cat.class)
Handler(Cat.class)
("ROLE_ADMIN")
public class CatEventHandler {
private static final Logger LOG = LoggerFactory.getLogger(CatEventHandler.class);
e
public void handleBeforeSave(Cat c) {
LOG.debug("Before save " + c);
}
ate
public void handleBeforeCreate(Cat c) {
LOG.debug("Before create " + c);
}
kSave
public void handleBeforeLinkSave(Cat c) {
LOG.debug("Before link save " + c);
}
ete
public void handleBeforeDelete(Cat c) {
LOG.debug("Before delete " + c);
}
kDelete
public void handleBeforeLinkDelete(Cat c) {
LOG.debug("Before link delete " + c);
}
}
Few important things happening here:
All the .. methods will be invoked before the corresponding response is given to users
The (“ROLE_ADMIN”) annotation will ensure only users with ROLE_ADMIN can invoke all those methods in the class (although the method has nothing in it except a logging statement)
Let’s give this a try. Authenticate as bob:bob123 again and try creating a new Cat. Error will be presented:
I find it annoying to keep having to encode our username and password into Base64. I wanted a more simplified login such as posting u=bob&p=bob123 to http://localhost:8080/login.
Let’s try doing this.
Borrowing idea from , let’s create a LoginController
("/login")
public class LoginController {
private SecurityConfig securityConfig;
private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
private static final Logger LOG = LoggerFactory.getLogger(LoginController.class);
(method = RequestMethod.POST)
public String login(("u") String username,
("p") String password,
HttpServletRequest req) throws Exception {
// Force session creation so it's available to Spring Security post processor filter
req.getSession(true);
// Authenticate using AuthenticationManager configured on SecurityContext
AuthenticationManager authMgr = securityConfig.authenticationManagerBean();
UsernamePasswordAuthenticationToken authReq = new UsernamePasswordAuthenticationToken(username, password);
authReq.setDetails(authenticationDetailsSource.buildDetails(req));
Authentication authResp = authMgr.authenticate(authReq);
// If successful add the authentication response to context so the post processor filter
// can save it to session
SecurityContextHolder.getContext().setAuthentication(authResp);
return "Authentication successful";
}
...
}
Also create some exception handlers so login failure will produce 401 – Unauthorized HTTP status code.
Yay! Well done on making it this far. Hope you get a pretty decent looking REST API with minimal effort thanks to Spring. Don’t forget to always serve your API in HTTPS if deploying in production environment and secure it further with firewall if applicable.
As always you can browse the source code of this article on github:
Here’s my situation, I have myapp.firstdomain.com and myapp.seconddomain.com and I want my app to be styled / branded differently according to which domain used to access the app.
To keep things simple, each theme would have its own properties file with corresponding values. For example:
Next we have to register a ResourceBundleThemeSource. This tells spring where the theme specific properties file are located (in this instance we place them on themes/ folder on classpath root).
In Spring Boot, since I used automatic configuration for the web application context, additional customization has to be done via WebMvcConfigurerAdapter
public class WebMvcConfig extends WebMvcConfigurerAdapter {
public ThemeSource themeSource() {
ResourceBundleThemeSource themeSource = new ResourceBundleThemeSource();
themeSource.setBasenamePrefix("themes/");
return themeSource;
}
}
Next I need to create a ThemeResolver bean that can determine the theme based on domain names. In this instance since I used a reverse proxy, I used X-Forwarded-For HTTP header to determine the hostname, but you can simply use Host header otherwise.
This ThemeResolver keeps a map of which domain transaltes to what theme.
public class DomainNameThemeResolver extends AbstractThemeResolver {
private Map domainNameThemeMap = new HashMap();
public String resolveThemeName(HttpServletRequest request) {
String xFwdFor = request.getHeader("x-forwarded-for");
if(xFwdFor == null) {
return getDefaultThemeName();
}
String themeName = domainNameThemeMap.get(xFwdFor.trim().toLowerCase());
if(StringUtils.isBlank(themeName)) themeName = getDefaultThemeName();
return themeName;
}
public void setThemeName(HttpServletRequest request, HttpServletResponse response, String themeName) {
}
public Map getDomainNameThemeMap() {
return domainNameThemeMap;
}
public void setDomainNameThemeMap(Map domainNameThemeMap) {
this.domainNameThemeMap = domainNameThemeMap;
}
}
And finally, back to the WebMvcConfigurerAdapter, I also need to register this ThemeResolver bean
public ThemeResolver themeResolver() {
DomainNameThemeResolver themeResolver = new DomainNameThemeResolver();
themeResolver.setDefaultThemeName("firstdomain");
themeResolver.getDomainNameThemeMap().put("myapp.firstdomain.com", "firstdomain");
themeResolver.getDomainNameThemeMap().put("myapp.seconddomain.com", "seconddomain");
return themeResolver;
}
The theme-specific key value pairs can then be obtained on the thymeleaf view using the special #themes.code(...) syntax. Here’s an example:
Note that if you’re debugging your theme resolver, Spring won’t actually invoke the resolveTheme() method unless you are actually rendering a view that needs one (eg: a jsp view that contain tag).
There’s one thing that has been bothering me with Spring Boot’s embedded Tomcat container on Windows: it forks a new process which doesn’t die when the the parent process is terminated.
Here’s an example of the problem:
Run the app using maven mvn clean spring-boot:run command
Container starts up, do some testing, changed controller code and it’s now time to recompile again..
Clicked Eclipse’s stop and run button, and port has been used error came up. Now I have to go task manager, find the java process (among 3-4 other java processes) and kill it manually
This additional step really frustates me, identifying which java process you should kill isn’t always straight forward, you have to see the full command and see what it does.
Fortunately I found a workaround for this: scrap spring-boot-maven-plugin altogether and use the old-school tomcat7-maven-plugin. Here’s the step required to achieve this:
Remove spring-boot-maven-plugin configuration on pom.xml
Setup tomcat7-maven-plugin
org.apache.tomcat.maventomcat7-maven-plugin2.0
Instead of SpringApplication.run(Application.class, args), bootstrap Spring Boot using SpringBootServletInitializer instead, eg:
guration
public class Application extends SpringBootServletInitializer {
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
This is required such that SpringBoot can boot the container when tomcat starts up
Ok all good now, I started my app with mvn clean test tomcat7:run command, but wait.. there’s a weird exception being thrown:
Aug 22, 2014 4:01:46 PM org.apache.catalina.startup.ContextConfig parseWebXml
SEVERE: Parse error in application web.xml file at file:/C:/***/***/***/target/tomcat/conf/web.xml
org.xml.sax.SAXParseException; systemId: file:/C:/***/***/***/target/tomcat/conf/web.xml; lineNumber: 108; columnNumber: 15; Error at (108, 15) : org.apache.catalina.deploy.WebXml addServlet
at org.apache.tomcat.util.digester.Digester.createSAXException(Digester.java:2687)
at org.apache.tomcat.util.digester.Digester.createSAXException(Digester.java:2719)
at org.apache.tomcat.util.digester.Digester.endElement(Digester.java:1054)
at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.endElement(AbstractSAXParser.java:606)
...
Caused by: java.lang.NoSuchMethodException: org.apache.catalina.deploy.WebXml addServlet
at org.apache.tomcat.util.IntrospectionUtils.callMethod1(IntrospectionUtils.java:855)
at org.apache.tomcat.util.digester.SetNextRule.end(SetNextRule.java:201)
at org.apache.tomcat.util.digester.Digester.endElement(Digester.java:1051)
... 27 more
Took me quite a while to figure out, but it seems this problem is caused by a classpath conflict. My app uses spring-boot-started-web dependency which pulls spring-boot-starter-tomcat, which pulls various tomcat jars as dependency.
After a few trial and errors, I found the solution by marking spring-boot-started-tomcat as provided in the dependencies in pom.xml