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.
- .... 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.
- UsernamePassword Aithentication Filter. ClassSpring Security comes with a form login validation filter, which is also the filter used in this article.
- BasicAuthenticationFilter.class
- ExceptionTranslation.class exception interpreter
- FilterSecurity Interceptor. class interceptor finally decides whether the request can pass
- 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 & 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