Spring Boot+Spring Security+Thymeleaf Simple Tutorial

Posted by rmbarnes82 on Thu, 01 Aug 2019 04:22:03 +0200

Because there is a project that needs MVC framework, so I learned Spring Security and recorded it. I hope you can learn and give advice together.

GitHub address: https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo

If you have any questions, please post the issue in GitHub. I will answer it for you when I am free.

This project is based on Spring Boot 2 + Spring Security 5 + Thymeleaf 2 + JDK11 (you can also use 8, which should make little difference)

The following functions are realized:

  • Annotation-based access control
  • Use Spring Security tags in Thymeleaf
  • Custom permission annotations
  • Memorizing Password Function

If you need to separate front-end and back-end security framework building tutorials can be referred to: Shiro+JWT+Spring Boot Restful Simple Tutorial

Project demonstration

If you want to experience it directly, you can run the mvn spring-boot:run command to access the clone project directly. The rules of the web site are at the end of the tutorial.

home page

Log in

Logout

Home page

Admin page

403 Unauthorized Page

Basic Principles of Spring Security

Spring Security Filter Chain

Spring Security implements a series of filter chains, which are executed one by one in the following order.

  1. .... class has some custom filters (you can choose which filter to insert before you configure it), because this requirement varies from person to person. This article does not discuss it, you can study it by yourself.
  2. UsernamePassword Aithentication Filter. ClassSpring Security comes with a form login validation filter, which is also the filter used in this article.
  3. BasicAuthenticationFilter.class
  4. ExceptionTranslation.class exception interpreter
  5. FilterSecurity Interceptor. class interceptor finally decides whether the request can pass
  6. Controller The Controller We Finally Write Our Own Controller

Relevant Class Description

  • User.class: Note that this class is not written by ourselves, but is officially provided by Spring Security. It provides some basic functions, and we can extend the method by inheriting this class. See CustomUser.java in the code for details
  • UserDetails Service. class: An interface officially provided by Spring Security. There is only one method loadUserByUsername(). Spring Security calls this method to get the data in the database, and then compares it with the user name password from the user POST to determine whether the user's username password is positive or not. True. So we need to implement the loadUserByUsername() method ourselves. See CustomUserDetailsService.java in the code for details.

Project logic

In order to show the difference of permissions, we constructed a database through HashMap, which contains four users.

ID User name Password Jurisdiction
1 jack jack123 user
2 danny danny123 editor
3 alice alice123 reviewer
4 smith smith123 admin

Explanatory authority

User: The most basic privilege, as long as the user is logged in, there is user privilege

Editor: Added editor permissions to user permissions

reviewer: Similarly, editor s and reviewers belong to the same level of authority

admin: Includes all permissions

To verify permissions, we provide several pages

Website Explain Accessible privileges
/ home page anonymous
/login Login page anonymous
/logout end page anonymous
/user/home User Center user
/user/editor   editor, admin
/user/reviewer   reviewer, admin
/user/admin   admin
/403 403 error page, beautified, you can use it directly anonymous
/404 404 error page, beautified, you can use it directly anonymous
/500 500 error pages, beautified, you can directly use anonymous

Code configuration

Maven configuration

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.inlighting</groupId>
    <artifactId>security-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security-demo</name>
    <description>Demo project for Spring Boot &amp; Spring Security</description>

    <!--Appoint JDK Version, you can change to your own-->
    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</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-web</artifactId>
        </dependency>
        <!--Yes Thymeleaf Add to Spring Security Label support-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--Developed Hot Loading Configuration-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.properties configuration

In order to make the hot load (so that you don't need to restart Tomcat after modifying the template) take effect, we need to add a paragraph to the Spring Book configuration file.

spring.thymeleaf.cache=false

For more information on hot loading, see the official documentation: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-hotswapping

Spring Security Configuration

First we turn on method annotation support: simply add the @EnableGlobalMethod Security (secured Enabled = true, prePostEnabled = true) annotation to the class, and we set prePostEnabled = true to support expressions such as hasRole(). If you want to know more about method annotations, you can see Introduction to Spring Method Security This article.

SecurityConfig.java

/**
 * Opening method annotation support, we set prePostEnabled = true to enable hasRole() expressions later
 * For further information, see the tutorial: https://www.baeldung.com/spring-security-method-security
 */
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * TokenBasedRememberMeServices Generate the key,
     * The implementation of the algorithm is detailed in the document https://docs.spring.io/spring-security/site/docs/5.1.3.RELEASE/reference/htmlsingle/#remember-me-hash-token.
     */
    private final String SECRET_KEY = "123456";

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    /**
     * There must be this method, and Spring Security officially stipulates that there must be a password encryption method.
     * Note: For example, BCryptPasswordEncoder() is used here, so it must be used when saving user passwords to ensure consistency.
     * See the Logic for Saving Users in Database.java in the Project for details.
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * To configure Spring Security, here are some considerations.
     * 1. Spring Security By default, CSRF is enabled, at which point the POST form we submit must have hidden fields to pass CSRF.
     * And in logout, we have to exit the user by POST to / logout. See our login.html and logout.html for details.
     * 2. When rememberMe() is enabled, we must provide rememberMeServices, such as the getRememberMeServices() method below.
     * And we can only configure cookie names, expiration times and other related configurations in TokenBasedRememberMeServices. If we configure cookies at the same time elsewhere, we will report errors.
     * Error examples: xxxx. and (). rememberMe (). rememberMeServices (getRememberMeServices (). rememberMeCookieName ("cookie-name")
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login") // Custom user login page
                .failureUrl("/login?error") // Customize the failed login page. The front end can provide friendly user login prompts by using error s in the url.
                .and()
                .logout()
                .logoutUrl("/logout")// Custom User Logout Page
                .logoutSuccessUrl("/")
                .and()
                .rememberMe() // Turn on Password Memory
                .rememberMeServices(getRememberMeServices()) // Must be provided
                .key(SECRET_KEY) // This SECRET needs the same key as the one used to generate TokenBasedRememberMeServices
                .and()
                /*
                 * By default, all paths are accessible to everyone, ensuring normal access to static resources.
                 * Later, the method annotations are used to control the right of control.
                 */
                .authorizeRequests().anyRequest().permitAll()
                .and()
                .exceptionHandling().accessDeniedPage("/403"); // Insufficient permission automatic jump 403
    }

    /**
     * If you want to set cookie expiration time or other related configuration, please configure it yourself below.
     */
    private TokenBasedRememberMeServices getRememberMeServices() {
        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices(SECRET_KEY, customUserDetailsService);
        services.setCookieName("remember-cookie");
        services.setTokenValiditySeconds(100); // Default 14 days
        return services;
    }
}

UserService.java

Service that simulates database operations by itself is used to obtain data from data sources simulated by HashMap.

@Service
public class UserService {

    private Database database = new Database();

    public CustomUser getUserByUsername(String username) {
        CustomUser originUser = database.getDatabase().get(username);
        if (originUser == null) {
            return null;
        }

        /*
         * There are pits here, because when Spring Security gets the User, it empties the password field in the User to ensure security.
         * Because Java classes are reference transfers, in order to prevent Spring Security from modifying our source data, we replicate an object to Spring Security.
         * If it is accessed through a real database, there is no such problem to worry about.
          */
        return new CustomUser(originUser.getId(), originUser.getUsername(), originUser.getPassword(), originUser.getAuthorities());
    }
}

CustomUserDetailsService.java

/**
 * Implement the official User Details Service interface
 */
@Service
public class CustomUserDetailsService implements UserDetailsService {

    private Logger LOGGER = LoggerFactory.getLogger(getClass());

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        CustomUser user = userService.getUserByUsername(username);
        if (user == null) {
            throw new  UsernameNotFoundException("The user does not exist");
        }
        LOGGER.info("User name:"+username+" Roles:"+user.getAuthorities().toString());
        return user;
    }
}

Custom permission annotations

In the process of developing websites, such as GET/user/editor, the request roles of EDITOR and ADMIN are certainly all right. If we write a long list of privilege expressions on each method that needs to determine the privilege, it must be very complicated. But by customizing the permission annotations, we can use the @IsEditor method to judge, which is much simpler. Further understanding can be seen as follows: Introduction to Spring Method Security

IsUser.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyAuthority('ROLE_USER', 'ROLE_EDITOR', 'ROLE_REVIEWER', 'ROLE_ADMIN')")
public @interface IsUser {
}

IsEditor.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_EDITOR', 'ROLE_ADMIN')")
public @interface IsEditor {
}

IsReviewer.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_REVIEWER', 'ROLE_ADMIN')")
public @interface IsReviewer {
}

IsAdmin.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public @interface IsAdmin { 
}

Spring Security Expressions

  • hasRole(), Does it have a permission?

  • hasAnyRole(), one of several permissions, such as hasAnyRole("ADMIN","USER")

  • hasAuthority(), Authority and Role are very similar, the only difference is that Authority prefix has more ROLE_, such as hasAuthority("ROLE_ADMIN") is equivalent to hasRole("ADMIN"), can refer to the above IsUser.java.

  • hasAnyAuthority(), as above, one of the multiple permissions is sufficient

  • permitAll(), denyAll(),isAnonymous(), isRememberMe(), which can be understood literally

  • isAuthenticated() and isFully Authenticated (), the two differences are that isFully Authenticated () has higher security requirements for authentication. For example, the user logs in to the system for sensitive operations by remembering the password function, and isFully Authenticated () returns false. At this time, we can let the user enter the password again to ensure security, while isAuthenticated() returns true as long as the user logs in.

  • principal(), authentication(), for example, if we want to get the id of the logged-in user, we can get it through the Object returned by principal(), in fact, the Object returned by principal() can basically be equivalent to the CustomUser we wrote ourselves. Authentication returned by authentication() is the parent class of Principal. The source code of Authentication can be seen for related operations. To get a better understanding of the four ways to get user data in Controller writing

  • hasPermission(), literally

If you want to know more, you can refer to it. Intro to Spring Security Expressions

Add Thymeleaf support

We add Thymeleaf support for Spring Security through thymeleaf-extras-spring security.

Maven configuration

The above Maven configuration has been added

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

Use examples

Note that we need to add support for xmlns:sec to html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>Admin</title>
</head>
<body>
<p>This is a home page.</p>
<p>Id: <th:block sec:authentication="principal.id"></th:block></p>
<p>Username: <th:block sec:authentication="principal.username"></th:block></p>
<p>Role: <th:block sec:authentication="principal.authorities"></th:block></p>
</body>
</html>

For further information, see the documentation. thymeleaf-extras-springsecurity

Controller Writing

IndexController.java

This controller does not have any permissions.

@Controller
public class IndexController {

    @GetMapping("/")
    public String index() {
        return "index/index";
    }

    @GetMapping("/login")
    public String login() {
        return "index/login";
    }

    @GetMapping("/logout")
    public String logout() {
        return "index/logout";
    }
}

UserController.java

In this controller, I synthesized the use of custom annotations and four ways to get user information.

@IsUser // Indicates that all requests under this controller need to be logged in before they can be accessed.
@Controller
@RequestMapping("/user")
public class UserController {

    @GetMapping("/home")
    public String home(Model model) {
        // Method 1: Get it through Security ContextHolder
        CustomUser user = (CustomUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        model.addAttribute("user", user);
        return "user/home";
    }

    @GetMapping("/editor")
    @IsEditor
    public String editor(Authentication authentication, Model model) {
        // Method 2: Obtain Authentication by Method Injection
        CustomUser user = (CustomUser)authentication.getPrincipal();
        model.addAttribute("user", user);
        return "user/editor";
    }

    @GetMapping("/reviewer")
    @IsReviewer
    public String reviewer(Principal principal, Model model) {
        // Method 3: The same injection method, pay attention to the transformation, this method is very two, not recommended.
        CustomUser user = (CustomUser) ((Authentication)principal).getPrincipal();
        model.addAttribute("user", user);
        return "user/reviewer";
    }

    @GetMapping("/admin")
    @IsAdmin
    public String admin() {
        // Method 4: Through Thymeleaf's Security tag, see admin.html for details.
        return "user/admin";
    }
}

Be careful

  • If method A of security control is invoked by other methods in the same class, then the permission control of method A will be ignored, and private methods will also be affected.
  • Spring's SecurityContext is thread-bound. If we create other threads in the current thread, their SecurityContext is not shared. For more information, see Spring Security Context Propagation with @Async

Writing of Html

When writing html, it's basically the same thing, that is to note that ** if CSRF is turned on, hidden fields are added when writing POST requests for forms, such as **<input type="hidden" th: name="${csrf.parameterName}" th: value="${csrf.token}"/>, but you don't need to add them, because Th is OK. Ymeleaf automatically adds _____________.


Link: https://www.jianshu.com/p/dcf227d53ab5
 

Topics: Spring Thymeleaf Java Database