“This is my fifth day of the November Gwen Challenge.The final text challenge in 2021”.

Configuration-based authentication and authorization

  • newcontrollerpackage
  • Three new controller under the package, AdminController, AppController, UserController

  • Create test apis separately
/** ** /
@RequestMapping("/admin/api")
@RestController
public class AdminController {

    @RequestMapping(value = "/hi",method = RequestMethod.GET)
    public String  hi(a){
        return "hi,admin."; }}Copy the code
/** * simulates the public Api */
@RequestMapping("/app/api")
@RestController
public class AppController {

    @RequestMapping(value = "/hi", method = RequestMethod.GET)
    public String hi(a) {
        return "hi,app."; }}Copy the code
/** * simulates user related Api */
@RequestMapping("/user/api")
@RestController
public class UserController {

    @RequestMapping(value = "/hi", method = RequestMethod.GET)
    public String hi(a) {
        return "hi,user."; }}Copy the code
  • Configuring Resource Authorization
  • configurationconfigure
  • Modify the previous configuration
 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/api/**").hasRole("ADMIN")
                .antMatchers("/user/api/**").hasRole("USER")
                .antMatchers("/app/api/**").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginPage("/myLogin.html")
                // Specify the path to process the login request. Modify the path of the request. The default path is /login
                .loginProcessingUrl("/mylogin")
                // Make the login page unrestricted
                .permitAll()
                .and()
                .csrf().disable();

    }
Copy the code
  • antMatchers()A usedANTPatterns ofURLmatcher
  • ?Matches any single character
  • *Matches 0 or any number of characters
  • **Represents directories that match 0 or more
  • Restart the service
  • accessapi http://localhost:8080/app/api/hi
  • The access success page is displayedhi,app.
  • accessapi http://localhost:8080/user/api/hi
  • The login page is displayed
  • Enter a user-defined user name and password
  • If the login succeeds, error 403 is reported, indicating that the authorization fails
  • The authentication succeeded, but the authorization failed. Procedure

Because we configure.antmatchers (“/user/ API /**”).hasrole (” user “), which requires users to have user role permissions

  • Modifying a Configuration Fileapplication.yml
spring:
  security:
    user:
      name: caoshenyang
      password: 123456
      roles: USER
Copy the code
  • Add USER permission to a USER
  • Restart the project
  • accessapi http://localhost:8080/user/api/hi
  • After successful login, the page is displayedhi,user.

Access API http://localhost:8080/admin/api/hi

In the same situation

Modify the configuration file application.yml

Add ADMIN permission to the user

Restart the project

The access is normal, and the page displays hi and admin.

2. Memory-based multi-user Settings

1. Implement the user-defined UserDetailsService

@Bean
public UserDetailsService userDetailsService(a){
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    / / 698 d51a19d8a121ce581499d7b701668 proclaimed in 111 after the encryption MD5 encryption
    / / it is clear
    manager.createUser(User.withUsername("aa").password("{MD5}698d51a19d8a121ce581499d7b701668").roles("USER").build());
    manager.createUser(User.withUsername("bb").password("{noop}222").roles("USER").build());

    return manager;
}
Copy the code

Note: Encryption must be configured for springSecurity 5.x or later, the following exceptions occur

There is no PasswordEncoder mapped for the id "null"
Copy the code

The springSecurity5. x encryption mode is configured in the {Id}password format

Take a look at the encryption that comes with PasswordEncoderFactories

public class PasswordEncoderFactories {
    public static PasswordEncoder createDelegatingPasswordEncoder(a) {
        String encodingId = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap();
        encoders.put(encodingId, new BCryptPasswordEncoder());
        encoders.put("ldap".new LdapShaPasswordEncoder());
        encoders.put("MD4".new Md4PasswordEncoder());
        encoders.put("MD5".new MessageDigestPasswordEncoder("MD5"));
        encoders.put("noop", NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2".new Pbkdf2PasswordEncoder());
        encoders.put("scrypt".new SCryptPasswordEncoder());
        encoders.put("SHA-1".new MessageDigestPasswordEncoder("SHA-1"));
        encoders.put("SHA-256".new MessageDigestPasswordEncoder("SHA-256"));
        encoders.put("sha256".new StandardPasswordEncoder());
        encoders.put("argon2".new Argon2PasswordEncoder());
        return new DelegatingPasswordEncoder(encodingId, encoders);
    }

    private PasswordEncoderFactories(a) {}}Copy the code
  • Restart the
  • Enter your account password
  • Login successful
  • This configuration overrides the previous oneapplication.ymlThe configuration in

2. Through congfigure

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .passwordEncoder(NoOpPasswordEncoder.getInstance())
                .withUser("tom").password("111").roles("ADMIN"."USER")
                .and()
                .withUser("lisi").password("222").roles("USER");
    }
Copy the code

This configuration overwrites the original application. Yml configuration and the custom UserDetailsService configuration. You can choose either one

Authorization and authentication based on the default database model

  • Check the InMemoryUserDetailsManager source
  • UserDetailsManager interface is realized

  • Select the UserDetailsManager interface, Ctrl+H

Another implementation class, JdbcUserDetailsManager, was found to implement this interface

You can guess from the name that the implementation class connects to the database via JDBC

  • Introduce JDBC and MYSQL dependencies for the project
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.20</version>
</dependency>
Copy the code
  • application.ymlConfigure data connection parameters
spring:
  datasource:
  	driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/springSecurityDemo? useUnicode=true&&characterEncoding=utf8&&useSSL=false&&serverTimezone=Asia/Shanghai
Copy the code
  • Create the database springSecurityDemo

SpringSecurity provides a default database model

public JdbcUserDetailsManagerConfigurer<B> withDefaultSchema(a) {
		this.initScripts.add(new ClassPathResource(
				"org/springframework/security/core/userdetails/jdbc/users.ddl"));
		return this;
	}
Copy the code

Address in org/springframework/security/core/populated userdetails/JDBC/users. The DDL

create table users(username varchar_ignorecase(50) not null primary key.password varchar_ignorecase(500) not null,enabled boolean not null);
create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null.constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);
Copy the code

Note: MySql does not support the varchar_ignorecase type. Change it to varchar

create table users(username VARCHAR(50) not null primary key.password VARCHAR(500) not null,enabled boolean not null);
create table authorities (username VARCHAR(50) not null,authority VARCHAR(50) not null.constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);
Copy the code
  • Execute the construction statement
  • Create two tables

Authorities table

The users table

  • Build an instance of JdbcUserDetailsManager and let SpringSecurity manage users using a database, similar to memory, except that the user information comes from the database
  • The introduction of the DataSource
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/api/**").hasRole("ADMIN")
                .antMatchers("/user/api/**").hasRole("USER")
                .antMatchers("/app/api/**").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginPage("/myLogin.html")
                // Specify the path to process the login request. Modify the path of the request. The default path is /login
                .loginProcessingUrl("/mylogin")
                .permitAll()
                .and()
                .csrf().disable();
    }

    /** * User Settings based on default database data model */
    @Bean
    public UserDetailsService userDetailsService(a){
        JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
        manager.setDataSource(dataSource);
        
        / / the MD5 encryption article 111 698 d51a19d8a121ce581499d7b701668 after encryption
        manager.createUser(User.withUsername("aa").password("{MD5}698d51a19d8a121ce581499d7b701668").roles("USER").build());
        manager.createUser(User.withUsername("bb").password("{noop}222").roles("USER").build());
        returnmanager; }}Copy the code
  • Restart the project
  • accessapi http://localhost:8080/user/api/hi
  • Enter a user nameaa password111
  • Access to success

The database is found to store this information

Also notice that we prefix the permissions we set with **ROLE_**

  • View the JdbcUserDetailsManager source code

The discovery defines a large number of SQL execution statements

CreateUser () is equivalent to executing the following SQL statement

insert into users (username, password, enabled) values (? ,? ,?)
Copy the code

There is a problem in the above code. Whenever we restart the project, we will create a user, but username is the primary key, and we will get a primary key conflict exception

nested exception is java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'aa' for key 'PRIMARY'
Copy the code
  • Make a little change
/** * User Settings based on default database data model */
    @Bean
    public UserDetailsService userDetailsService(a) {
        JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
        manager.setDataSource(dataSource);
        if(! manager.userExists("aa")) {
            / / the MD5 encryption article 111 698 d51a19d8a121ce581499d7b701668 after encryption
            manager.createUser(User.withUsername("aa").password("{MD5}698d51a19d8a121ce581499d7b701668").roles("USER").build());

        }
        if(! manager.userExists("bb")) {
            manager.createUser(User.withUsername("bb").password("{noop}222").roles("USER").build());
        }
        return manager;
    }
Copy the code
  • Restart the project

  • The normal operation

  • Add administrators by modifying database data

  • accessapi http://localhost:8080/admin/api/hi

Enter the administrator user name and password defined by yourself. The access succeeds

4. Authorization and authentication based on custom database model

In project development, the default database model was too simple to meet the needs of our business. SpringSecurity also supports authorization and authentication for custom database models.

  • Let’s plug in the custom database model
  • The persistence layer framework uses MyBatis-Plus
  • Use the Lombok plug-in to simplify code
  • Introduce dependencies for the project
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.0</version>
</dependency>
    
Copy the code

1. Implement populated UserDetails

In the previous example, by implementing the UserDetailsService and annotating it into the Spring container, Spring Security will automatically discover and use it. UserDetailsService only implements a loadUserByUsername() method. Used to get the UserDetails object, which contains a list of information needed for validation

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword(a);

    String getUsername(a);

    boolean isAccountNonExpired(a);

    boolean isAccountNonLocked(a);

    boolean isCredentialsNonExpired(a);

    boolean isEnabled(a);
}
Copy the code

So no matter what the data source is, or how the database structure changes, we just need to construct a UserDetails.

1.1 Implement your own user table

CREATE TABLE `t_user` (
	`id` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT,
	`username` VARCHAR ( 60 ) NOT NULL.`password` VARCHAR ( 60 ) NOT NULL.`enable` TINYINT ( 4 ) NOT NULL DEFAULT '1' COMMENT 'User available'.`roles` text CHARACTER SET utf8mb4 COMMENT 'User roles, separated by commas',
	PRIMARY KEY ( `id` ), KEY ( `username`))ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
Copy the code
  • Add an index to the username field to speed up search
  • Insert two pieces of data manually

1.2 Writing our User entity

  • createentityPackage storage entity
  • Create the User entity class
@Data
public class User {
    private Long id;
    private String username;
    private String password;
    private String roles;
    private boolean enable;
}
Copy the code
  • Implement populated UserDetails
@Data
public class User implements UserDetails {
    private Long id;
    private String username;
    private String password;
    private String roles;
    private boolean enable;

    private List<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public boolean isAccountNonExpired(a) {
        return true;
    }

    @Override
    public boolean isAccountNonLocked(a) {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired(a) {
        return true;
    }

    @Override
    public boolean isEnabled(a) {
        return this.enable; }}Copy the code

Overriding methods

  • IsAccountNonExpired (), isAccountNonLocked(), isCredentialsNonExpired()It won’t take all of them to returntrue
  • IsEnabled () corresponds to the Enable field
  • GetAuthorities () originally corresponding to the roles field, but defined its own structural changes, so we first created a new authorities and filled it in later.

1.3 Preparation of the Persistence Layer

  • createmapperpackage
  • Create UserMapper
@Component
public interface UserMapper extends BaseMapper<User> {

    @Select("SELECT * FROM t_user WHERE username = #{username}")
    User findByUserName(@Param("username") String username);
}
Copy the code
  • Start class to add package scan annotations
@SpringBootApplication
@MapperScan("com.yang.springsecurity.mapper")
public class SpringSecurityApplication {

    public static void main(String[] args) { SpringApplication.run(SpringSecurityApplication.class, args); }}Copy the code
  • Write business code
  • createservicepackage
  • Create MyUserDetailsService to implement UserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Query user information
        User user = userMapper.selectByUsername(username);
        if (user==null) {throw new UsernameNotFoundException(username+"User does not exist");
        }
        // Repopulate roles
        user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
        returnuser; }}Copy the code

Note: Encryption must be configured for springSecurity 5.x or later, the following exceptions occur

There is no PasswordEncoder mapped for the id "null"
Copy the code
  • Configure the default encryption mode
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;
    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/api/**").hasRole("ADMIN")
                .antMatchers("/user/api/**").hasRole("USER")
                .antMatchers("/app/api/**").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginPage("/myLogin.html")
                // Specify the path to process the login request. Modify the path of the request. The default path is /login
                .loginProcessingUrl("/mylogin")
                .permitAll()
                .and()
                .csrf().disable();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailsService).passwordEncoder(NoOpPasswordEncoder.getInstance()); }}Copy the code
  • Restart the project
  • accessapi http://localhost:8080/admin/api/hi
  • Enter the user name and password
  • Access to success

So far, we have realized the authorization and authentication of the customized database model. Later, we can enrich the authentication logic and strengthen security according to the needs of the project

There is always a question here, why do we need to prefix our database permissions with ROLE_?

Look at the source code for the hasRole() method

private static String hasRole(String role) {
		Assert.notNull(role, "role cannot be null");
		if (role.startsWith("ROLE_")) {
			throw new IllegalArgumentException(
					"role should not start with 'ROLE_' since it is automatically inserted. Got '"
							+ role + "'");
		}
		return "hasRole('ROLE_" + role + "')";
	}
Copy the code

If you do not want to match this prefix, use the **hasAuthority()** method instead