Control of user login and access authority of the project (5)

13. User Login-Preparation

When developing the registration function, SecurityConfigconfigure the following code in the class:

@Override
protected void configure(HttpSecurity http) throws Exception {
    
    
    http.csrf().disable();
}

The function of the above code is to close cross-domain attacks. If there is no above code, an error will occur when executing asynchronous requests!

Once the above code is added, but no more detailed configuration is added, Spring Security's login interception will not take effect! In order to facilitate the development of the login function, the above code is temporarily removed (deleted or added as a comment).

In addition, SecurityConfigthere are:

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

The function of the above code is to create a password encryptor object and give it to the Spring container for management, so that when you need to perform password encryption, you can directly install the password encryptor!

At present, in order to ensure that you can log in correctly, you need to remove the above password encryptor, because after the registration function is developed, the password after the user registration is successfully stored in the database in the form of cipher text, and a {bcrypt}prefix is added to declare encryption When using the algorithm, Spring Security will automatically use the above code assembly to PasswordEncoderperform one encryption, and also perform another encryption because of the {bcrypt}prefix, which will cause the login authentication to fail!

[Summary] You can only choose one of the two methods of using ${bcrypt}prefixes for ciphertext and letting the Spring container manage BcryptPasswordEncoderthem!

Once the above code is removed, there will be no PasswordEncoderobjects in the Spring container . However, if UserServiceImplyou still need to use it, you should adjust it to a self-created mode, namely:

// @Autowired // 需要去除自动装配注解
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

14. User login-simulated login based on memory authentication

First remove the Spring Security username and password configured in application.properties !

Then, override the method in the SecurityConfigclass (inherited from WebSecurityConfigurerAdapterthe configuration class) protected void configure(AuthenticationManagerBuilder auth), and configure the allowed user name, password, and account permissions in this method:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
    auth.inMemoryAuthentication()
            .withUser("java")
            .password("{bcrypt}$2a$10$tsM03ULkiifEpSCWtQ5Mq.yrLZIPKVr5vHwU1FGjtT9B1vPlswa.C")
            .authorities("/test");
}

The original text of the above ciphertext password is 1234.

Note: When configuring the above code, you must call authorities()to configure the authorization range. If there is no configuration, the startup will fail. Since the permissions required for each request have not been configured currently, the above range can be temporarily represented by any string.

15. User login-UserDetailsService interface

Spring Security defines an UserDetailsServiceinterface, and there are abstract methods in the interface:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

The function of this method is: given a user name, you need to return user details ( UserDetailsobjects of type). After Spring Security obtains the user details, it will automatically complete the user identity verification, including the user authority information after the verification is successful. What the framework deals with, as a developer, you only need to solve the problem of "getting user details based on user name"!

You can cn.tedu.straw.portal.securitycreate a UserDetailsServiceImplclass in the package , implement the above interface, and simulate the realization of obtaining user data:

@Component
public class UserDetailsServiceImpl implements UserDetailsService {
    
    

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        // 假设正确的用户名是security
        // 假设用户名是正确值:security
        if ("security".equals(username)) {
    
    
            // 通过Spring-Security提供的User类来构建UserDetails对象
            UserDetails userDetails = User.builder()
                    .username("security")
                    .password("{bcrypt}$2a$10$tsM03ULkiifEpSCWtQ5Mq.yrLZIPKVr5vHwU1FGjtT9B1vPlswa.C")
                    .authorities("test")
                    .build();
            return userDetails;
        }
        return null;
    }

}

Note: The above classes must be in the package scanned by the component, and @Componentannotated, the Spring framework will automatically create and manage the objects of the above classes, and then you can directly assemble the objects of this class!

Then, go back to the SecurityConfigclass and apply the objects of the above class:

@Autowired
UserDetailsServiceImpl userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
    auth.userDetailsService(userDetailsService);
}

Note: The above global attributes are declared as UserDetailsServiceImpltypes and cannot be declared as interface types, because there are more than one object of interface type.

16. User login-query database to verify login

First IUserServiceadd abstract methods to the interface:

UserDetails login(String username);

Strictly speaking, the above method is not a "login" method, but a method of "getting user details". You don't even know whether the login is successful or not. Therefore, there is no password in the parameter list. In the future, Spring Security will obtain the above The object returned by the method, and verify that the password is correct, etc.

Then, UserServiceImplrewrite the above abstract method in the implementation class:

@Override
public UserDetails login(String username) {
    
    
    // 根据参数username查询用户信息
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("username", username);
    User user = userMapper.selectOne(queryWrapper);
    // 判断查询结果是否为null,即:有没有这个用户
    // 注意:后续的验证和最终的界面是由Spring-Security显示的,此处不要抛出异常
    if (user == null) {
    
    
        return null;
    }
    // 组织“用户详情”对象
    // TODO 未完
    UserDetails userDetails = org.springframework.security.core.userdetails.User
            .builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .authorities("test")
            .build();
    return userDetails;
}

After completion, write and execute unit tests under src/test/javacn.tedu.straw.portal.service.UserServiceTests :

@Test
void login() {
    
    
    String username = "13988139111";
    UserDetails userDetails = userService.login(username);
    log.debug("login, user details={}", userDetails);
}

If the test passes, the UserDetailsobject obtained above can be applied to UserDetailsServiceImplthe return value:

@Component
public class UserDetailsServiceImpl implements UserDetailsService {
    
    

    @Autowired
    private IUserService userService;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        return userService.login(username);
    }

}

17. User login-about access control (equivalent to interceptor)

In SecurityConfigthe rewrite protected void configure(HttpSecurity http)method:

@Override
protected void configure(HttpSecurity http) throws Exception {
    
    
    // 准备白名单,是不需要登录就可以访问的路径
    String[] antMatchers = {
    
    
        "/index.html"
    };
    // 授权设置,是相对固定的配置
    // csrf().disable() > 关闭跨域攻击
    // authorizeRequests() > 对请求进行授权
    // antMatchers() > 配置访问白名单
    // permitAll() > 对白名单中的路径进行授权
    // anyRequest() > 其它的请求
    // authenticated() > 仅经过授权的允许访问,也可以理解为“未被授权将不允许访问”
    // and.formLogin() > 未被授权的将通过登录表单进行验证登录并授权
    http.csrf().disable()
            .authorizeRequests()
            .antMatchers(antMatchers).permitAll()
            .anyRequest().authenticated()
            .and().formLogin();
}

Regarding the above code:

  • httpThe configuration of the chain method of calling the parameter object is relatively fixed, you can try to understand it, or you can apply it directly;

  • The above call is antMatchers()equivalent to configuring the whitelist when using the SpringMVC interceptor. In the method parameters, all paths that need to be directly released (locations that can be accessed without logging in) should be added, for example:

    • String[] antMatchers = {
              
              
      	"/index.html",
      	"/bower_components/**",
          "/css/**",
          "/img/**",
          "/js/**"
      };
      

So far, the homepage index.htmlcan be accessed without logging in, while other pages are temporarily allowed to be accessed without logging in!

18. User login-replace custom login page

First, add Thymeleaf dependency to the project:

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

The custom login page will be designed as an HTML template page. When the login URL is requested, it will be forwarded to the HTML template page, and the templates folder will be created under the project's src/main/resoueces , which is used by the SpringBoot project by default The template page folder does not need to be configured. When forwarding, the HTML template file will be queried in this folder by default. When the folder is created, drag the login.html file in the static folder to the templates folder .

Next, customize the controller and design the request path of the login page. When processing the request of the path, it is directly forwarded to the **/templates/login.html** file. Because Thymeleaf has already configured the prefix during integration /templates/, The suffix is ​​configured in order .html, so the view name returned in the controller is login:

@Controller
public class SystemController {
    
    

    @GetMapping("/login.html")
    public String login() {
    
    
        return "login";
    }

    // 适用于使用@RestController时
    // public ModelAndView login() {
    
    
    //    return new ModelAndView("login");
    // }

}

Then, the request path designed above needs to be added to the configured whitelist.

After completion, restart the project, and http://localhost:8080/login.htmlyou can see the customized login page through the browser .

At present, http://localhost:8080/login.htmlyou can access the customized login page through, and http://localhost:8080/loginyou can also access the built-in login page of Spring Security through , that is to say, the two login pages coexist! It should be configured so that Spring Security always automatically uses our customized login page! Need SecurityConfigto add in the configuration of the class:

package cn.tedu.straw.portal.security;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

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

//    @Override
//    protected void configure(HttpSecurity http) throws Exception {
    
    
//        http.csrf().disable();
//    }

    @Autowired
    UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.userDetailsService(userDetailsService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        // 登录页面的URL
        String loginPageUrl = "/login.html";
        // 处理登录请求的URL
        String loginProcessingUrl = "/login";
        // 登录失败后的URL
        String loginFailureUrl = "/login.html?error";
        // 登录成功后的URL
        String loginSuccessUrl = "/index.html";
        // 退出登录的URL
        String logoutUrl = "/logout";
        // 退出登录成功后的URL
        String logoutSuccessUrl = "/login.html?logout";
        // 准备白名单,是不需要登录就可以访问的路径
        String[] antMatchers = {
    
    
                loginPageUrl,
                "/index.html",
                "/bower_components/**",
                "/css/**",
                "/img/**",
                "/js/**"
        };
        // 授权设置,是相对固定的配置
        // csrf().disable() > 关闭跨域攻击
        // authorizeRequests() > 对请求进行授权
        // antMatchers() > 配置访问白名单
        // permitAll() > 对白名单中的路径进行授权
        // anyRequest() > 其它的请求
        // authenticated() > 仅经过授权的允许访问,也可以理解为“未被授权将不允许访问”
        // and.formLogin() > 未被授权的将通过登录表单进行验证登录并授权
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers(antMatchers).permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .loginPage(loginPageUrl)
                .loginProcessingUrl(loginProcessingUrl)
                .failureUrl(loginFailureUrl)
                .defaultSuccessUrl(loginSuccessUrl)
                .and().logout()
                .logoutUrl(logoutUrl)
                .logoutSuccessUrl(logoutSuccessUrl);
    }

}

19. About Access Control

First prepare the URL for testing:

@RestController
@RequestMapping("/test")
public class TestController {
    
    

    @Autowired
    private IUserService userService;

    // http://localhost:8080/test/user/1
    @GetMapping("/user/{id}")
    public User getUserById(@PathVariable("id") Integer id) {
    
    
        return userService.getById(id);
    }

}

When designing the request path, you can use {}a name in the request path to represent a variable. Later, when the client submits the request, the {}placeholder corresponding to the location can be any data, and it will be matched!

When a {}placeholder is used in the request path , in the parameter list of the method to process the request, add an @PathVariableannotation before the declaration of the parameter to get the value of the placeholder!

Put the core parameters in the URL, which is a RESTful style API.

After completion, you can access it through http://localhost:8080/test/user/1.

If you need to restrict access to the above URL, for example, some users can access, but some other users cannot access, you can design a "authorization string" by yourself, such as "a"or "hello"etc.! It is generally recommended to use the URL style to define access permissions, such as using "test:user:info"or "/user/user/info".

Note: The design of the permission string has nothing to do with the design of the URL!

An @PreAuthorizeannotation can be configured before the method of processing the request to declare that "you must have certain permissions to access the requested path", for example:

@GetMapping("/user/{id}")
@PreAuthorize("hasAuthority('test:user:info')")
public User getUserById(@PathVariable("id") Integer id) {
    
    
    return userService.getById(id);
}

About the above annotation configuration:

  • The name of the annotation @PreAuthorizemeans "verify permissions before processing the request";
  • The annotation attribute hasAuthoritymeans "need to have certain authority";
  • The annotation attribute test:user:infois a custom permission string, which is just an identification.

Then, you need SecurityConfigto add an @EnableGlobalMethodSecurity(prePostEnabled = true)annotation before the declaration of the class to allow the access permission check! E.g:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    // 忽略类中的代码
    
}

If you visit http://localhost:8080/test/user/1 again, it will cause it because you don’t have permission, AccessDeniedExceptionand because the unified exception handling mechanism is used in the current project, all unknown exceptions will also be processed, you can see JSON data representing error information.

You can try to add permissions directly so that users can access the above URL. For example, in the business layer implementation class, when processing "get user details", encapsulate the matching permission string for the user details (the permission string required by the controller) Just keep the same):

// 权限字符串数组
String[] authorities = {
    
    
    "test:user:info"
};
// 组织“用户详情”对象
// TODO 未完
UserDetails userDetails = org.springframework.security.core.userdetails.User
        .builder()
        .username(user.getUsername())
        .password(user.getPassword())
        .authorities(authorities)
        .build();

According to the user's id, find out the permissions that the user has ( List<Permission>).

Guess you like

Origin blog.csdn.net/qq_44273429/article/details/107601603