Explain Spring Security in detail

Table of contents

1 Overview

2. Login

2.1. Default user

2.2. Custom user

2.3. Encryption

2.4. Bypass encryption

2.5. How to transfer user information

2.6. Remember me

3. Log out

4. Working with databases

4.1.jdbcAuthentication

4.2.userDetailsService

5. Custom Processor

6. More fine-grained control

7. Brief description of the principle


1 Overview

Spring Security is a security framework based on the Spring framework, which provides a series of APIs and extension points to help developers easily implement security authentication and authorization control in applications.

We can understand that Spring Security maintains a set of access rules that we can customize. Every time we visit, we will compare the rules, and only those that meet the rules will be released.

These rules can have many dimensions. This article will start with the most basic role-based control and gradually expand and introduce Spring Security in detail.

Based on role control, we can understand that Spring Security maintains a "white list" for us, and login is to go to the white list for comparison.

2. Login

2.1. Default user

rely:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

After the dependency is introduced, spring security has already taken effect. At this time, accessing our interface will automatically jump to the login page built in spring security. After the verification is passed, it will jump to the back-end interface:

Default username: user

Default password: will be output in the log

2.2. Custom user

Spring security provides us with an interface to configure security, including custom users and roles:

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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests().
                //所有用户都可以访问/all
                        antMatchers("/all").permitAll().
                //admin可以访问/admin
                        antMatchers("/admin").hasRole("admin");
        //如果验证未通过,转跳spring security自带的登录页面进行登录
        //如果不配置此处的步骤,验证未通过则会直接返回403访问被拒绝。
        http.formLogin();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定义两个用户user、admin,user对应user角色,admin对应admin角色
        auth.inMemoryAuthentication()
                .withUser("user").password("123").roles("user")
                .and()
                .withUser("admin").password("456").roles("user", "admin");
    }
}

2.3. Encryption

After completing the above configuration, we will visit our interface again. After jumping to the login page, enter the corresponding user name and password we defined, such as admin 456. It seems that we should be able to access our interface normally. But it will actually report an error:

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

This is because the password is not encrypted when we define the user. Since spring security 5.0, the password must be encrypted. Otherwise, when the password is compared, there will be an exception that the password encryption algorithm cannot be resolved. The mainstream version we are currently using must be above 5.0, so encryption is a must.

Spring security provides a PasswordEncoder interface for our customizer, in which the encryption process can be customized by method rewriting. But in actual use, there is no need to write an encryption algorithm by yourself. Security has prepared a variety of encryptors and encryption methods for us, and the ones written by ourselves are definitely not as stable and easy to use as open source.

The encryptors provided by spring security are as follows:

  • BCryptPasswordEncoder: This is one of the most commonly used encryption algorithms, it uses a hash and a random salt to encrypt passwords.

  • Pbkdf2PasswordEncoder: This is also a password encryption algorithm that uses a password-based key derivation function (PBKDF2) to encrypt passwords.

  • SCryptPasswordEncoder: This is a memory-based password hashing algorithm that uses a lot of memory to prevent hash collision attacks.

  • NoOpPasswordEncoder: This is an insecure encryption algorithm, which only uses plaintext passwords as encrypted passwords. Not recommended for use in production environments.

We choose BCryptPasswordEncoder to encrypt the password, modify it, and use BCryptPasswordEncoder to encrypt when defining the username and password:

 @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定义两个用户user、admin,user对应user角色,admin对应admin角色
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("user").password("123").roles("user")
                .and()
                .withUser("admin").password("456").roles("user", "admin");
    }

After encryption, when we log in on the login page, we cannot use the plaintext admin 456 to log in. We need to use the ciphertext password encrypted by the same encryption algorithm to log in to 456.

2.4. Bypass encryption

Of course, it is understandable to use ciphertext passwords in the actual production environment, but it will be very troublesome in the development and testing stages. Is there a way to bypass it? Yes, {noop}", the abbreviation of "no operation", means no operation is performed. If the prefix of the password is "{noop}", Spring Security will recognize it as a plaintext password without encryption and store it directly in In database or memory. You can change the above code to:

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定义两个用户user、admin,user对应user角色,admin对应admin角色
        auth.inMemoryAuthentication()
                .withUser("user").password("{noop}123").roles("user")
                .and()
                .withUser("admin").password("{noop}456").roles("user", "admin");
    }

2.5. How to transfer user information

Since we always enter the username and password on the login interface for our request, it is impossible to do this every time in actual use. How to carry the username and password in the request and pass it to security? It depends on the configuration, two ways are supported:

  • form

  • in the request header

form:

Backend configuration:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
                //指定用表单的方式登录
                .formLogin()
                //登录地址,不配置的话会有默认值,默认是login,可以用这个配置来设置新的请求页
                .loginPage("/login")
                //配置用户名的参数名,不配置的话会有默认值,默认是username
                .usernameParameter("username")
                //配置密码的参数名,不配置的话会有默认值,默认是password
                .passwordParameter("password")
                .permitAll()
    }

Front-end form:

<form action="/login" method="post">
  <label for="username">Username:</label>
  <input type="text" id="username" name="username"><br><br>
  <label for="password">Password:</label>
  <input type="password" id="password" name="password"><br><br>
  <input type="submit" value="Submit">
</form>

When the user submits the form, a POST request will be sent to the backend, the URL of the request is /login, and the request parameters include the username and password.

Put it in the table header:

Add the Authorization field to the request header, encode the username and password in Base64, and pass them to the backend for verification. Example backend configuration:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .antMatchers("/", "/home").permitAll()
        .anyRequest().authenticated()
        .and()
        .httpBasic()
        .permitAll();
}

2.6. Remember me

Security supports the "remember me" function, which is implemented based on token. After remembering me is enabled, a cookie will be returned to the client after successful login. Use this cookie to achieve the effect of remembering me. The configuration example is as follows:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests().
                antMatchers("/all").permitAll().
                antMatchers("/admin").hasRole("admin").and()
                .formLogin().and()
                //开启记住我功能
                .rememberMe()
                //设置返回的cookie的键名
                .rememberMeParameter("remember-me")
                //设置过期时间
                .tokenValiditySeconds(7*24*60*60);
    }

3. Log out

It’s easy to understand after logging out after talking about logging in:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        // 其他配置
        .logout()
            .logoutUrl("/logout") // 配置登出的URL
            .logoutSuccessUrl("/login") // 配置登出成功后的跳转URL
            .deleteCookies("remember-me") // 删除Cookie
            .permitAll(); // 允许所有用户访问登出URL
}

4. Working with databases

In the above, we use auth.inMemoryAuthentication() to store the defined user and role information in memory. In actual business scenarios, sometimes we need to store this information in the database for persistence. There are two common ways in spring security to put information in the database:

  • auth.jdbcAuthentication()

  • auth.userDetailsService()

4.1.jdbcAuthentication

When using this method, the table structure is strictly stipulated, and the specific table creation statement is here:

After it is built, there will be two tables:

Here are a few points to note:

  • Add ROLE_ to the permission prefix, otherwise it will not take effect, the enabled attribute is whether the user is valid 1 is 0 is not

  • The password must be encrypted, otherwise it cannot be recognized

The next step is to use:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private DataSource dataSource;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication().dataSource(dataSource)
            .usersByUsernameQuery("select username, password, enabled from users where username=?")
            .authoritiesByUsernameQuery("select username, authority from authorities where username=?")
            .passwordEncoder(new BCryptPasswordEncoder());
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // ...
    }
}

4.2.userDetailsService

The jdbcAuthentication method has strict requirements on the table structure. If you think it is too rigid, you need to be more flexible. Security also provides a flexible method of userDetailsService for us to use flexibly.

We can implement UserDetailsService by ourselves, reloadUserByUsername method, and flexibly convert system users to security users in the method.

@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //从存储中获取系统用户
        SystemUser systemUser = userDao.getUser();
        //将系统用户的用户名密码来创建security的用户
        User.UserBuilder builder = User.builder();
        UserDetails user = builder.username(systemUser.getUserName()).password(systemUser.getPassword()).roles("admin").build();
        return user;
    }
}

5. Custom Processor

In Spring Security, the interface can be accessed after the default authentication is successful, but if I want to do other things instead of directly accessing the interface after the authentication is successful? Spring security also provides us with the ability to customize the processing process after the operation is completed. We can use a custom processor (Handler) to handle authentication, authorization, logout and other operations. Custom processors can be accomplished by implementing the corresponding interface or inheriting the corresponding class.

The following are the interfaces/classes of several commonly used custom processors and their functions:

  1. AuthenticationSuccessHandler: Used for processing after successful authentication, such as logging, jumping to a specified page, etc.

  2. AuthenticationFailureHandler: Used for processing after authentication failure, such as recording logs, displaying error messages, etc.

  3. AccessDeniedHandler: It is used to handle the situation that the access is denied, such as jumping to the error page, recording logs, etc.

  4. LogoutSuccessHandler: Used for processing after successful logout, such as jumping to the login page, deleting cookies, etc.

  5. InvalidSessionStrategy: Used to handle invalid sessions, such as jumping to the login page, recording logs, etc.

In Spring Security, we can use custom processors through configuration. For example:

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 处理认证成功的逻辑,比如记录日志、跳转页面等
        response.sendRedirect("/index");
    }
}
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        // 处理认证失败的逻辑,比如记录日志、跳转页面等
        response.sendRedirect("/login?error=true");
    }
}
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 处理访问拒绝的逻辑,比如记录日志、跳转页面等
        response.sendRedirect("/accessDenied");
    }
}
public class MyLogoutHandler implements LogoutHandler {

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        // 处理注销的逻辑,比如记录日志、清除缓存等
    }
}
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 处理注销成功的逻辑,比如记录日志、跳转页面等
        response.sendRedirect("/logoutSuccess");
    }
}

It can be used directly after definition:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    //省略其他配置...
    
    @Autowired
    private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
    
    @Autowired
    private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
    
    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;
    
    @Autowired
    private CustomLogoutSuccessHandler customLogoutSuccessHandler;
    
    @Autowired
    private CustomInvalidSessionStrategy customInvalidSessionStrategy;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .successHandler(customAuthenticationSuccessHandler)
            .failureHandler(customAuthenticationFailureHandler)
            .and()
            .exceptionHandling()
            .accessDeniedHandler(customAccessDeniedHandler)
            .and()
            .logout()
            .logoutSuccessHandler(customLogoutSuccessHandler)
            .and()
            .sessionManagement()
            .invalidSessionStrategy(customInvalidSessionStrategy);
    }
    
}

6. More fine-grained control

In the previous article, we all carried out access control based on roles. After a little thought, we will think that it may not be enough to control the actual application by roles. Sometimes it is also based on HTTP METHOD, IP, permissions, etc., of course spring security gives us These more fine-grained access control methods are provided:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 配置安全规则
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        
            // 基于角色控制访问
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/user/**").hasRole("USER")
            
            // 基于权限控制访问
            .antMatchers("/article/**").hasAuthority("ARTICLE_VIEW")
            
            // 基于IP地址控制访问
            .antMatchers("/profile/**").hasIpAddress("192.168.1.0/24")
            
            // 基于方法表达式控制访问
            .antMatchers(HttpMethod.DELETE, "/article/**").access("hasRole('ADMIN') or hasAuthority('ARTICLE_DELETE')")
            .antMatchers(HttpMethod.PUT, "/article/**").access("hasRole('ADMIN') or hasAuthority('ARTICLE_EDIT')")
            
            // 其他请求需要进行身份认证
            .anyRequest().authenticated()
            
            .and()
            .formLogin()
            
            .and()
            .httpBasic();
    }

    // 配置用户信息
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
        
            // 配置用户角色和权限
            .withUser("user").password("{noop}password").roles("USER").authorities("ARTICLE_VIEW")
            .and()
            .withUser("admin").password("{noop}password").roles("ADMIN").authorities("ARTICLE_VIEW", "ARTICLE_EDIT", "ARTICLE_DELETE");
    }
}

Of course, in addition to being configurable in the code, an annotation version is also provided:

// 基于角色的访问控制注解
    @Secured("ROLE_ADMIN")
    public void adminOnlyMethod() {
        // 只有 ADMIN 角色的用户可以调用这个方法
    }
 
    // 基于角色的访问控制注解
    @RolesAllowed("ROLE_USER")
    public void userOnlyMethod() {
        // 只有 USER 角色的用户可以调用这个方法
    }
 
    // 基于方法调用之后的返回值进行访问控制
    @PostAuthorize("returnObject.owner == authentication.name")
    public Resource getResourceById(String id) {
        // 获取资源
        return resource;
    }
 
    // 在方法调用前进行集合过滤
    @PreFilter("filterObject.owner == authentication.name")
    public List<Resource> getResources(List<String> ids) {
        // 获取指定 ID 的资源列表
        return resources;
    }
 
    // 在方法调用后进行集合过滤
    @PostFilter("filterObject.owner == authentication.name")
    public List<Resource> getAllResources() {
        // 获取所有资源列表
        return resources;
    }
 
    // 基于SpEL表达式的访问控制注解,控制HTTP方法
    @PreAuthorize("hasRole('ADMIN') and #httpMethod == 'GET'")
    @GetMapping("/adminOnly")
    public ResponseEntity<String> adminOnlyEndpoint(HttpServletRequest request) {
        return ResponseEntity.ok("Only accessible to admins");
    }

7. Brief description of the principle

Looking at the picture above, the implementation principle of Spring Security is clear at a glance. In fact, it is mounted into the request process of the application through a Servlet filter. When a request arrives at the application, it will first be intercepted by Spring Security's filter for security control processing, and then the request will be passed on to other components of the application for processing.

There is no need to delve into the voting device here. Its function is actually to determine whether a user has permission to access a specific resource. The voter decides whether to allow users to access resources by evaluating the authentication information, access control list (ACL) and other factors in the security context (SecurityContext).

Guess you like

Origin blog.csdn.net/Joker_ZJN/article/details/130290806