Spring Security fundamentals
Spring Security filter chain
Spring Security implements a chain of filters, one by one in the following order.
. class
Some custom filters (you can choose which filter to insert before the configuration), because this needs to vary from person to person, this article is not discussed, you can studyUsernamePasswordAithenticationFilter.class
The form login authentication filter that comes with Spring Security is the filter used in this articleBasicAuthenticationFilter.class
ExceptionTranslation.class
Exception interpreterFilterSecurityInterceptor.class
The interceptor ultimately decides whether the request can passController
We ended up writing our own controller
Description of related classes
User.class
Note that this class was not written by us, but is officially provided by Spring Security, which provides some basic functionality that can be extended by inheriting from this class. See in the codeCustomUser.java
UserDetailsService.class
: An interface officially provided by Spring Security that contains only one methodloadUserByUsername()
, Spring Security will call this method to get the data in the database, and then compare it with the user’s posted username and password to determine whether the user’s username and password are correct. So we need to do it ourselvesloadUserByUsername()
This method. See in the codeCustomUserDetailsService.java
.
Program logic
To reflect the difference in permissions, we constructed a database with four users using HashMap
ID | The user name | password | permissions |
---|---|---|---|
1 | jack | jack123 | user |
2 | danny | danny123 | editor |
3 | alice | alice123 | reviewer |
4 | smith | smith123 | admin |
Description of permissions
User: the most basic permission, as long as the login user has user permission
Editor: Added the editor permission to the user permission
Reviewer: As above, editor and Reviewer belong to the same level of authority
Admin: Contains all permissions
To verify permissions, we provide several pages
The url | instructions | Access rights |
---|---|---|
/ | Home page | Accessible to all (Anonymous) |
/login | Login page | Accessible to all (Anonymous) |
/logout | Exit pages | Accessible to all (Anonymous) |
/user/home | The user center | user |
/user/editor | editor, admin | |
/user/reviewer | reviewer, admin | |
/user/admin | admin | |
/ 403 | 403 error page, embellished, you can use directly | Accessible to all (Anonymous) |
/ 404 | 404 error page, beautification, you can use directly | Accessible to all (Anonymous) |
/ 500 | 500 error page, beautification, you can directly use | Accessible to all (Anonymous) |
Code configuration
Maven configuration
<? The 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 > < the parent > < groupId > org. Springframework. Boot < / groupId > The < artifactId > spring - the 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> <! -- Specify the JDK version, </java.version> </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> <! --> <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> <! > <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>Copy the code
Application. The properties configuration
In order for hot loading to work (so that you don’t need to restart Tomcat after modifying the template), we need to add a paragraph to the Spring Boot configuration file
spring.thymeleaf.cache=false
Copy the code
For more details on hot loading, see the official documentation: docs.spring. IO /spring-boot…
Spring Security configuration
First we turn on method annotation support: Only need to add on the class @ EnableGlobalMethodSecurity (securedEnabled = true, prePostEnabled = true), We set prePostEnabled = true to support expressions like hasRole(). For more information about Method annotations, see Introduction to Spring Method Security.
SecurityConfig.java
/** * To enable method annotation support, we set prePostEnabled = true so that we can use expressions like hasRole() later. https://www.baeldung.com/spring-security-method-security */ @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * TokenBasedRememberMeServices generate keys, * algorithm can be found in the documents: 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; /** * This method is required. Spring Security officially requires a password encryption method. * Note: For example, the BCryptPasswordEncoder() encryption method is used here, so this method must also be used when saving the user password to ensure consistency. */ @bean public PasswordEncoder PasswordEncoder() {return new BCryptPasswordEncoder(); } /** * To configure Spring Security, here are some considerations. CSRF is enabled by default, so the POST form must have a hidden field to pass CSRF * and POST to /logout to exit the user. See our login. HTML and logout.html. * 2. Once the rememberMe() function is enabled, we must provide a rememberMe services, such as the getRememberMeServices() method below, * and we can set the cookie in TokenBasedRememberMeServices name, expiration date and other relevant configuration, if the configuration at the same time, in other places will be an error. * Error examples: xxxx.and().rememberMe().rememberMeServices(getRememberMeServices()).rememberMeCookieName("cookie-name") */ @Override Protected void configure(HttpSecurity HTTP) throws Exception {http.formlogin ().loginPage("/login") // Customize the user login page .failureUrl("/login? Error ") // Customize login failure page, And ().logout().logouturl ("/logout")// customize the user to logout of the page.logoutsuccessurl ("/").and() .rememberme () // Open rememberMe password function. RememberMeServices(getRememberMeServices()) // must provide.key(SECRET_KEY) // This SECRET needs and generate TokenBasedRememberMeServices key. The same and () / * * the default allows all paths everyone can access, ensure the normal access static resources. * Control permissions through method annotations. */ .authorizeRequests().anyRequest().permitAll() .and() .exceptionHandling().accessDeniedPage("/403"); } /** * If you want to set the cookie expiration time or other related configurations, Please configure below * / private TokenBasedRememberMeServices getRememberMeServices () {TokenBasedRememberMeServices services = new TokenBasedRememberMeServices(SECRET_KEY, customUserDetailsService); services.setCookieName("remember-cookie"); services.setTokenValiditySeconds(100); // Default 14 days return services; }}Copy the code
UserService.java
A Service that emulates database operations on its own to get data from the data source that it emulates through the 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; } /* * This is done because Spring Security will empty the password field in User to ensure Security. * Since Java classes are passed by reference, to prevent Spring Security from modifying our source data, we provide a copy of an object to Spring Security. * If you get it from a real database, you don't have to worry about this. */ return new CustomUser(originUser.getId(), originUser.getUsername(), originUser.getPassword(), originUser.getAuthorities()); }}Copy the code
CustomUserDetailsService.java
@service public class CustomUserDetailsService implements UserDetailsService {/** * 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 (" username: "+username+" Role: "+ user.getauthorities ().tostring ()); return user; }}Copy the code
Custom permission annotations
In the process of developing the website, for example, GET /user/editor can request roles as editor and ADMIN. If we write a long list of permission expressions above each method that needs to determine the permission, it must be very complicated. But with custom permissions annotations, we can use methods like @isEditor to make it much easier. For further information, see 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 {
}
Copy the code
IsEditor.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_EDITOR', 'ROLE_ADMIN')")
public @interface IsEditor {
}
Copy the code
IsReviewer.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_REVIEWER', 'ROLE_ADMIN')")
public @interface IsReviewer {
}
Copy the code
IsAdmin.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public @interface IsAdmin {
}
Copy the code
Spring Security comes with expressions
hasRole()
, whether you have a permissionhasAnyRole()
, one of multiple permissions, such ashasAnyRole("ADMIN","USER")
hasAuthority()
.Authority
和Role
It’s similar. The only difference isAuthority
The prefix is muchROLE_
, such ashasAuthority("ROLE_ADMIN")
Is equivalent tohasRole("ADMIN")
, can refer to aboveIsUser.java
The writing ofhasAnyAuthority()
, as above, one of multiple permissions is requiredpermitAll()
.denyAll()
.isAnonymous()
.isRememberMe()
It can be understood literallyisAuthenticated()
.isFullyAuthenticated()
The two differences areisFullyAuthenticated()
Higher security requirements for certification. For example, the userRemember password functionLog in to the system for sensitive operations,isFullyAuthenticated()
Returns thefalse
, at this point we can ask the user to enter the password again to ensure security, andisAuthenticated()
All login users are returnedtrue
.principal()
.authentication()
For example, if we want to obtain the id of the login user, we can clickprincipal()
The returnedObject
Get, actuallyprincipal()
The returnedObject
Basically equivalent to what we wrote ourselvesCustomUser
. whileauthentication()
The returnedAuthentication
是Principal
Parent class, related operations can be seenAuthentication
The source code. Read more about this laterFour ways to get user data in Controller writinghasPermission()
Just take the literal meaning
For more information, see Intro to Spring Security Expressions.
Added Thymeleaf support
We added Thymeleaf support for Spring Security with Thymeleaf – Extras-Spring Security.
Maven configuration
The Maven configuration above has been added
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
Copy the code
Using the example
Note that we add XMLNS: SEC support to the 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>Copy the code
For more information, see the documentation Thymeleaf – Extras-Spring Security.
The Controller to write
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"; }}Copy the code
UserController.java
In this controller, I show a combination of the use of custom annotations and four ways to get user information
@controller @requestMapping ("/user") public class UserController {@getMapping ("/home") Public String home(Model Model) { Get CustomUser User = via SecurityContextHolder (CustomUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal(); model.addAttribute("user", user); return "user/home"; } @getMapping ("/editor") @isEditor public String Editor (Authentication Authentication, Model Model) { Through the method of injection form to obtain the Authentication CustomUser user = (CustomUser) Authentication. GetPrincipal (); model.addAttribute("user", user); return "user/editor"; } @getMapping ("/reviewer") @isReviewer public String Reviewer (Principal Principal, Model Model) { CustomUser User = (CustomUser) ((Authentication)principal).getPrincipal(); CustomUser = (CustomUser) ((Authentication)principal). model.addAttribute("user", user); return "user/reviewer"; } @getMapping ("/admin") @isadmin public String admin() { Use Thymeleaf's Security tag. For details, see admin. HTML return "user/admin". }}Copy the code
Pay attention to
- If A method with security controls is called by another method in the same class, the permission control for method A is ignored, and the private methods are also affected
- Spring’s
SecurityContext
Thread-bound, so if we create another thread in the current thread, then theirSecurityContext
Is not shared, see for more informationSpring Security Context Propagation with @Async
The writing of the Html
When writing HTML, it’s pretty much the same, except that ** if you turn on CSRF and add hidden fields to your form POST request, ** Because Thymeleaf automatically adds 😀.
Github address: github.com/Smith-Cruis…
This article was first published on the public account: Java Version of the Web project, welcome to pay attention to get more exciting content