Spring Security fundamentals

Spring Security filter chain

Spring Security implements a chain of filters, one by one in the following order.

  1. . classSome 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 study
  2. UsernamePasswordAithenticationFilter.classThe form login authentication filter that comes with Spring Security is the filter used in this article
  3. BasicAuthenticationFilter.class
  4. ExceptionTranslation.classException interpreter
  5. FilterSecurityInterceptor.classThe interceptor ultimately decides whether the request can pass
  6. ControllerWe ended up writing our own controller

Description of related classes

  • User.classNote 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 &amp; 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 permission
  • hasAnyRole(), one of multiple permissions, such ashasAnyRole("ADMIN","USER")
  • hasAuthority().AuthorityRoleIt’s similar. The only difference isAuthorityThe prefix is muchROLE_, such ashasAuthority("ROLE_ADMIN")Is equivalent tohasRole("ADMIN"), can refer to aboveIsUser.javaThe writing of
  • hasAnyAuthority(), as above, one of multiple permissions is required
  • permitAll().denyAll().isAnonymous().isRememberMe()It can be understood literally
  • isAuthenticated().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 returnedObjectGet, actuallyprincipal()The returnedObjectBasically equivalent to what we wrote ourselvesCustomUser. whileauthentication()The returnedAuthenticationPrincipalParent class, related operations can be seenAuthenticationThe source code. Read more about this laterFour ways to get user data in Controller writing
  • hasPermission()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’sSecurityContextThread-bound, so if we create another thread in the current thread, then theirSecurityContextIs 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