Spring Boot + Spring Security + Thymeleaf example
A Spring Boot Thymeleaf example, uses Spring Security to protect path /admin and /user
Technologies used :
- Spring Boot 1.5.3.RELEASE
- Spring 4.3.8.RELEASE
- Spring Security 4.2.2
- Thymeleaf 2.1.5.RELEASE
- Thymeleaf extras Spring Security4 2.1.3
- Tomcat Embed 8.5.14
- Maven 3
- Java 8
1. Project Directory
2. Project Dependencies
Declares spring-boot-starter-security, it will get anything you need to develop a Spring Boot + Spring Security web application.
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <artifactId>spring-boot-web-spring-security</artifactId> <packaging>jar</packaging> <name>Spring Boot Web Spring Security</name> <description>Spring Boot Web Spring Security Example</description> <url>https://www.mkyong.com</url> <version>1.0</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.3.RELEASE</version> </parent> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- do you like thymeleaf? --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- optional, it brings userful tags to display spring security stuff --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> </dependency> <!-- hot swapping, disable cache for template, enable live reload --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <!-- Optional, for bootstrap --> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>3.3.7</version> </dependency> </dependencies> <build> <plugins> <!-- Package as an executable jar/war --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Display project dependencies :
$ mvn dependency:tree [INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building Spring Boot Web Spring Security 1.0 [INFO] ------------------------------------------------------------------------ [INFO] [INFO] --- maven-dependency-plugin:2.10:tree (default-cli) @ spring-boot-web-spring-security --- [INFO] org.springframework.boot:spring-boot-web-spring-security:jar:1.0 [INFO] +- org.springframework.boot:spring-boot-starter-thymeleaf:jar:1.5.3.RELEASE:compile [INFO] | +- org.springframework.boot:spring-boot-starter:jar:1.5.3.RELEASE:compile [INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:1.5.3.RELEASE:compile [INFO] | | | +- ch.qos.logback:logback-classic:jar:1.1.11:compile [INFO] | | | | \- ch.qos.logback:logback-core:jar:1.1.11:compile [INFO] | | | +- org.slf4j:jcl-over-slf4j:jar:1.7.25:compile [INFO] | | | +- org.slf4j:jul-to-slf4j:jar:1.7.25:compile [INFO] | | | \- org.slf4j:log4j-over-slf4j:jar:1.7.25:compile [INFO] | | +- org.springframework:spring-core:jar:4.3.8.RELEASE:compile [INFO] | | \- org.yaml:snakeyaml:jar:1.17:runtime [INFO] | +- org.springframework.boot:spring-boot-starter-web:jar:1.5.3.RELEASE:compile [INFO] | | +- org.springframework.boot:spring-boot-starter-tomcat:jar:1.5.3.RELEASE:compile [INFO] | | | +- org.apache.tomcat.embed:tomcat-embed-core:jar:8.5.14:compile [INFO] | | | +- org.apache.tomcat.embed:tomcat-embed-el:jar:8.5.14:compile [INFO] | | | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:8.5.14:compile [INFO] | | +- org.hibernate:hibernate-validator:jar:5.3.5.Final:compile [INFO] | | | +- javax.validation:validation-api:jar:1.1.0.Final:compile [INFO] | | | +- org.jboss.logging:jboss-logging:jar:3.3.1.Final:compile [INFO] | | | \- com.fasterxml:classmate:jar:1.3.3:compile [INFO] | | +- com.fasterxml.jackson.core:jackson-databind:jar:2.8.8:compile [INFO] | | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.8.0:compile [INFO] | | | \- com.fasterxml.jackson.core:jackson-core:jar:2.8.8:compile [INFO] | | +- org.springframework:spring-web:jar:4.3.8.RELEASE:compile [INFO] | | \- org.springframework:spring-webmvc:jar:4.3.8.RELEASE:compile [INFO] | +- org.thymeleaf:thymeleaf-spring4:jar:2.1.5.RELEASE:compile [INFO] | | \- org.thymeleaf:thymeleaf:jar:2.1.5.RELEASE:compile [INFO] | | +- ognl:ognl:jar:3.0.8:compile [INFO] | | +- org.javassist:javassist:jar:3.21.0-GA:compile [INFO] | | \- org.unbescape:unbescape:jar:1.1.0.RELEASE:compile [INFO] | \- nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:jar:1.4.0:compile [INFO] | \- org.codehaus.groovy:groovy:jar:2.4.10:compile [INFO] +- org.springframework.boot:spring-boot-starter-security:jar:1.5.3.RELEASE:compile [INFO] | +- org.springframework:spring-aop:jar:4.3.8.RELEASE:compile [INFO] | | \- org.springframework:spring-beans:jar:4.3.8.RELEASE:compile [INFO] | +- org.springframework.security:spring-security-config:jar:4.2.2.RELEASE:compile [INFO] | | +- org.springframework.security:spring-security-core:jar:4.2.2.RELEASE:compile [INFO] | | \- org.springframework:spring-context:jar:4.3.8.RELEASE:compile [INFO] | \- org.springframework.security:spring-security-web:jar:4.2.2.RELEASE:compile [INFO] | \- org.springframework:spring-expression:jar:4.3.8.RELEASE:compile [INFO] +- org.thymeleaf.extras:thymeleaf-extras-springsecurity4:jar:2.1.3.RELEASE:compile [INFO] | \- org.slf4j:slf4j-api:jar:1.7.25:compile [INFO] +- org.springframework.boot:spring-boot-devtools:jar:1.5.3.RELEASE:compile [INFO] | +- org.springframework.boot:spring-boot:jar:1.5.3.RELEASE:compile [INFO] | \- org.springframework.boot:spring-boot-autoconfigure:jar:1.5.3.RELEASE:compile [INFO] \- org.webjars:bootstrap:jar:3.3.7:compile [INFO] \- org.webjars:jquery:jar:1.11.1:compile [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.072 s [INFO] Finished at: 2017-05-04T10:13:05+08:00 [INFO] Final Memory: 19M/309M [INFO] ------------------------------------------------------------------------
3. Spring Security
3.1 Extends WebSecurityConfigurerAdapter, and defined the security rules in the configure method.
For user “admin” :
- Able to access /admin page
- Unable to access /user page, redirect to 403 access denied page.
For user “user” :
- able to access /user page
- unable to access /admin page, redirect to 403 access denied page.
package com.mkyong.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; 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.web.access.AccessDeniedHandler; @Configuration public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AccessDeniedHandler accessDeniedHandler; // roles admin allow to access /admin/** // roles user allow to access /user/** // custom 403 access denied handler @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/", "/home", "/about").permitAll() .antMatchers("/admin/**").hasAnyRole("ADMIN") .antMatchers("/user/**").hasAnyRole("USER") .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll() .and() .exceptionHandling().accessDeniedHandler(accessDeniedHandler); // create two users, admin and user @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user").password("password").roles("USER") .and() .withUser("admin").password("password").roles("ADMIN");
3.2 Custom 403 Access denied handler, logs the request and redirect to /403
package com.mkyong.error; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; // handle 403 page @Component public class MyAccessDeniedHandler implements AccessDeniedHandler { private static Logger logger = LoggerFactory.getLogger(MyAccessDeniedHandler.class); @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null) { logger.info("User '" + auth.getName() + "' attempted to access the protected URL: " + httpServletRequest.getRequestURI()); httpServletResponse.sendRedirect(httpServletRequest.getContextPath() + "/403");
4. Spring Boot
4.1 A controller class, to define the http request and view name.
package com.mkyong.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class DefaultController { @GetMapping("/") public String home1() { return "/home"; @GetMapping("/home") public String home() { return "/home"; @GetMapping("/admin") public String admin() { return "/admin"; @GetMapping("/user") public String user() { return "/user"; @GetMapping("/about") public String about() { return "/about"; @GetMapping("/login") public String login() { return "/login"; @GetMapping("/403") public String error403() { return "/error/403";
4.2 Start Spring Boot application.
package com.mkyong; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringBootWebApplication { public static void main(String[] args) throws Exception { SpringApplication.run(SpringBootWebApplication.class, args);
5. Thymeleaf + Resources + Static files
5.1 For Thymeleaf files, put in src/main/resources/templates/ folder.
5.2 Thymeleaf fragments, for template layout – header.
<html xmlns:th="http://www.thymeleaf.org"> <head> <div th:fragment="header-css"> <!-- this is header-css --> <link rel="stylesheet" type="text/css" href="webjars/bootstrap/3.3.7/css/bootstrap.min.css" /> <link rel="stylesheet" th:href="@{/css/main.css}" href="../../css/main.css" /> </div> </head> <body> <div th:fragment="header"> <!-- this is header --> <nav class="navbar navbar-inverse"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" th:href="@{/}">Spring Boot</a> </div> <div id="navbar" class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <li class="active"><a th:href="@{/}">Home</a></li> </ul> </div> </div> </nav> </div> </body> </html>
5.3 Thymeleaf fragments, for template layout – footer. Review the sec tag, it is a useful tag to display the Spring Security stuff, refer to this Thymeleaf extra Spring Security for detail.
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"> <head> </head> <body> <div th:fragment="footer"> <div class="container"> <footer> <!-- this is footer --> © 2017 mkyong.com <span sec:authorize="isAuthenticated()"> | Logged user: <span sec:authentication="name"></span> | Roles: <span sec:authentication="principal.authorities"></span> | <a th:href="@{/logout}">Sign Out</a> </span> <script type="text/javascript" src="webjars/bootstrap/3.3.7/js/bootstrap.min.js"></script> </footer> </div> </div> </body> </html>
5.4 List of the Thymeleaf files, nothing special, self-explanatory.
home ~
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Spring Boot Thymeleaf + Spring Security</title> <div th:replace="fragments/header :: header-css"/> </head> <body> <div th:replace="fragments/header :: header"/> <div class="container"> <div class="starter-template"> <h1>Spring Boot Web Thymeleaf + Spring Security</h1> <h2>1. Visit <a th:href="@{/admin}">Admin page (Spring Security protected, Need Admin Role)</a></h2> <h2>2. Visit <a th:href="@{/user}">User page (Spring Security protected, Need User Role)</a></h2> <h2>3. Visit <a th:href="@{/about}">Normal page</a></h2> </div> </div> <!-- /.container --> <div th:replace="fragments/footer :: footer"/> </body> </html>
admin ~
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <div th:replace="fragments/header :: header-css"/> </head> <body> <div th:replace="fragments/header :: header"/> <div class="container"> <div class="starter-template"> <h1>Admin page (Spring Security protected)</h1> <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1> <form th:action="@{/logout}" method="post"> <input type="submit" value="Sign Out"/> </form> </div> </div> <!-- /.container --> <div th:replace="fragments/footer :: footer"/> </body> </html>
user ~
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <div th:replace="fragments/header :: header-css"/> </head> <body> <div th:replace="fragments/header :: header"/> <div class="container"> <div class="starter-template"> <h1>User page (Spring Security protected)</h1> <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1> <form th:action="@{/logout}" method="post"> <input type="submit" value="Sign Out"/> </form> </div> </div> <!-- /.container --> <div th:replace="fragments/footer :: footer"/> </body> </html>
about ~
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <div th:replace="fragments/header :: header-css"/> </head> <body> <div th:replace="fragments/header :: header"/> <div class="container"> <div class="starter-template"> <h1>Normal page (No need login)</h1> </div> </div> <!-- /.container --> <div th:replace="fragments/footer :: footer"/> </body> </html>
login ~
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" > <head> <title>Spring Security Example </title> <div th:replace="fragments/header :: header-css"/> </head> <body> <div th:replace="fragments/header :: header"/> <div class="container"> <div class="row" style="margin-top:20px"> <div class="col-xs-12 col-sm-8 col-md-6 col-sm-offset-2 col-md-offset-3"> <form th:action="@{/login}" method="post"> <fieldset> <h1>Please Sign In</h1> <div th:if="${param.error}"> <div class="alert alert-danger"> Invalid username and password. </div> </div> <div th:if="${param.logout}"> <div class="alert alert-info"> You have been logged out. </div> </div> <div class="form-group"> <input type="text" name="username" id="username" class="form-control input-lg" placeholder="UserName" required="true" autofocus="true"/> </div> <div class="form-group"> <input type="password" name="password" id="password" class="form-control input-lg" placeholder="Password" required="true"/> </div> <div class="row"> <div class="col-xs-6 col-sm-6 col-md-6"> <input type="submit" class="btn btn-lg btn-primary btn-block" value="Sign In"/> </div> <div class="col-xs-6 col-sm-6 col-md-6"> </div> </div> </fieldset> </form> </div> </div> </div> <div th:replace="fragments/footer :: footer"/> </body> </html>
403 ~
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <div th:replace="fragments/header :: header-css"/> </head> <body> <div th:replace="fragments/header :: header"/> <div class="container"> <div class="starter-template"> <h1>403 - Access is denied</h1> <div th:inline="text">Hello '[[${#httpServletRequest.remoteUser}]]', you do not have permission to access this page.</div> </div> </div> <!-- /.container --> <div th:replace="fragments/footer :: footer"/> </body> </html>
5.5 For static files like CSS or Javascript, put in /src/main/resources/static/
h1{ color:#0000FF; h2{ color:#FF0000; footer{ margin-top:60px;
Read this Spring Boot Serving static content to understand the resource mapping.
6. Demo
6.1 Start the Spring Boot web app. This /admin/** is protected, you need login as admin to access it.
$ mvn spring-boot:run //...
6.2 Access http://localhost:8080
6.3 Access http://localhost:8080/admin, redirect to http://localhost:8080/login
6.4 Invalid username or password http://localhost:8080/login
6.5 Login successful, redirect back to admin page http://localhost:8080/admin , review the footer section, the user info is displayed.
6.6 Access http://localhost:8080/user, redirect to http://localhost:8080/403
6.7 Clicks on the sign out link in the footer, redirect to http://localhost:8080/login?logout
Done. Try login with another username “user” and access the admin page.
References
- Securing a Web Application
- Spring Security Reference
- Spring Boot Security features
- Spring Boot Hello World Example – Thymeleaf
- Spring Security Hello World Annotation Example
- Thymeleaf – Spring Security integration basics
- Thymeleaf extra – Spring Security integration basics
- Thymeleaf – Standard URL Syntax
- Spring Boot + Spring MVC + Spring Security + MySQL
- Spring Boot – Static content
- Spring MVC – Inlucde CSS file
From:一号门
Previous:How to tell Maven to use Java 8
COMMENTS