spring security (the most complete in history)

Authentication and Authorization¶

Generally speaking, application access security revolves around the two core concepts of authentication and authorization.

Right now:

  • First you need to identify the user identity,
  • Then determine whether the user has access to the specified resources.

There are many solutions for authentication, the mainstream ones are CAS, , SAML2, OAUTH2etc. (unfortunately, these have been used -_-). The single sign-on solution (SSO) we often talk about is this.

For authorization, the mainstream ones are spring security and shiro.

Shiro is relatively lightweight. In comparison, spring security does have a more complex architecture. But shiro and ss, just master one.

Here, Nien will give you a systematic and systematic sorting out, for the reference of the small partners in the future, and improve everyone's 3-level architecture, design, and development level.

Note: This article is continuously updated in PDF. For the latest PDF files of Nien’s architecture notes and interview questions, please get them from the link below: Code Cloud

What is OAuth2?

OAuth2 is an open standard about authorization . The core idea is to authenticate user identities through various authentication methods (OAuth2 does not care about the specific methods).

And issue a token (token), so that third-party applications can use the token to access specified resources within a limited time and within a limited scope .

The main RFC specifications involved are [ RFC6749(overall authorization framework)], [ RFC6750(token usage)], and [ RFC6819(threat model)], which are generally what we need to understand RFC6749.

There are four main ways to obtain tokens, namely 授权码模式,简单模式 , 密码模式, 客户端模式,

In short: OAuth2 is an authorization (Authorization) protocol.

Authentication (Authentication) proves that you are this person,

And authorization (Authorization) is to prove whether this person has access to this resource (Resource).

The following picture comes from the OAuth 2.0 authorization framework RFC Document , which is an abstract process of OAuth2.

     +--------+                               +---------------+
     |        |--(A)- Authorization Request ->|   Resource    |
     |        |                               |     Owner     |
     |        |<-(B)-- Authorization Grant ---|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(C)-- Authorization Grant -->| Authorization |
     | Client |                               |     Server    |
     |        |<-(D)----- Access Token -------|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(E)----- Access Token ------>|    Resource   |
     |        |                               |     Server    |
     |        |<-(F)--- Protected Resource ---|               |
     +--------+                               +---------------+

Let me first explain the nouns in the above picture:

Resource Owner : the resource owner, that is, the user

Client : client application (Application)

Authorization Server : authorization server

Resource Server : resource server

Let's explain the general process of the above figure:

(A) After the user connects to the client application, the client application asks the user for authorization

(B) The user agrees to authorize the client application

(C) The client application uses the authorization (Grant) obtained in the previous step to apply for a token from the authorization server

(D) After the authorization server verifies the authorization (Grant) of the client application, it confirms that it is correct and issues a token

(E) The client application uses the token to apply to the resource server for resources

(F) The resource server confirms that the token is correct and agrees to open resources to the client application

As can be seen from the above process, how to obtain the **authorization (Grant)** is the key.

In OAuth2 there are 4 grant types:

  • Authorization Code (authorization code mode) :

The authorization model with the most complete functions and the strictest process. Interact with the authentication server through a third-party application server. Widely used in various third-party authentication.

  • Implicit (simplified mode):

Apply for a token directly to the authentication server in the browser without going through a third-party application server, which is more suitable for mobile apps and third-party single-page applications without a server.

  • Resource Owner Password (password mode) :

Users provide their own user names and passwords to the client server, which are used when users have a high degree of trust in the client, such as internal systems of companies and organizations, SSO.

  • Client Credentials (client mode):

The client server authenticates to the authentication server on its own behalf, not on the user's behalf.

The following mainly talks about the most commonly used (1) and (3). In addition, there is another mode called Refresh Token, which will be introduced below.

Resource Owner Password (password mode)

     +----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          v
          |    Resource Owner
         (A) Password Credentials
          |
          v
     +---------+                                  +---------------+
     |         |>--(B)---- Resource Owner ------->|               |
     |         |         Password Credentials     | Authorization |
     | Client  |                                  |     Server    |
     |         |<--(C)---- Access Token ---------<|               |
     |         |    (w/ Optional Refresh Token)   |               |
     +---------+                                  +---------------+

            Figure 5: Resource Owner Password Credentials Flow

Its steps are as follows:

(A) The user (Resource Owner) provides the user name and password to the client (Client).

(B) The client sends the user name and password to the authentication server (Authorization Server), and requests a token from the latter.

(C) After the authentication server confirms that it is correct, it provides the access token to the client.

Authorization Code (authorization code mode)

     +----------+
     | Resource |
     |   Owner  |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier      +---------------+
     |         -+----(A)-- & Redirection URI ---->|               |
     |  User-   |                                 | Authorization |
     |  Agent  -+----(B)-- User authenticates --->|     Server    |
     |          |                                 |               |
     |         -+----(C)-- Authorization Code ---<|               |
     +-|----|---+                                 +---------------+
       |    |                                         ^      v
      (A)  (C)                                        |      |
       |    |                                         |      |
       ^    v                                         |      |
     +---------+                                      |      |
     |         |>---(D)-- Authorization Code ---------'      |
     |  Client |          & Redirection URI                  |
     |         |                                             |
     |         |<---(E)----- Access Token -------------------'
     +---------+       (w/ Optional Refresh Token)

   Note: The lines illustrating steps (A), (B), and (C) are broken into
   two parts as they pass through the user-agent.

Its steps are as follows:

(A) The user (Resource Owner) accesses the client (Client) through the user agent (User-Agent), and the client asks for authorization and directs the user to the authentication server (Authorization Server).

(B) The user chooses whether to authorize the client.

(C) Assuming that the user grants authorization, the authentication server directs the user to the "redirection URI" (redirection URI) specified by the client in advance, and attaches an authorization code at the same time.

(D) The client receives the authorization code, attaches the previous "redirect URI", and applies for a token to the authentication server. This step is done on the server in the background of the client and is not visible to the user.

(E) The authentication server checks the authorization code and the redirection URI, and after confirming that it is correct, sends the access token (access token) and update token (refresh token) to the client. This step is also invisible to the user.

Token refresh (refresh token)

  +--------+                                           +---------------+
  |        |--(A)------- Authorization Grant --------->|               |
  |        |                                           |               |
  |        |<-(B)----------- Access Token -------------|               |
  |        |               & Refresh Token             |               |
  |        |                                           |               |
  |        |                            +----------+   |               |
  |        |--(C)---- Access Token ---->|          |   |               |
  |        |                            |          |   |               |
  |        |<-(D)- Protected Resource --| Resource |   | Authorization |
  | Client |                            |  Server  |   |     Server    |
  |        |--(E)---- Access Token ---->|          |   |               |
  |        |                            |          |   |               |
  |        |<-(F)- Invalid Token Error -|          |   |               |
  |        |                            +----------+   |               |
  |        |                                           |               |
  |        |--(G)----------- Refresh Token ----------->|               |
  |        |                                           |               |
  |        |<-(H)----------- Access Token -------------|               |
  +--------+           & Optional Refresh Token        +---------------+

When we applied for token, Authorization Server not only gave us Access Token, but also Refresh Token.

When the Access Token expires, we can use the Refresh Token to access the /refresh endpoint to get a new Access Token.

We have to distinguish it from Spring Security's Authentication ,

What is Spring Security?

Spring Security is a security framework that can control user access rights based on RBAC (role-based access control),

The core idea is to intercept and filter through a series of filter chains to control the user's access rights.

The core functions of spring security mainly include:

  • Authentication (who you are)
  • authorization (what can you do)
  • Attack protection (prevention of forged identities)

At its core is a set of filter chains that will be automatically configured when the project starts. The core is that the Basic Authentication Filter is used to authenticate the user's identity, and a filter in spring security handles an authentication method.

img

For example, for the username password authentication filter,

Will check if it is a login request;

Whether to include username and password (that is, some authentication information required by the filter);

If not satisfied, release to the next one.

The next step is to judge whether it is the information you need according to your own responsibilities. The characteristic of basic is that there is Authorization: Basic eHh4Onh4 information in the request header. There may be more authentication filters in between. The last link is FilterSecurityInterceptor , which will determine whether the request can access the rest service. The basis for the judgment is the configuration in BrowserSecurityConfig. If it is rejected, different exceptions will be thrown (according to the specific reason). Exception Translation Filter will catch the thrown error, and then return information according to different authentication methods.

Note: The green filter can be configured to take effect, and the others cannot be controlled.

2. Getting started project

First create the spring boot project HelloSecurity, whose pom mainly depends on the following:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</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>
</dependencies>

Then create a page in the src/main/resources/templates/ directory:

home.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example</title>
    </head>
    <body>
        <h1>Welcome!</h1>

        <p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
    </body>
</html>

As we can see, a link is included in this simple view: "/hello". Linking to the following page, the Thymeleaf template is as follows:

hello.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1>Hello world!</h1>
    </body>
</html>

The web application is based on Spring MVC. Therefore, you need to configure Spring MVC and set up view controllers to expose these templates. The following is a typical Spring MVC configuration class. In the src/main/java/hello directory (so java is here):

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
    
    
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
    
    
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }
}

The addViewControllers() method (overriding the WebMvcConfigurerAdapter method of the same name) adds four view controllers. Two view controllers refer to a view named "home" (defined in home.html), and the other refers to a view named "hello" (defined in hello.html). The fourth view controller references another view called "login". This view will be created in the next section. At this point, you can skip ahead to make the application executable and run the application without logging into anything. Then start the program as follows:

@SpringBootApplication
public class Application {
    
    

    public static void main(String[] args) throws Throwable {
    
    
        SpringApplication.run(Application.class, args);
    }
}

2. Join Spring Security

Suppose you want to prevent unauthorized users from accessing "/hello". At this point, if the user clicks on the link on the home page, they see the greeting and the request is not blocked. You need to add a barrier to make the user log in before seeing the page. You can do this by configuring Spring Security in your application. If Spring Security is on the classpath, Spring Boot automatically secures all HTTP endpoints using "Basic Authentication". At the same time, you can further customize the security settings. First introduce in the pom file:

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

The following is the security configuration so that only authenticated users can access the greeting page:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth
            .inMemoryAuthentication()
                .withUser("user").password("password").roles("USER");
    }
}

The WebSecurityConfig class uses the @EnableWebSecurity annotation to enable Spring Security's web security support and provide Spring MVC integration. It also extends WebSecurityConfigurerAdapter and overrides some methods to set some details of the web security configuration.

The configure(HttpSecurity) method defines which URL paths should be secured and which should not. Specifically, the "/" and "/home" paths are configured not to require any authentication. All other paths must be authenticated.

When a user successfully logs in, they will be redirected to the previously requested page that requires authentication. There is a custom "/login" page specified by loginPage() that everyone can view.

For the configureGlobal(AuthenticationManagerBuilder) method, it sets a single user in memory. The user name of this user is "user", the password is "password", and the role is "USER".

Now we need to create the login page. We already configured the "login" view controller earlier, so now we only need to create the login page:

login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>
        <div th:if="${param.error}">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}">
            You have been logged out.
        </div>
        <form th:action="@{/login}" method="post">
            <div><label> User Name : <input type="text" name="username"/> </label></div>
            <div><label> Password: <input type="password" name="password"/> </label></div>
            <div><input type="submit" value="Sign In"/></div>
        </form>
    </body>
</html>

As you can see, this Thymeleaf template just provides a form to get username and password and submit them to "/login". Depending on configuration, Spring Security provides a filter that intercepts this request and authenticates the user. If the user is not authenticated, the page will redirect to "/login?error" and display the appropriate error message on the page. After a successful logout, our application sends to "/login?logout" and our page displays a corresponding logout success message. Finally, we need to provide the user with a method to display the current username and logout. Update hello.html to print a hello to the current user and include a "logout" form as follows:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
        <form th:action="@{/logout}" method="post">
            <input type="submit" value="Sign Out"/>
        </form>
    </body>
</html>

3. Detailed explanation of parameters

1. Annotate @EnableWebSecurity

Using Spring Security in Spring boot applications, the @EnableWebSecurity annotation is used. The official description is that this annotation is used together with the @Configuration annotation to annotate the class of type WebSecurityConfigurer, or use the @EnableWebSecurity annotation to inherit the class of WebSecurityConfigurerAdapter, thus forming Spring Security Configuration.

2. Abstract class WebSecurityConfigurerAdapter

In general, you will choose to inherit the WebSecurityConfigurerAdapter class. The official description is: WebSecurityConfigurerAdapter provides a convenient way to create an instance of WebSecurityConfigurer. You only need to rewrite the method of WebSecurityConfigurerAdapter to configure what URLs to intercept, what permissions to set, and other security controls.

3、方法 configure(AuthenticationManagerBuilder auth) 和 configure(HttpSecurity http)

Two methods of WebSecurityConfigurerAdapter are rewritten in Demo:

/**
     * 通过 {@link #authenticationManager()} 方法的默认实现尝试获取一个 {@link AuthenticationManager}.
     * 如果被复写, 应该使用{@link AuthenticationManagerBuilder} 来指定 {@link AuthenticationManager}.
     *
     * 例如, 可以使用以下配置在内存中进行注册公开内存的身份验证{@link UserDetailsService}:
     *
     * // 在内存中添加 user 和 admin 用户
     * @Override
     * protected void configure(AuthenticationManagerBuilder auth) {
     *     auth
     *       .inMemoryAuthentication().withUser("user").password("password").roles("USER").and()
     *         .withUser("admin").password("password").roles("USER", "ADMIN");
     * }
     *
     * // 将 UserDetailsService 显示为 Bean
     * @Bean
     * @Override
     * public UserDetailsService userDetailsServiceBean() throws Exception {
     *     return super.userDetailsServiceBean();
     * }
     *
     */
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
    this.disableLocalConfigureAuthenticationBldr = true;
}


/**
     * 复写这个方法来配置 {@link HttpSecurity}. 
     * 通常,子类不能通过调用 super 来调用此方法,因为它可能会覆盖其配置。 默认配置为:
     * 
     * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
     *
     */
protected void configure(HttpSecurity http) throws Exception {
    
    
    logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

    http
        .authorizeRequests()
        .anyRequest().authenticated()
        .and()
        .formLogin().and()
        .httpBasic();
}

4. Final class HttpSecurity

HttpSecurity common methods and instructions:

method illustrate
openidLogin() For OpenId based authentication
headers() Add a security header to the response
cors() Configure Cross-Origin Resource Sharing ( CORS )
sessionManagement() Allows configuration of session management
portMapper() Allows to configure one PortMapper( HttpSecurity#(getSharedObject(class))), other provided SecurityConfigurerobjects use redirection PortMapperfrom HTTP to HTTPS or from HTTPS to HTTP. By default, Spring Security uses a PortMapperImplmapping HTTP port 8080 to HTTPS port 8443, HTTP port 80 to HTTPS port 443
jee() Configure container-based pre-authentication. In this case authentication is managed by the Servlet container
x509() Configure x509-based authentication
rememberMe Allows configuration of "remember me" authentication
authorizeRequests() HttpServletRequestAllow access based on usage restrictions
requestCache() Allows configuring request caching
exceptionHandling() Allow configuration error handling
securityContext() Management set on HttpServletRequestsbetween . This will be automatically applied when usingSecurityContextHolderSecurityContextWebSecurityConfigurerAdapter
servletApi() Integrate HttpServletRequestthe method with the value found on it SecurityContext. WebSecurityConfigurerAdapterThis will be automatically applied when using
csrf() Add CSRF support, WebSecurityConfigurerAdapterwhen used, enabled by default
logout() Added logout support. When used WebSecurityConfigurerAdapter, this will be applied automatically. By default, access the URL "/logout", invalidate the HTTP Session to clear the user, clear any #rememberMe()authentication that has been configured, clear SecurityContextHolder, then redirect to "/login?success"
anonymous() Allows configuration of the representation of anonymous users. WebSecurityConfigurerAdapterThis will be applied automatically when used in conjunction with By default anonymous users will be org.springframework.security.authentication.AnonymousAuthenticationTokenrepresented using and include the role "ROLE_ANONYMOUS"
formLogin() Specifies that forms-based authentication is supported. If not specified FormLoginConfigurer#loginPage(String), a default login page will be generated
oauth2Login() Configure authentication against an external OAuth 2.0 or OpenID Connect 1.0 provider
requiresChannel() Configure channel security. For this configuration to be useful, at least one mapping to the desired channel must be provided
httpBasic() Configure Http Basic Authentication
addFilterAt() Add a filter at the location of the specified Filter class

5、类 AuthenticationManagerBuilder

/**
* {@link SecurityBuilder} used to create an {@link AuthenticationManager}. Allows for
* easily building in memory authentication, LDAP authentication, JDBC based
* authentication, adding {@link UserDetailsService}, and adding
* {@link AuthenticationProvider}'s.
*/

It means that AuthenticationManagerBuilder is used to create an AuthenticationManager, which allows me to easily implement memory authentication, LADP authentication, JDBC-based authentication, add UserDetailsService, and add AuthenticationProvider.

Use the username and password defined in the yaml file to log in

Define username and password in application.yaml:

spring:
  security:
    user:
      name: root
      password: root

Log in with root/root, and you can access normally /hello.

Log in with the username and password specified in the code

  • Use configure(AuthenticationManagerBuilder) to add authentication.
  • Use configure(httpSecurity) to add permissions
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth
                .inMemoryAuthentication()
                .withUser("admin") // 添加用户admin
                .password("{noop}admin")  // 不设置密码加密
                .roles("ADMIN", "USER")// 添加角色为admin,user
                .and()
                .withUser("user") // 添加用户user
                .password("{noop}user") 
                .roles("USER")
            	.and()
            	.withUser("tmp") // 添加用户tmp
                .password("{noop}tmp")
            	.roles(); // 没有角色
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                .authorizeRequests()
                .antMatchers("/product/**").hasRole("USER") //添加/product/** 下的所有请求只能由user角色才能访问
                .antMatchers("/admin/**").hasRole("ADMIN") //添加/admin/** 下的所有请求只能由admin角色才能访问
                .anyRequest().authenticated() // 没有定义的请求,所有的角色都可以访问(tmp也可以)。
                .and()
                .formLogin().and()
                .httpBasic();
    }
}

添加AdminController、ProductController

@RestController
@RequestMapping("/admin")
public class AdminController {
    
    
    @RequestMapping("/hello")
    public String hello(){
    
    
        return "admin hello";
    }
}
@RestController
@RequestMapping("/product")
public class ProductController {
    
    
    @RequestMapping("/hello")
    public String hello(){
    
    
        return "product hello";
    }
}

Through the above settings, access to http://localhost:8080/admin/hello can only be accessed by admin, http://localhost:8080/product/hello can be accessed by both admin and user, http://localhost:8080/hello All users (including tmp) have access.

Login with database user name and password

add dependencies
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
Add database configuration
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
Configure spring-security authentication and authorization
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.userDetailsService(userDetailsService)// 设置自定义的userDetailsService
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                .authorizeRequests()
                .antMatchers("/product/**").hasRole("USER")
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated() //
                .and()
                .formLogin()
                .and()
                .httpBasic()
                .and().logout().logoutUrl("/logout");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return NoOpPasswordEncoder.getInstance();// 使用不使用加密算法保持密码
//        return new BCryptPasswordEncoder();
    }
}

If you need to use it BCryptPasswordEncoder, you can first encrypt it in the test environment and put it in the database:

@Test
void encode() {
    
    
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    String password = bCryptPasswordEncoder.encode("user");
    String password2 = bCryptPasswordEncoder.encode("admin");
    System.out.println(password);
    System.out.println(password2);
}
Configure a custom UserDetailsService for authentication
@Component("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
    
    

   @Autowired
   UserRepository userRepository;

   @Override
   public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
    
    
         // 1. 查询用户
      User userFromDatabase = userRepository.findOneByLogin(login);
      if (userFromDatabase == null) {
    
    
         //log.warn("User: {} not found", login);
       throw new UsernameNotFoundException("User " + login + " was not found in db");
            //这里找不到必须抛异常
      }

       // 2. 设置角色
      Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
      GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(userFromDatabase.getRole());
      grantedAuthorities.add(grantedAuthority);

      return new org.springframework.security.core.userdetails.User(login,
            userFromDatabase.getPassword(), grantedAuthorities);
   }
}
Configure UserRepository in JPA
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    
    User findOneByLogin(String login);
}
Add database data

image-20201130200749622

CREATE TABLE `user` (
  `id` int(28) NOT NULL,
  `login` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `password` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `role` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `demo`.`user`(`id`, `login`, `password`, `role`) VALUES (1, 'user', 'user', 'ROLE_USER');
INSERT INTO `demo`.`user`(`id`, `login`, `password`, `role`) VALUES (2, 'admin', 'admin', 'ROLE_ADMIN');

The default role prefix must be ROLE_, because spring-security will automatically use the role in match for comparison during authorization ROLE_.

Four: Obtain login information

@RequestMapping("/info")
public String info(){
    
    
    String userDetails = null;
    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    if(principal instanceof UserDetails) {
    
    
        userDetails = ((UserDetails)principal).getUsername();
    }else {
    
    
        userDetails = principal.toString();
    }
    return userDetails;
}

Use SecurityContextHolder.getContext().getAuthentication().getPrincipal();to get the current login information.

Five: Spring Security core components

SecurityContext

SecurityContextIt is a security context, and all data is saved in SecurityContext.

SecurityContextObjects that can be obtained through :

  • Authentication

SecurityContextHolder

SecurityContextHolderUtilities used to retrieve data held in the SecurityContext. Obtain the corresponding data of the SecurityContext by using the static method.

SecurityContext context = SecurityContextHolder.getContext();

Authentication

Authentication indicates the current authentication status, and the objects that can be obtained are:

UserDetails: Obtain additional information such as user information, whether to lock or not.

Credentials: Get the password.

isAuthenticated: Gets whether it has been authenticated.

Principal: Get the user, if not authenticated, then it is the username, if authenticated, return UserDetails.

UserDetails

public interface UserDetails extends Serializable {
    
    

	Collection<? extends GrantedAuthority> getAuthorities();
	String getPassword();
	String getUsername();
	boolean isAccountNonExpired();
	boolean isAccountNonLocked();
	boolean isCredentialsNonExpired();
	boolean isEnabled();
}

UserDetailsService

UserDetailsService can get UserDetails object through loadUserByUsername. This interface is used by spring security for user authentication.

Usually, a custom UserDetailsService is used to implement the UserDetailsService interface, and UserDetails is queried through customization.

AuthenticationManager

AuthenticationManager is used for verification, and if the verification fails, a corresponding exception will be thrown.

PasswordEncoder

password encryptor. Usually custom specified.

BCryptPasswordEncoder: hash algorithm encryption

NoOpPasswordEncoder: no encryption is used

Six: spring security session stateless support permission control (separation before and after)

Spring security will put authentication information into HttpSession by default.

But for the situation where our front and back ends are separated, such as apps, small programs, web front and back separation, etc., httpSession is useless. At this time, we can configure(httpSecurity)set whether to use httpSession in spring security.

@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    
    // code...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                .sessionManagement()
            	//设置无状态,所有的值如下所示。
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // code...
    }
    // code...
}

There are four values, of which the default is ifRequired.

  • always – a session will always be created if one doesn’t already exist,没有session就创建。
  • ifRequired – a session will be created only if required ( default ), if required (default).
  • never – the framework will never create a session itself but it will use one if it already exists
  • stateless – no session will be created or used by Spring Security

Since the front and back ends do not judge by saving sessions and cookies, in order to ensure that spring security can record the login status, it is necessary to pass a value so that the value can self-verify the source and obtain data information at the same time. For type selection, we choose JWT . For the java client we choose to use jjwt .

add dependencies

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

Create a tool class JWTProvider

JWTProvider needs to provide at least two methods, one is used to create our token, and the other is used to obtain Authentication based on token.

The provider needs to ensure that the Key key is unique and built using init(), otherwise an exception will be thrown.

@Component
@Slf4j
public class JWTProvider {
    
    
    private Key key;	// 私钥
    private long tokenValidityInMilliseconds; // 有效时间
    private long tokenValidityInMillisecondsForRememberMe; // 记住我有效时间
    @Autowired
    private JJWTProperties jjwtProperties; // jwt配置参数
    @Autowired
    private UserRepository userRepository; 
    @PostConstruct
    public void init() {
    
    
        byte[] keyBytes;
        String secret = jjwtProperties.getSecret();
        if (StringUtils.hasText(secret)) {
    
    
            log.warn("Warning: the JWT key used is not Base64-encoded. " +
                    "We recommend using the `jhipster.security.authentication.jwt.base64-secret` key for optimum security.");
            keyBytes = secret.getBytes(StandardCharsets.UTF_8);
        } else {
    
    
            log.debug("Using a Base64-encoded JWT secret key");
            keyBytes = Decoders.BASE64.decode(jjwtProperties.getBase64Secret());
        }
        this.key = Keys.hmacShaKeyFor(keyBytes); // 使用mac-sha算法的密钥
        this.tokenValidityInMilliseconds =
                1000 * jjwtProperties.getTokenValidityInSeconds();
        this.tokenValidityInMillisecondsForRememberMe =
                1000 * jjwtProperties.getTokenValidityInSecondsForRememberMe();
    }
    public String createToken(Authentication authentication, boolean rememberMe) {
    
    
        long now = (new Date()).getTime();
        Date validity;
        if (rememberMe) {
    
    
            validity = new Date(now + this.tokenValidityInMillisecondsForRememberMe);
        } else {
    
    
            validity = new Date(now + this.tokenValidityInMilliseconds);
        }
        User user = userRepository.findOneByLogin(authentication.getName());
        Map<String ,Object> map = new HashMap<>();
        map.put("sub",authentication.getName());
        map.put("user",user);
        return Jwts.builder()
                .setClaims(map) // 添加body
                .signWith(key, SignatureAlgorithm.HS512) // 指定摘要算法
                .setExpiration(validity) // 设置有效时间
                .compact();
    }
    public Authentication getAuthentication(String token) {
    
    
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token).getBody(); // 根据token获取body
        User principal;
        Collection<? extends GrantedAuthority> authorities;
        principal = userRepository.findOneByLogin(claims.getSubject());
        authorities = principal.getAuthorities();
        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }
}

Note that the User we created here needs to implement the UserDetails object, so that we can principal.getAuthorities()obtain permissions based on it. If UserDetails is not implemented, then we need to customize the authorities and add them to the UsernamePasswordAuthenticationToken.

@Data
@Entity
@Table(name="user")
public class User implements UserDetails {
    
    
    @Id
    @Column
    private Long id;
    @Column
    private String login;
    @Column
    private String password;
    @Column
    private String role;
    @Override
    // 获取权限,这里就用简单的方法
    // 在spring security中,Authorities既可以是ROLE也可以是Authorities
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        return Collections.singleton(new SimpleGrantedAuthority(role));
    }
    @Override
    public String getUsername() {
    
    
        return login;
    }
    @Override
    public boolean isAccountNonExpired() {
    
    
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
    
    
        return false;
    }
    @Override
    public boolean isCredentialsNonExpired() {
    
    
        return true;
    }
    @Override
    public boolean isEnabled() {
    
    
        return true;
    }
}

Create a successful login and successful logout handler

Send jwt to the foreground after successful login.

Authentication is successful, jwt is returned:

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
    
    
    void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException{
    
    
        PrintWriter writer = response.getWriter();
        writer.println(jwtProvider.createToken(authentication, true));
    }
}

Logout succeeded:

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    
    
    void onLogoutSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3) throws IOException, ServletException{
    
    
        PrintWriter writer = response.getWriter();
        writer.println("logout success");
        writer.flush();
    }
}

Set login, logout, cancel csrf protection

Logout cannot invalidate the token, you can use the database to save the token, and then delete the token when you log out.

@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    
    // code...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
       http
           // code...
           // 添加登录处理器
           .formLogin().loginProcessingUrl("/login").successHandler((request, response, authentication) -> {
    
    
           PrintWriter writer = response.getWriter();
           writer.println(jwtProvider.createToken(authentication, true));
       })
           // 取消csrf防护
           .and().csrf().disable() 
           // code...
           // 添加登出处理器
           .and().logout().logoutUrl("/logout").logoutSuccessHandler((HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> {
    
    
           PrintWriter writer = response.getWriter();
           writer.println("logout success");
           writer.flush();
       })
        	// code...
    }
    // code...
}

Integrate spring-security with JWT

Add Filter for spring-security to parse the token, and add our user information to the securityContext.

Before UsernamePasswordAuthenticationFilter.class, we need to add authentication based on token. The key method is to get authentication from jwt and add it to securityContext.

In SecurityConfiguration, you need to set the location where Filter is added.

Create a custom Filter for jwt to obtain authentication:

@Slf4j
public class JWTFilter extends GenericFilterBean {
    
    

    private final static String HEADER_AUTH_NAME = "auth";

    private JWTProvider jwtProvider;

    public JWTFilter(JWTProvider jwtProvider) {
    
    
        this.jwtProvider = jwtProvider;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
    
        try {
    
    
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
            String authToken = httpServletRequest.getHeader(HEADER_AUTH_NAME);
            if (StringUtils.hasText(authToken)) {
    
    
                // 从自定义tokenProvider中解析用户
                Authentication authentication = this.jwtProvider.getAuthentication(authToken);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
            // 调用后续的Filter,如果上面的代码逻辑未能复原“session”,SecurityContext中没有想过信息,后面的流程会检测出"需要登录"
            filterChain.doFilter(servletRequest, servletResponse);
        } catch (Exception ex) {
    
    
            throw new RuntimeException(ex);
        }
    }
}

Add Filter to HttpSecurity and set Filter location:

public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    
    // code...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                .sessionManagement()
            	//设置添加Filter和位置
                .and().addFilterBefore(new JWTFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);
                // code...
    }
    // code...
}

MySecurityConfiguration code

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JWTProvider jwtProvider;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.userDetailsService(userDetailsService)// 设置自定义的userDetailsService
                .passwordEncoder(passwordEncoder());
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)//设置无状态
                .and()
                .authorizeRequests() // 配置请求权限
                .antMatchers("/product/**").hasRole("USER") // 需要角色
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated() // 所有的请求都需要登录
                .and()
            	// 配置登录url,和登录成功处理器
                .formLogin().loginProcessingUrl("/login").successHandler((request, response, authentication) -> {
    
    
                    PrintWriter writer = response.getWriter();
                    writer.println(jwtProvider.createToken(authentication, true));
                })
            	// 取消csrf防护
                .and().csrf().disable() 
                .httpBasic()
            	// 配置登出url,和登出成功处理器
				.and().logout().logoutUrl("/logout")
            	.logoutSuccessHandler((HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> {
    
    
                    PrintWriter writer = response.getWriter();
                    writer.println("logout success");
                    writer.flush();
                })
            	// 在UsernamePasswordAuthenticationFilter之前执行我们添加的JWTFilter
                .and().addFilterBefore(new JWTFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return NoOpPasswordEncoder.getInstance();
    }
    @Override
    public void configure(WebSecurity web) {
    
    
        // 添加不做权限的URL
        web.ignoring()
                .antMatchers("/swagger-resources/**")
                .antMatchers("/swagger-ui.html")
                .antMatchers("/webjars/**")
                .antMatchers("/v2/**")
                .antMatchers("/h2-console/**");
    }
}

Use annotations to manage permissions on methods

Notes need to be MySecurityConfigurationadded , prePostEnabled is false by default, and it needs to be set to true to enable global annotation permission control.@EnableGlobalMethodSecurity(prePostEnabled = true)

After prePostEnabled is set to true, four annotations can be used:

Add entity class School:

@Data
public class School implements Serializable {
    
    
    private Long id;
    private String name;
    private String address;
}
  • @PreAuthorize

Permission judgment before access

@RestController
public class AnnoController {
    
    
    @Autowired
    private JWTProvider jwtProvider;
    @RequestMapping("/annotation")
//    @PreAuthorize("hasRole('ADMIN')")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public String info(){
    
    
        return "拥有admin权限";
    }
}

Both hasRole and hasAuthority will judge the getAuthorities in UserDetails. The difference is that hasRole will add the fields ROLE_before making judgments. If it is used in the above example hasRole('ADMIN'), then it will be used ROLE_ADMINfor judgment. If it is hasAuthority('ADMIN'), then it will be used ADMINfor judgment.

  • @PostAuthorize

Judging after the request, if the return value does not meet the conditions, an exception will be thrown, but the method itself has already been executed.

@RequestMapping("/postAuthorize")
@PreAuthorize("hasRole('ADMIN')")
@PostAuthorize("returnObject.id%2==0")
public School postAuthorize(Long id) {
    
    
    School school = new School();
    school.setId(id);
    return school;
}

returnObject is a built-in object that refers to the return value of the method.

If returnObject.id%2==0true, return the method value. If false, returns 403 Forbidden.

  • @PreFilter

Used to filter values ​​in a collection before method execution.

@RequestMapping("/preFilter")
@PreAuthorize("hasRole('ADMIN')")
@PreFilter("filterObject%2==0")
public List<Long> preFilter(@RequestParam("ids") List<Long> ids) {
    
    
    return ids;
}

filterObjectIt is a built-in object that refers to the generic class in the collection. If there are multiple collections, it needs to be specified filterTarget.

@PreFilter(filterTarget="ids", value="filterObject%2==0")
public List<Long> preFilter(@RequestParam("ids") List<Long> ids,@RequestParam("ids") List<User> users,) {
    
    
    return ids;
}

filterObject%2==0The values ​​in the collection will be filtered, and the value of true will be retained.

The value returned by the first example is filtered before execution to return 2, 4.

image-20201202115120854

  • @PostFilter

The returned collection is filtered.

@RequestMapping("/postFilter")
@PreAuthorize("hasRole('ADMIN')")
@PostFilter("filterObject.id%2==0")
public List<School> postFilter() {
    
    
    List<School> schools = new ArrayList<School>();
    School school;
    for (int i = 0; i < 10; i++) {
    
    
        school = new School();
        school.setId((long)i);
        schools.add(school);
    }
    return schools;
}

The above method returns results: School objects with ids 0, 2, 4, 6, and 8.

7. Principle explanation

1. Calibration flow chart

img

2. Source code analysis

  • AbstractAuthenticationProcessingFilter abstract class
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    throws IOException, ServletException {
    
    

    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;

    if (!requiresAuthentication(request, response)) {
    
    
        chain.doFilter(request, response);

        return;
    }

    if (logger.isDebugEnabled()) {
    
    
        logger.debug("Request is to process authentication");
    }

    Authentication authResult;

    try {
    
    
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {
    
    
            // return immediately as subclass has indicated that it hasn't completed
            // authentication
            return;
        }
        sessionStrategy.onAuthentication(authResult, request, response);
    }
    catch (InternalAuthenticationServiceException failed) {
    
    
        logger.error(
            "An internal error occurred while trying to authenticate the user.",
            failed);
        unsuccessfulAuthentication(request, response, failed);

        return;
    }
    catch (AuthenticationException failed) {
    
    
        // Authentication failed
        unsuccessfulAuthentication(request, response, failed);

        return;
    }

    // Authentication success
    if (continueChainBeforeSuccessfulAuthentication) {
    
    
        chain.doFilter(request, response);
    }

    successfulAuthentication(request, response, chain, authResult);
}

Call requiresAuthentication(HttpServletRequest, HttpServletResponse) to determine whether authentication is required . If authentication is required, the attemptAuthentication(HttpServletRequest, HttpServletResponse) method will be called, with three results:

  1. Returns an Authentication object. The configured SessionAuthenticationStrategy` will be called, and then the successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication) method will be called.
  2. An AuthenticationException occurred while authenticating. The unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) method will be called.
  3. Returns Null, indicating incomplete authentication. The method will return immediately, assuming the subclass has done some necessary work (such as redirection) to continue processing validation. It is assumed that the latter request will be received by this method, where the returned Authentication object is not null.
  • UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter的子类)
public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
    
    
        if (postOnly && !request.getMethod().equals("POST")) {
    
    
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String username = obtainUsername(request);
        String password = obtainPassword(request);

        if (username == null) {
    
    
            username = "";
        }

        if (password == null) {
    
    
            password = "";
        }

        username = username.trim();

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

The attemptAuthentication () method generates a UsernamePasswordAuthenticationToken object from the username and password in the request , which is used for AuthenticationManager verification (ie this.getAuthenticationManager().authenticate(authRequest)). By default the AuthenticationManager injected into the Spring container is the ProviderManager.

  • ProviderManager (the implementation class of AuthenticationManager)
public Authentication authenticate(Authentication authentication)
    throws AuthenticationException {
    
    
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    Authentication result = null;
    boolean debug = logger.isDebugEnabled();

    for (AuthenticationProvider provider : getProviders()) {
    
    
        if (!provider.supports(toTest)) {
    
    
            continue;
        }

        if (debug) {
    
    
            logger.debug("Authentication attempt using "
                         + provider.getClass().getName());
        }

        try {
    
    
            result = provider.authenticate(authentication);

            if (result != null) {
    
    
                copyDetails(authentication, result);
                break;
            }
        }
        catch (AccountStatusException e) {
    
    
            prepareException(e, authentication);
            // SEC-546: Avoid polling additional providers if auth failure is due to
            // invalid account status
            throw e;
        }
        catch (InternalAuthenticationServiceException e) {
    
    
            prepareException(e, authentication);
            throw e;
        }
        catch (AuthenticationException e) {
    
    
            lastException = e;
        }
    }

    if (result == null && parent != null) {
    
    
        // Allow the parent to try.
        try {
    
    
            result = parent.authenticate(authentication);
        }
        catch (ProviderNotFoundException e) {
    
    
            // ignore as we will throw below if no other exception occurred prior to
            // calling parent and the parent
            // may throw ProviderNotFound even though a provider in the child already
            // handled the request
        }
        catch (AuthenticationException e) {
    
    
            lastException = e;
        }
    }

    if (result != null) {
    
    
        if (eraseCredentialsAfterAuthentication
            && (result instanceof CredentialsContainer)) {
    
    
            // Authentication is complete. Remove credentials and other secret data
            // from authentication
            ((CredentialsContainer) result).eraseCredentials();
        }

        eventPublisher.publishAuthenticationSuccess(result);
        return result;
    }

    // Parent was null, or didn't authenticate (or throw an exception).

    if (lastException == null) {
    
    
        lastException = new ProviderNotFoundException(messages.getMessage(
            "ProviderManager.providerNotFound",
            new Object[] {
    
     toTest.getName() },
            "No AuthenticationProvider found for {0}"));
    }

    prepareException(lastException, authentication);

    throw lastException;
}

Attempts to authenticate the Authentication object . The list of AuthenticationProviders will be tried consecutively until the AuthenticationProvider indicates that it can authenticate the passed Authentication object. Authentication will then be attempted using that AuthenticationProvider. If more than one AuthenticationProvider supports authenticating the passed Authentication object, the first one determines the result, overriding any possible AuthenticationException raised by earlier supporting AuthenticationProviders. After successful authentication, subsequent AuthenticationProviders will not be attempted. If at last all AuthenticationProviders fail to authenticate the Authentication object successfully, an AuthenticationException will be thrown. It is not difficult to see from the code that authentication is verified by the provider, and the core point method is:

Authentication result = provider.authenticate(authentication);

The provider here is AbstractUserDetailsAuthenticationProvider, AbstractUserDetailsAuthenticationProvider is the implementation of AuthenticationProvider, look at its authenticate(authentication) method:

// 验证 authentication
public Authentication authenticate(Authentication authentication)
    throws AuthenticationException {
    
    
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                        messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.onlySupports",
                            "Only UsernamePasswordAuthenticationToken is supported"));

    // Determine username
    String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
        : authentication.getName();

    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);

    if (user == null) {
    
    
        cacheWasUsed = false;

        try {
    
    
            user = retrieveUser(username,
                                (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (UsernameNotFoundException notFound) {
    
    
            logger.debug("User '" + username + "' not found");

            if (hideUserNotFoundExceptions) {
    
    
                throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
            }
            else {
    
    
                throw notFound;
            }
        }

        Assert.notNull(user,
                       "retrieveUser returned null - a violation of the interface contract");
    }

    try {
    
    
        preAuthenticationChecks.check(user);
        additionalAuthenticationChecks(user,
                                       (UsernamePasswordAuthenticationToken) authentication);
    }
    catch (AuthenticationException exception) {
    
    
        if (cacheWasUsed) {
    
    
            // There was a problem, so try again after checking
            // we're using latest data (i.e. not from the cache)
            cacheWasUsed = false;
            user = retrieveUser(username,
                                (UsernamePasswordAuthenticationToken) authentication);
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user,
                                           (UsernamePasswordAuthenticationToken) authentication);
        }
        else {
    
    
            throw exception;
        }
    }

    postAuthenticationChecks.check(user);

    if (!cacheWasUsed) {
    
    
        this.userCache.putUserInCache(user);
    }

    Object principalToReturn = user;

    if (forcePrincipalAsString) {
    
    
        principalToReturn = user.getUsername();
    }

    return createSuccessAuthentication(principalToReturn, authentication, user);
}

AbstractUserDetailsAuthenticationProvider has a built-in caching mechanism. If the UserDetails information cannot be obtained from the cache, the following method is called to obtain the user information , and then compared with the information sent by the user to determine whether the authentication is successful.

// 获取用户信息
UserDetails user = retrieveUser(username,
 (UsernamePasswordAuthenticationToken) authentication);

The retrieveUser() method is implemented in DaoAuthenticationProvider, which is a subclass of AbstractUserDetailsAuthenticationProvider. The specific implementation is as follows:

protected final UserDetails retrieveUser(String username,
                                         UsernamePasswordAuthenticationToken authentication)
    throws AuthenticationException {
    
    
    UserDetails loadedUser;

    try {
    
    
        loadedUser = this.getUserDetailsService().loadUserByUsername(username);
    }
    catch (UsernameNotFoundException notFound) {
    
    
        if (authentication.getCredentials() != null) {
    
    
            String presentedPassword = authentication.getCredentials().toString();
            passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
                                            presentedPassword, null);
        }
        throw notFound;
    }
    catch (Exception repositoryProblem) {
    
    
        throw new InternalAuthenticationServiceException(
            repositoryProblem.getMessage(), repositoryProblem);
    }

    if (loadedUser == null) {
    
    
        throw new InternalAuthenticationServiceException(
            "UserDetailsService returned null, which is an interface contract violation");
    }
    return loadedUser;
}

It can be seen that the returned object userDetails here is obtained by #loadUserByUsername(username) of UserDetailsService.

Eight, play custom login

1. Form login process

The following is the basic process of form login:

As long as it is a form login, it can basically be transformed into the above process. Next, let's see how Spring Security handles it.

3. Login in Spring Security

By default it provides three login methods:

  • formLogin()normal form login
  • oauth2Login()Based on OAuth2.0authentication /authorization protocols
  • openidLogin()Based on OpenIDthe identity authentication specification

The above three methods are all AbstractAuthenticationFilterConfigurerrealized ,

4. Form form login in HttpSecurity

There are two ways to enable form login. One HttpSecurityis apply(C configurer)to construct AbstractAuthenticationFilterConfigureran . This is a more advanced way of playing. The other is our HttpSecuritycommon formLogin()method to customize FormLoginConfigurer. Let's start with the more conventional second one.

4.1 FormLoginConfigurer

This class is the configuration class for form form login. It provides some of our commonly used configuration methods:

  • loginPage(String loginPage) : The login page is not an interface. For the front and rear separation mode, we need to modify it by default /login.
  • loginProcessingUrl(String loginProcessingUrl) The actual form submits user information to the background , and then is intercepted and processed Actionby the filter , which does not actually process any logic.UsernamePasswordAuthenticationFilterAction
  • usernameParameter(String usernameParameter) is used to customize the user parameter name, default username.
  • passwordParameter(String passwordParameter) is used to customize user password name, defaultpassword
  • failureUrl(String authenticationFailureUrl) will be redirected to this path after login failure, and it is generally not used for front and back separation.
  • failureForwardUrl(String forwardUrl) Login failures will be forwarded here, and it is generally used separately before and after. You can define a Controller(controller) to handle the return value, but be careful RequestMethod.
  • defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) By default, you will be redirected to this after successful login. alwaysUseIf trueYes, you will always be redirected to this as long as the authentication process is successful. Generally recommended defaultfalse
  • The effect of successForwardUrl(String forwardUrl) is the same as defaultSuccessUrlabove butalwaysUse be careful .trueRequestMethod
  • successHandler (AuthenticationSuccessHandler successHandler) custom authentication success handler, which can replace all the above successmethods
  • failureHandler (AuthenticationFailureHandler authenticationFailureHandler) custom failure success handler, which can replace all the above failuremethods
  • permitAll(boolean permitAll) form form login whether to let go

Knowing this, we can create a customized login.

5. Spring Security aggregated login practice

Next is our most exciting actual login operation. If you have any doubts, you can carefully read a series of warm-up articles on Spring combat .

5.1 Simple requirements

Our interface access must be authenticated, and an error message (json) will be returned after a login error. After success, the front desk can obtain the corresponding database user information (json) (remember to desensitize in actual combat ).

We define a controller that handles success and failure:

@RestController
@RequestMapping("/login")
public class LoginController {
    
    
    @Resource
    private SysUserService sysUserService;

    /**
      * 登录失败返回 401 以及提示信息.
      *
      * @return the rest
      */
    @PostMapping("/failure")
    public Rest loginFailure() {
    
    

        return RestBody.failure(HttpStatus.UNAUTHORIZED.value(), "登录失败了,老哥");
    }

    /**
      * 登录成功后拿到个人信息.
      *
      * @return the rest
      */
    @PostMapping("/success")
    public Rest loginSuccess() {
    
    
        // 登录成功后用户的认证信息 UserDetails会存在 安全上下文寄存器 SecurityContextHolder 中
        User principal = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String username = principal.getUsername();
        SysUser sysUser = sysUserService.queryByUsername(username);
        // 脱敏
        sysUser.setEncodePassword("[PROTECT]");
        return RestBody.okData(sysUser,"登录成功");
    }
}

Then we customize the configuration override void configure(HttpSecurity http)method to configure as follows (crsf needs to be disabled here):

@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {
    
    

    @Configuration
    @Order(SecurityProperties.BASIC_AUTH_ORDER)
    static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
    
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
            super.configure(auth);
        }

        @Override
        public void configure(WebSecurity web) throws Exception {
    
    
            super.configure(web);
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
    
            http.csrf().disable()
                .cors()
                .and()
                .authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/process")
                .successForwardUrl("/login/success").
                failureForwardUrl("/login/failure");

        }
    }
}

Using Postman or other tools to submit the Post form http://localhost:8080/process?username=Felordcn&password=12345will return user information:

{
    
    
    "httpStatus": 200,
    "data": {
    
    
        "userId": 1,
        "username": "Felordcn",
        "encodePassword": "[PROTECT]",
        "age": 18
    },
    "msg": "登录成功",
    "identifier": ""
}

After changing the password to another value and failing to request authentication again:

{
    
    
    "httpStatus": 401,
    "data": null,
    "msg": "登录失败了,老哥",
    "identifier": "-9999"
}

6. Simple implementation of multiple login methods

Is that the end? There are many tricks to log in now. Conventional ones include text messages, emails, and code scanning. The third party is beyond the scope of today's scope that I will talk about in the future. How to deal with product managers with many ideas? Let's make a login method that can expand various postures. We can add an adapter between the user and the judgment in the above 2. form login process to adapt. We know this so-called judgment is .UsernamePasswordAuthenticationFilter

We only need to ensure that the uri is /process configured above and the user name and password can be obtained through getParameter(String name) .

I suddenly felt that I could DelegatingPasswordEncoderimitate the method of maintaining a registry to implement different processing strategies. Of course we have to implement GenericFilterBeana UsernamePasswordAuthenticationFilterbefore execution. At the same time, develop a login strategy.

6.1 Definition of login method

Defines the login method enum ``.

public enum LoginTypeEnum {
    
    

    /**
       * 原始登录方式.
       */
    FORM,
    /**
       * Json 提交.
       */
    JSON,
    /**
       * 验证码.
       */
    CAPTCHA

}

6.2 Define preprocessor interface

Define the pre-processor interface to process the received login parameters of various characteristics and process specific logic. This excuse is actually a bit random, the important thing is that you have to learn how to think. I have implemented a default form' 表单登录 和 通过RequestBody 放入json` in two ways, which will not be shown here due to space limitations. See the bottom for the specific DEMO .

public interface LoginPostProcessor {
    
    

    /**
        * 获取 登录类型
        *
        * @return the type
        */
    LoginTypeEnum getLoginTypeEnum();

    /**
        * 获取用户名
        *
        * @param request the request
        * @return the string
        */
    String obtainUsername(ServletRequest request);

    /**
        * 获取密码
        *
        * @param request the request
        * @return the string
        */
    String obtainPassword(ServletRequest request);

}

6.3 Implement login pre-processing filter

This filter maintains LoginPostProcessora mapping table. The front end is used to determine the login method for policy preprocessing, and it will eventually be handed over UsernamePasswordAuthenticationFilter. Prepended by the method HttpSecurityof .addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)

package cn.felord.spring.security.filter;

import cn.felord.spring.security.enumation.LoginTypeEnum;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY;
import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY;

/**
  * 预登录控制器
  *
  * @author Felordcn
  * @since 16 :21 2019/10/17
  */
public class PreLoginFilter extends GenericFilterBean {
    
    


    private static final String LOGIN_TYPE_KEY = "login_type";


    private RequestMatcher requiresAuthenticationRequestMatcher;
    private Map<LoginTypeEnum, LoginPostProcessor> processors = new HashMap<>();


    public PreLoginFilter(String loginProcessingUrl, Collection<LoginPostProcessor> loginPostProcessors) {
    
    
        Assert.notNull(loginProcessingUrl, "loginProcessingUrl must not be null");
        requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(loginProcessingUrl, "POST");
        LoginPostProcessor loginPostProcessor = defaultLoginPostProcessor();
        processors.put(loginPostProcessor.getLoginTypeEnum(), loginPostProcessor);

        if (!CollectionUtils.isEmpty(loginPostProcessors)) {
    
    
            loginPostProcessors.forEach(element -> processors.put(element.getLoginTypeEnum(), element));
        }

    }


    private LoginTypeEnum getTypeFromReq(ServletRequest request) {
    
    
        String parameter = request.getParameter(LOGIN_TYPE_KEY);

        int i = Integer.parseInt(parameter);
        LoginTypeEnum[] values = LoginTypeEnum.values();
        return values[i];
    }


    /**
      * 默认还是Form .
      *
      * @return the login post processor
      */
    private LoginPostProcessor defaultLoginPostProcessor() {
    
    
        return new LoginPostProcessor() {
    
    


            @Override
            public LoginTypeEnum getLoginTypeEnum() {
    
    

                return LoginTypeEnum.FORM;
            }

            @Override
            public String obtainUsername(ServletRequest request) {
    
    
                return request.getParameter(SPRING_SECURITY_FORM_USERNAME_KEY);
            }

            @Override
            public String obtainPassword(ServletRequest request) {
    
    
                return request.getParameter(SPRING_SECURITY_FORM_PASSWORD_KEY);
            }
        };
    }


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    
    
        ParameterRequestWrapper parameterRequestWrapper = new ParameterRequestWrapper((HttpServletRequest) request);
        if (requiresAuthenticationRequestMatcher.matches((HttpServletRequest) request)) {
    
    

            LoginTypeEnum typeFromReq = getTypeFromReq(request);

            LoginPostProcessor loginPostProcessor = processors.get(typeFromReq);


            String username = loginPostProcessor.obtainUsername(request);

            String password = loginPostProcessor.obtainPassword(request);


            parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_USERNAME_KEY, username);
            parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_PASSWORD_KEY, password);

        }

        chain.doFilter(parameterRequestWrapper, response);
    }
}

6.4 Verification

The request can be successful through POSTthe form submission method http://localhost:8080/process?username=Felordcn&password=12345&login_type=0. Or submit successfully in the following ways:

More ways only need to implement interface LoginPostProcessorinjectionPreLoginFilter

Nine integration JWT for login authentication

JWT is the abbreviation of JSON Web Token, which is currently the most popular cross-domain authentication solution.

The general process of Internet service certification is:

  1. The user sends the account number and password to the server
  2. After the server is authenticated, save the user's role, login time and other information in the current session
  3. At the same time, the server returns a session_id to the user (usually stored in a cookie)
  4. When the user sends the request again, the cookie containing the session_id is sent to the server
  5. The server receives the session_id, finds the session, and extracts user information

The above authentication mode has the following disadvantages:

  • Cookies do not allow cross-domain
  • Because each server must save the session object, the scalability is not good

The principle of JWT authentication is:

  1. The user sends the account number and password to the server
  2. After the server is authenticated, generate a token token and return it to the client (token can contain user information)
  3. When the user requests again, put the token in the request Authorizationheader
  4. After receiving the request, the server verifies that the token is legal and releases the request

The JWT token token can contain information such as user identity and login time, so that the login status holder changes from the server to the client, and the server becomes stateless; the token is placed in the request header to achieve cross-domain

Composition of JWT

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT consists of three parts:

  • Header
  • Payload
  • Signature

The form of expression is:Header.Payload.Signature

The Header part is a JSON object that describes the metadata of the JWT, usually as follows:

{
    
    
  "alg": "HS256",
  "typ": "JWT"
}

In the above code, algthe attribute indicates the signature algorithm (algorithm), the default is HMAC SHA256 (written as HS256); the typattribute indicates the type of the token (token), and the JWT token is uniformly written as JWT.

The above JSON object is converted into a string using the Base64URL algorithm

Payload

The Payload part is also a JSON object, which is used to store the actual data that needs to be passed. JWT specifies 7 official fields:

  • iss (issuer): Issuer
  • exp (expiration time): expiration time
  • sub (subject): subject
  • aud (audience): audience
  • nbf (Not Before): effective time
  • iat (Issued At): Issue time
  • jti (JWT ID): number

Of course, users can also define private fields.

This JSON object should also be converted into a string using the Base64URL algorithm

Signature

The Signature part is the signature of the first two parts to prevent data tampering

The signature algorithm is as follows:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
)

After calculating the signature, combine the three parts of Header, Payload and Signature into a string, and separate each part with "."

JWT authentication and authorization

Security is a security framework based on AOP and Servlet filters. In order to implement JWT to rewrite those methods and customize those filters, you need to first understand the filters that come with security. The security default filter chain is as follows:

  1. org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
  2. org.springframework.security.web.context.SecurityContextPersistenceFilter
  3. org.springframework.security.web.header.HeaderWriterFilter
  4. org.springframework.security.web.authentication.logout.LogoutFilter
  5. org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
  6. org.springframework.security.web.savedrequest.RequestCacheAwareFilter
  7. org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
  8. org.springframework.security.web.authentication.AnonymousAuthenticationFilter
  9. org.springframework.security.web.session.SessionManagementFilter
  10. org.springframework.security.web.access.ExceptionTranslationFilter
  11. org.springframework.security.web.access.intercept.FilterSecurityInterceptor

SecurityContextPersistenceFilter

This filter does two things:

  • When the user sends a request, extract the user information from the session object and save it in the securitycontext of the SecurityContextHolder
  • At the end of the current request response, put the user information saved in the securitycontext of the SecurityContextHolder into the session, so as to share data in the next request; at the same time, clear the securitycontext of the SecurityContextHolder

Since the session function is disabled, the filter has only one function left, which is to clear the securitycontext of the SecurityContextHolder. To illustrate why the securitycontext should be cleared: user 1 sends a request, which is processed by thread M, and when the response is completed, thread M puts it back into the thread pool; user 2 sends a request, and this request is also processed by thread M. Since the securitycontext is not cleared, it should The information of user 2 is stored but the information of user 1 is stored at this time, causing the user information to be inconsistent

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilterInherited from AbstractAuthenticationProcessingFilter, the processing logic is doFilterin the method:

  1. When the request is UsernamePasswordAuthenticationFilterintercepted, judge whether the request path matches the login URL, if not, continue to execute the next filter; otherwise, execute step 2
  2. Call attemptAuthenticationmethod for authentication. UsernamePasswordAuthenticationFilterThe method is rewritten attemptAuthentication, responsible for reading form login parameters, entrusting AuthenticationManagerauthentication, and returning an authenticated token (null means authentication failed)
  3. Determine whether the token is null, non-null means authentication is successful, null means authentication failed
  4. Called if the authentication is successful successfulAuthentication. This method puts the authenticated token into the securitycontext for subsequent request authorization. At the same time, this method reserves an extension point ( AuthenticationSuccessHandler.onAuthenticationSuccess方法) for processing after successful authentication
  5. If the authentication fails, it can also be extended uthenticationFailureHandler.onAuthenticationFailureto process after the authentication fails
  6. As long as the current request path matches the login URL, no matter whether the authentication succeeds or fails, the current request will be completed and the filter chain will not be executed.

UsernamePasswordAuthenticationFiltermethod attemptAuthentication, the execution logic is as follows:

  1. Get form parameters from the request. Because HttpServletRequest.getParameterthe method is used to obtain parameters, it can only handle requests whose Content-Type is application/x-www-form-urlencoded or multipart/form-data. If it is application/json, the value cannot be obtained
  2. Encapsulate the account and password obtained in step 1 into UsernamePasswordAuthenticationTokenan object, and create an unauthenticated token. UsernamePasswordAuthenticationTokenThere are two overloaded construction methods, which public UsernamePasswordAuthenticationToken(Object principal, Object credentials)create an unauthenticated token and public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)create an authenticated token
  3. Get the authentication manager AuthenticationManager, whose default implementation is ProviderManager, call it authenticatefor authentication
  4. ProviderManagerIt authenticateis a template method, it traverses all AuthenticationProvideruntil it finds a certain type of token that supports authentication AuthenticationProvider, calls AuthenticationProvider.authenticatethe method authentication, AuthenticationProvider.authenticateloads the correct account number and password for comparison and verification
  5. AuthenticationManager.authenticateThe method returns an authenticated token

AnonymousAuthenticationFilter

AnonymousAuthenticationFilterResponsible for creating anonymous tokens:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    
    
    if (SecurityContextHolder.getContext().getAuthentication() == null) {
    
    
        SecurityContextHolder.getContext().setAuthentication(this.createAuthentication((HttpServletRequest)req));
        if (this.logger.isTraceEnabled()) {
    
    
            this.logger.trace(LogMessage.of(() -> {
    
    
                return "Set SecurityContextHolder to " + SecurityContextHolder.getContext().getAuthentication();
            }));
        } else {
    
    
            this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext");
        }
    } else if (this.logger.isTraceEnabled()) {
    
    
        this.logger.trace(LogMessage.of(() -> {
    
    
            return "Did not set SecurityContextHolder since already authenticated " + SecurityContextHolder.getContext().getAuthentication();
        }));
    }

    chain.doFilter(req, res);
}

If the current user is not authenticated, an anonymous token will be created, and whether the user can read the resource FilterSecurityInterceptorwill be entrusted to the decision manager by the filter to determine whether it has permission to read

Implementation ideas

JWT authentication ideas:

  1. Use Security's native form authentication filter to verify username and password
  2. After the verification is passed, customize AuthenticationSuccessHandlerthe authentication success processor, and the token token will be generated by the processor

JWT authorization idea:

  1. The purpose of using JWT is to make the server stateless and not share data with sessions, so the session function of security should be disabled (http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS))
  2. When designing the token token data structure, the payload part should store user name and role information
  3. The token token has two functions:
    1. Authentication, the token sent by the user is legal, which means the authentication is successful
    2. Authorization, extract the role information after the token verification is successful, construct the authenticated token, put it into the securitycontext, and hand over the specific authority judgment to the security framework
  4. Implement a filter by yourself, intercept user requests, and implement the functions mentioned in (3)

Code implementation to create JWT tool class

<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>3.12.0</version>
</dependency>

We encapsulate the API provided by java-jwt to facilitate the creation, verification and extraction of claims

@Slf4j
public class JWTUtil {
    
    
    // 携带token的请求头名字
    public final static String TOKEN_HEADER = "Authorization";
    //token的前缀
    public final static String TOKEN_PREFIX = "Bearer ";
    // 默认密钥
    public final static String DEFAULT_SECRET = "mySecret";
    // 用户身份
    private final static String ROLES_CLAIM = "roles";
    // token有效期,单位分钟;
    private final static long EXPIRE_TIME = 5 * 60 * 1000;
    // 设置Remember-me功能后的token有效期
    private final static long EXPIRE_TIME_REMEMBER = 7 * 24 * 60 * 60 * 1000;

    // 创建token
    public static String createToken(String username, List role, String secret, boolean rememberMe) {
    
    

        Date expireDate = rememberMe ? new Date(System.currentTimeMillis() + EXPIRE_TIME_REMEMBER) : new Date(System.currentTimeMillis() + EXPIRE_TIME);
        try {
    
    
            // 创建签名的算法实例
            Algorithm algorithm = Algorithm.HMAC256(secret);
            String token = JWT.create()
                .withExpiresAt(expireDate)
                .withClaim("username", username)
                .withClaim(ROLES_CLAIM, role)
                .sign(algorithm);
            return token;
        } catch (JWTCreationException jwtCreationException) {
    
    
            log.warn("Token create failed");
            return null;
        }
    }

    // 验证token
    public static boolean verifyToken(String token, String secret) {
    
    
        try{
    
    
            Algorithm algorithm = Algorithm.HMAC256(secret);
            // 构建JWT验证器,token合法同时pyload必须含有私有字段username且值一致
            // token过期也会验证失败
            JWTVerifier verifier = JWT.require(algorithm)
                .build();
            // 验证token
            DecodedJWT decodedJWT = verifier.verify(token);
            return true;
        } catch (JWTVerificationException jwtVerificationException) {
    
    
            log.warn("token验证失败");
            return false;
        }

    }

    // 获取username
    public static String getUsername(String token) {
    
    
        try {
    
    
            // 因此获取载荷信息不需要密钥
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException jwtDecodeException) {
    
    
            log.warn("提取用户姓名时,token解码失败");
            return null;
        }
    }

    public static List<String> getRole(String token) {
    
    
        try {
    
    
            // 因此获取载荷信息不需要密钥
            DecodedJWT jwt = JWT.decode(token);
            // asList方法需要指定容器元素的类型
            return jwt.getClaim(ROLES_CLAIM).asList(String.class);
        } catch (JWTDecodeException jwtDecodeException) {
    
    
            log.warn("提取身份时,token解码失败");
            return null;
        }
    }
}

code implementation certification

Verify the account number and password UsernamePasswordAuthenticationFilter, without modifying the code

After the authentication is successful, a token needs to be generated and returned to the client, which we AuthenticationSuccessHandler.onAuthenticationSuccess方法implement through extensions

@Component
public class JWTAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
    
    
        ResponseData responseData = new ResponseData();
        responseData.setCode("200");
        responseData.setMessage("登录成功!");
		
        // 提取用户名,准备写入token
        String username = authentication.getName();
        // 提取角色,转为List<String>对象,写入token
        List<String> roles = new ArrayList<>();
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        for (GrantedAuthority authority : authorities){
    
    
            roles.add(authority.getAuthority());
        }
		
        // 创建token
        String token = JWTUtil.createToken(username, roles, JWTUtil.DEFAULT_SECRET, true);
        httpServletResponse.setCharacterEncoding("utf-8");
        // 为了跨域,把token放到响应头WWW-Authenticate里
        httpServletResponse.setHeader("WWW-Authenticate", JWTUtil.TOKEN_PREFIX + token);
		// 写入响应里
        ObjectMapper mapper = new ObjectMapper();
        mapper.writeValue(httpServletResponse.getWriter(), responseData);
    }
}

In order to unify the return value, we encapsulate an ResponseDataobject

Code Implementation Authorization

Customize a filter JWTAuthorizationFilterto verify the token. After the token verification is successful, the authentication is considered successful

@Slf4j
public class JWTAuthorizationFilter extends OncePerRequestFilter {
    
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    
    

        String token = getTokenFromRequestHeader(request);
        Authentication verifyResult = verefyToken(token, JWTUtil.DEFAULT_SECRET);
        if (verifyResult == null) {
    
    
            // 即便验证失败,也继续调用过滤链,匿名过滤器生成匿名令牌
            chain.doFilter(request, response);
            return;
        } else {
    
    
            log.info("token令牌验证成功");
            SecurityContextHolder.getContext().setAuthentication(verifyResult);
            chain.doFilter(request, response);
        }
    }
	
    // 从请求头获取token
    private String getTokenFromRequestHeader(HttpServletRequest request) {
    
    
        String header = request.getHeader(JWTUtil.TOKEN_HEADER);
        if (header == null || !header.startsWith(JWTUtil.TOKEN_PREFIX)) {
    
    
            log.info("请求头不含JWT token, 调用下个过滤器");
            return null;
        }

        String token = header.split(" ")[1].trim();
        return token;
    }
	
    // 验证token,并生成认证后的token
    private UsernamePasswordAuthenticationToken verefyToken(String token, String secret) {
    
    
        if (token == null) {
    
    
            return null;
        }
		
        // 认证失败,返回null
        if (!JWTUtil.verifyToken(token, secret)) {
    
    
            return null;
        }

        // 提取用户名
        String username = JWTUtil.getUsername(token);
        // 定义权限列表
        List<GrantedAuthority> authorities = new ArrayList<>();
        // 从token提取角色
        List<String> roles = JWTUtil.getRole(token);
        for (String role : roles) {
    
    
            log.info("用户身份是:" + role);
            authorities.add(new SimpleGrantedAuthority(role));
        }
        // 构建认证过的token
        return new UsernamePasswordAuthenticationToken(username, null, authorities);
    }
}
OncePerRequestFilter`保证当前请求中,此过滤器只被调用一次,执行逻辑在`doFilterInternal

Code implementation security configuration

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    private AjaxAuthenticationEntryPoint ajaxAuthenticationEntryPoint;
    @Autowired
    private JWTAuthenticationSuccessHandler jwtAuthenticationSuccessHandler;

    @Autowired
    private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler;

    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return new BCryptPasswordEncoder();
    }

    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.csrf().disable()
                .authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                .successHandler(jwtAuthenticationSuccessHandler)
                .failureHandler(ajaxAuthenticationFailureHandler)
                .permitAll()
                .and()
                .addFilterAfter(new JWTAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .exceptionHandling().authenticationEntryPoint(ajaxAuthenticationEntryPoint);

    }
}

The session function is canceled in the configuration, and the filter we defined is added to the filter chain; at the same time, ajaxAuthenticationEntryPointthe exception thrown when unauthenticated users access unauthorized resources is defined

@Component
public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
    
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
    
    
        ResponseData responseData = new ResponseData();
        responseData.setCode("401");
        responseData.setMessage("匿名用户,请先登录再访问!");

        httpServletResponse.setCharacterEncoding("utf-8");
        ObjectMapper mapper = new ObjectMapper();
        mapper.writeValue(httpServletResponse.getWriter(), responseData);
    }
}

Introduction to the filter chain

In the previous section, we mainly talked about the core components and core methods of Spring Security authentication and authorization. But when are these methods called? The answer is Filter and AOP. Spring Security controls access to permissions through various interceptors when we authenticate users and grant permissions.
For the protection of endpoints based on HttpRequest, we use a Filter Chain to protect; for the protection based on method calls, we use AOP to protect. This article focuses on the types of filter chains in Spring Security and how to implement authentication and authorization in filters.

Spring Security will add 15 filters for us by default. We can see the construction process of the filter chain SecurityFilterChain from the performBuild() method of WebSecurity (WebSecurity is an important object loaded by Spring Security, which will be described in the next section), and Handed over to the proxy of the FilterChainProxy object. We can see from the log in DefaultSecurityFilterChain, the default implementation class of SecurityFilterChain, that Spring Security consists of the following filters to form a filter chain:

Creating filter chain: any request, [
  org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7f353a0f,
  org.springframework.security.web.context.SecurityContextPersistenceFilter@4735d6e5,
  org.springframework.security.web.header.HeaderWriterFilter@314a31b0,
  org.springframework.security.web.csrf.CsrfFilter@4ef2ab73,
  org.springframework.security.web.authentication.logout.LogoutFilter@57efc6fd,
  org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@d88f893,
  org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@2cd388f5,
  org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7ea2412c,
  org.springframework.security.web.authentication.www.BasicAuthenticationFilter@2091833,
  org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4dad0eed,
  org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@16132f21,
  org.springframework.security.web.authentication.AnonymousAuthenticationFilter@1c93b51e,
  org.springframework.security.web.session.SessionManagementFilter@59edb4f5,
  org.springframework.security.web.access.ExceptionTranslationFilter@104dc1a2,
  org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1de0641b
]

The following is the function of each filter,

Among them, SecurityContextPersistenceFilter, UsernamePasswordAuthenticationFilter and FilterSecurityInterceptor correspond to the processing of SecurityContext, AuthenticationManager and AccessDecisionManager respectively.

[WebAsyncManagerIntegrationFilter] (asynchronously) provides integration of securityContext and WebAsyncManager.

The way is to set SecurityContext to Callable through the beforeConcurrentHandling(NativeWebRequest, Callable) method of SecurityContextCallableProcessingInterceptor.

In fact, it is to set the SecurityContext to the asynchronous thread so that it can also obtain the user context authentication information.

[SecurityContextPersistenceFilter] (synchronous method) Get information from SecurityContextRepository (the default implementation is HttpSessionSecurityContextRepository) and populate SecurityContextHolder (if not, create a new ThreadLocal SecurityContext) before the request, and clear SecurityContextHolder and update SecurityContextRepository after the request is completed.

In Spring Security, although the security context information is stored in the Session, the actual Filter should not directly operate the Session (the filter is generally responsible for the core processing flow, and the specific business implementation is usually handed over to other entity classes aggregated in it ), but use loadContext() and saveContext() in HttpSessionSecurityContextRepository to access the session.

[HeaderWriterFilter] is used to add some Headers to the http response, such as X-Frame-Options, X-XSS-Protection*, X-Content-Type-Options.

[CsrfFilter] is enabled by default, a filter used to prevent csrf attacks

[LogoutFilter] Filter to handle logout

[UsernamePasswordAuthenticationFilter] submits username and password in the form, and is encapsulated into a UsernamePasswordAuthenticationToken object for a series of authentication, which is mainly done through this filter, that is, calling AuthenticationManager.authenticate(). In the form authentication method, this is the most critical filter. The specific process is:

(1) Call the AbstractAuthenticationProcessingFilter.doFilter() method to execute the filter

(2) Call the UsernamePasswordAuthenticationFilter.attemptAuthentication() method

(3) Call the AuthenticationManager.authenticate() method (actually entrusted to the implementation class of AuthenticationProvider to handle)

[DefaultLoginPageGeneratingFilter] & [DefaultLogoutPageGeneratingFilter] If /login and login page are not configured, the system will automatically configure these two Filters.

[BasicAuthenticationFilter] Processes a HTTP request’s BASIC authorization headers, putting the result into the SecurityContextHolder.

[RequestCacheAwareFilter] internally maintains a RequestCache for caching request requests

[SecurityContextHolderAwareRequestFilter] This filter wraps the ServletRequest so that the request has a richer API (populates the ServletRequest with a request wrapper which implements servlet API security methods)

[AnonymousAuthenticationFilter] Anonymous identity filter, in order to be compatible with unlogged access, spring security has also gone through a set of authentication processes, which is just an anonymous identity. It is located after the identity authentication filter (eg UsernamePasswordAuthenticationFilter), which means that the filter of AnonymousAuthenticationFilter will be meaningful only after the above identity filter is executed and the SecurityContext still has no user information.

[SessionManagementFilter] and session-related filters maintain a SessionAuthenticationStrategy internally to perform any session-related activities, such as session-fixation protection mechanisms or checking for multiple concurrent logins.

[ExceptionTranslationFilter] exception translation filter, this filter itself does not handle exceptions, but the exceptions (AccessDeniedException and AuthenticationException) that occur during the authentication process are handed over to some classes maintained internally for processing. It
is located behind the entire springSecurityFilterChain and is used to convert the exceptions that appear in the entire link and convert them. As the name suggests, the conversion means that it does not process itself. Generally, it only handles two types of exceptions: AccessDeniedException and AuthenticationException.

It connects exceptions in Java and HTTP responses together, so that when handling exceptions, we don't have to think about what page to jump to when the password is wrong, or how to lock the account. We only need to pay attention to our own business logic and throw corresponding exceptions You can. If the filter detects an AuthenticationException, it will be handed over to the internal AuthenticationEntryPoint for processing. If an AccessDeniedException is detected, it is necessary to first determine whether the current user is an anonymous user. If it is an anonymous access, run the AuthenticationEntryPoint as before, otherwise it will be delegated to AccessDeniedHandler to process, and the default implementation of AccessDeniedHandler is AccessDeniedHandlerImpl.

[FilterSecurityInterceptor] This filter determines the permissions that access to a specific path should have, and what permissions or roles are required for these restricted resource accesses. These judgments and processing are all performed by this class.

(1) Call the FilterSecurityInterceptor.invoke() method to execute the filter

(2) Call the AbstractSecurityInterceptor.beforeInvocation() method

(3) Call the AccessDecisionManager.decide() method to decide whether to have the permission

reference

JSON Web Token Introductory Tutorial
Spring Security-5-Authentication Process Combing
Spring Security3 Source Code Analysis (5)-SecurityContextPersistenceFilter Analysis
Spring Security addFilter() Sequence problem
Form Data and Request Payload of front-end and back-end joint debugging, do you really understand?
Spring Boot 2 + Spring Security 5 + JWT Single Page Application Restful Solution
SpringBoot Practical - Chapter 10 Source Code
https://www.cnblogs.com/cjsblog/p/9184173.html
https://www.cnblogs.com /storml/p/10937486.html

The Path to Technical Freedom PDF is available at:

Realize your architectural freedom:

" Have a thorough understanding of the 8-figure-1 template, everyone can do the architecture "

" 10Wqps review platform, how to structure it? This is what station B does! ! ! "

" Alibaba Two Sides: How to optimize the performance of tens of millions and billions of data?" Textbook-level answers are coming "

" Peak 21WQps, 100 million DAU, how is the small game "Sheep a Sheep" structured? "

" How to Scheduling 10 Billion-Level Orders, Come to a Big Factory's Superb Solution "

" Two Big Factory 10 Billion-Level Red Envelope Architecture Scheme "

… more architecture articles, being added

Realize your responsive freedom:

" Responsive Bible: 10W Words, Realize Spring Responsive Programming Freedom "

This is the old version of " Flux, Mono, Reactor Combat (the most complete in history) "

Realize your spring cloud freedom:

" Spring Cloud Alibaba Study Bible " PDF

" Sharding-JDBC underlying principle and core practice (the most complete in history) "

" Get it done in one article: the chaotic relationship between SpringBoot, SLF4j, Log4j, Logback, and Netty (the most complete in history) "

Realize your linux freedom:

" Linux Commands Encyclopedia: 2W More Words, One Time to Realize Linux Freedom "

Realize your online freedom:

" Detailed explanation of TCP protocol (the most complete in history) "

" Three Network Tables: ARP Table, MAC Table, Routing Table, Realize Your Network Freedom!" ! "

Realize your distributed lock freedom:

" Redis Distributed Lock (Illustration - Second Understanding - The Most Complete in History) "

" Zookeeper Distributed Lock - Diagram - Second Understanding "

Realize your king component freedom:

" King of the Queue: Disruptor Principles, Architecture, and Source Code Penetration "

" The King of Cache: Caffeine Source Code, Architecture, and Principles (the most complete in history, 10W super long text) "

" The King of Cache: The Use of Caffeine (The Most Complete in History) "

" Java Agent probe, bytecode enhanced ByteBuddy (the most complete in history) "

Realize your interview questions freely:

4000 pages of "Nin's Java Interview Collection" 40 topics

Please go to the official account of "Technical Freedom Circle" to get the PDF file updates of the above Nien architecture notes and interview questions↓↓↓

Guess you like

Origin blog.csdn.net/crazymakercircle/article/details/130276558