Spring security series learning: Password Authentication

Copyright notice: This article is compiled by the team of Digital China cloud base. If you reprint it, please indicate the source.

Evolution of password storage security

In this section, we still accumulate some knowledge and can't wait for the little partners who want to code to bear it again and lay a good foundation to avoid some pits step by step.

At first, passwords were stored in clear text. Hackers only need to break through your database to get all accounts and passwords
In order to prevent password leakage, the password was hashed and encrypted when storing the password. This encryption is irreversible. In this way, even if hackers break through the database and get the records saved by the account and password, they can't get the real password, because the password stored in the database is the data after the hash encryption algorithm.
In order to solve this situation, hackers used the rainbow table: hash and encrypt some strings in advance, and then store them. Because of the upgrading of modern hardware devices, they use high-performance devices to perform multiple hash calculations per second and store the results in a huge library, that is, the rainbow table. If you get the password hash value in your database, compare it with the pre calculated hash value in the rainbow table. Once it is equal, it means that the plaintext of your password is the corresponding string in the rainbow table.
In order to cope with the rainbow table, a measure called adding salt is adopted, that is, the hash calculation does not completely rely on the password. It's a random number, stored somewhere in the system, and then hashed with the salt value and password, which makes it more difficult for hackers to crack, and the rainbow table doesn't work.
Further development will become adaptive. With the development of hardware devices, the encryption of our passwords has become more and more advanced. The so-called advanced here is actually more and more hashes. Specifically:

  • You can configure the number of iterations (MD5 ("password"))

  • Random salt values that can be configured

  • Iterations and salt values are stored in the database

  • More coding formats and algorithms: Bcrypt,Scrypt,pdkdf2, etc

In addition to increasing the complexity of password storage, this method can also artificially reduce the response speed of authentication. In this way, if hackers use code for violent cracking, without this complex encryption method, hackers may make thousands of attempts of violent cracking per second. Using this complex encryption method, hackers can only do dozens or even once a second.
There are several development trends in the future: for example, multi factor authentication. Authentication not only depends on password, but also depends on three-party factors, such as SMS verification code, email verification code and so on.
Another example is fingerprint and face recognition, but this method needs hardware support.
But now, passwords are still the mainstream of the Internet.

Cipher encoder

Spring security also makes security settings for password storage. Passwords stored in the database are encoded. Usually, we will use BCryptPasswordEncoder to encode. This encoder uses SHA-256 + random salt + key to encrypt the password. Sha series is a Hash algorithm, not an encryption algorithm. Using an encryption algorithm means that it can be decrypted (the same as encoding / decoding), but the process of Hash processing is irreversible.

When registering a user, use SHA-256 + random salt + key to hash the password entered by the user, get the hash value of the password, and then store it in the database.

When the user logs in, the password matching phase does not decrypt the password (because the password is not reversible after hash processing), but uses the same algorithm to hash the password entered by the user to obtain the hash value of the password, and then compares it with the hash value of the password queried from the database. If they are the same, the password entered by the user is correct.

Let's write a simple demo to configure the password encoder

Password encoder demo

Define a password encoder bean

@Bean
    public PasswordEncoder passwordEncoder(){

        //The id of the default encoding algorithm and the encoder corresponding to this id will be used for new password encoding
        String idForEncode = "bcrypt";
        //Multiple encoders to support
        Map encoders = new HashMap();
        encoders.put(idForEncode,new BCryptPasswordEncoder());

        //Multiple encoders can be used. When matching passwords, only one encoder can be matched
        //For example: for historical reasons, the SHA-1 code was used before. Now we want the new password to use bcrypt code
        //Old users use SHA-1, the old encoding format, and new users use bcrypt, the login process switches seamlessly
        //encoders.put("SHA-1",new MessageDigestPasswordEncoder("SHA-1"));
        return new DelegatingPasswordEncoder(idForEncode,encoders);
    }

DelegatingPasswordEncoder here allows passwords to be verified in different formats, providing the possibility of upgrading. This is to solve the problem that in the old database, the coding system method used for password is older, but with the development of computing power, if not migrated, the old coding system will be easily cracked, so migrating to a more secure coding standard is a necessary process.

Configure the password encoder into the security configuration

/**
 * `@EnableWebSecurity` Note: when the debug parameter is true, the debug mode is turned on, and more debug output will be generated
 *
 * @author Copper nitrate
 * @date 2021/6/2
 */
@EnableWebSecurity(debug = true)
@Slf4j
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    ...
    @Resource
    private PasswordEncoder passwordEncoder;
    ...

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //Read user information from configuration database
        auth.authenticationProvider(daoAuthenticationProvider());
    }

    /**
     * Configure DaoAuthenticationProvider
     * @return DaoAuthenticationProvider
     */
    private DaoAuthenticationProvider daoAuthenticationProvider(){
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        ...
        // Cipher encoder
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        ...
        return daoAuthenticationProvider;
    }
}

Test the password encryption method: passwordencoder encode():

Encrypted password format: {code id}xxxxx

So the question is, if we don't want to use the old encoding format, we need to use the new one. Can we migrate the old password to the new encoding format?

The answer must be yes. Use the updatePassword method provided in UserDetailsPasswordService. This will be explained later and will not be discussed here.

In fact, in addition to using spring security, we can also implement it ourselves. The specific idea is that when logging in, we will get the plaintext password. If the authentication is successful, we can encrypt and assemble it ourselves according to the format of {ID} encoded password and store it.

Password verification rules

The password verification rules are very complex. For example, the password is required to be case sensitive and not long. These rules are very painful for users and rule makers. Fortunately, we can use the Passay framework for verification, which has encapsulated some rules.

We encapsulate the verification logic in annotations, effectively separating the verification logic and business logic

For compound validation of more than two attributes, you can write an annotation applied to the class

Custom password verification demo

By introducing the Passay dependency and the springvaluation dependency, we use the springvaluation method to verify the password

Please understand the content of spring valuation by yourself. It is a dependency of common parameter verification

<properties>
        ...
        <passay.verion>1.6.0</passay.verion>
    </properties>

...
            <dependency>
            <groupId>org.passay</groupId>
            <artifactId>passay</artifactId>
            <version>${passay.verion}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

Write password annotation

/**
 * Password verification annotation
 * @author Copper nitrate
 * @date 2021/6/7
 */
@Target({ElementType.FIELD,ElementType.TYPE,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordValidator.class)
@Documented
public @interface ValidPassword {
    String message() default "Invalid Password";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

Then implement the ConstraintValidator interface, realize the verification logic, and write the password verifier

import org.passay.*;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;

/**
 * Password verifier
 * @author Copper nitrate
 * @date 2021/6/7
 */
public class PasswordConstraintValidator implements ConstraintValidator<ValidPassword,String> {
    @Override
    public void initialize(ValidPassword constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        PasswordValidator validator = new PasswordValidator(Arrays.asList(
                //Length rule, 8-30
                new LengthRule(8,30),
                //Character rules have at least one uppercase letter
                new CharacterRule(EnglishCharacterData.UpperCase,1),
                //Character rules have at least one lowercase letter
                new CharacterRule(EnglishCharacterData.LowerCase,1),
                //The character rule has at least one special character
                new CharacterRule(EnglishCharacterData.Special,1),
                //Illegal order rules do not allow five consecutive alphabetic letters, such as abcde
                new IllegalSequenceRule(EnglishSequenceData.Alphabetical,5,false),
                //Illegal sequence rules do not allow numbers in the order of 5 consecutive numbers, such as 12345
                new IllegalSequenceRule(EnglishSequenceData.Numerical,5,false),
                //Illegal order rule does not allow 5 consecutive keyboard order letters, such as asdfg
                new IllegalSequenceRule(EnglishSequenceData.USQwerty,5,false),
                //Space rule, no spaces
                new WhitespaceRule()
        ));
        return validator.validate(new PasswordData(s)).isValid();
    }
}

Write a comment on the field of the input parameter Dto

@NotNull
    @ValidPassword
    private String password;

    @NotNull
    @ValidPassword
    private String matchingPassword;

Start and call the registration interface (I don't care about how this interface is implemented. I'll mainly look at how the password verifier works when passing parameters)

At this time, 12345678 cannot pass the verification, because there are more than five numbers in numerical order, and there are no uppercase letters, lowercase letters, and special characters.

If it fails, an HTTP 400 error will be returned. At this time, it does not enter the interface method

Only when the set rules are fully met can it enter the interface.

Keywords: Spring Microservices security

Added by h123z on Wed, 12 Jan 2022 05:58:46 +0200