Use shiro to encrypt and store passwords in the database (java+springboot+shiro)

Use shiro to encrypt and store passwords in the database (java+springboot+shiro)

Introduction: This article explains how to encrypt and store passwords in the database.

build database

Shiro is a Java security framework that provides security-related functions such as identity authentication, authorization, and encryption.

Encrypting user passwords in Shiro can be done by implementing the org.apache.shiro.authc.credential.CredentialsMatcher interface. In general, you can use existing encryption algorithms such as MD5, SHA, etc. to encrypt passwords, or you can customize the encryption method.

Here is an example of a simple user table:

CREATE TABLE users (
  id bigint(20) NOT NULL AUTO_INCREMENT,
  username varchar(50) NOT NULL,
  password varchar(100) NOT NULL,
  salt binary(16) DEFAULT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY idx_username (username)
);
INSERT INTO users (username, password, salt) VALUES ('user1', 'password1', '1234567890123456');
INSERT INTO users (username, password, salt) VALUES ('user2', 'password2', '1234567890123456');

This table contains the following four fields:

  1. id: Indicates the ID of the user, which is an auto-incrementing primary key used to uniquely identify each user.

  2. username: Indicates the username, which is a character string with a maximum length of 50 characters and cannot be empty. It is used to log in and display user information.

  3. password: Indicates the hash value of the password, which is a character string with a maximum length of 100 and cannot be empty, and is used to verify user identity.

  4. salt: Indicates the salt value, which is a binary data with a length of 16 and can be NULL, which is used to enhance the security of the password.

Note that in this table, we added a unique index called idx_username to the username column. This index is used to ensure the uniqueness of the user name and avoid repeated registration of the same user name. At the same time, we set the id column as an auto-increment primary key so that the user's ID value is automatically generated and used as a unique identifier.

When implementing functions such as user registration and login, we can query, insert, update, and delete the table through SQL statements to manage and maintain user information.

code demo

Principle demonstration

In the backend code, the specific implementation of encrypting the user's password will depend on the encryption algorithm you choose and the tool library you use. Here's one possible implementation:

  1. First, when a user registers, the plaintext password is converted to a byte array.

  2. Choose an appropriate encryption algorithm for password encryption. For example, you can use the SimpleHash class provided by the Apache Shiro framework to generate encrypted passwords. The sample code is as follows:

algorithmName: Specifies the name of the encryption algorithm used. Here we have chosen the MD5 algorithm.

hashIterations: Specifies the number of encryption times. The more encryption times, the more difficult the password is to crack, but it will also increase the calculation time. Here we only encrypt once.

salt: Salt value, you can choose to customize or use the default value. A salt is a random number used to enhance the security of a password. If no salt is specified, the default is used.

plaintextPassword: plaintext password.

hashedPassword: encrypted password. Use the SimpleHash object's toString() method to convert it to string form.

String algorithmName = "MD5"; // 选择 MD5 算法
int hashIterations = 1; // 加密次数
Object salt = null; // 盐值,可以选择自定义或者使用默认值
Object hashedPassword = new SimpleHash(algorithmName, plaintextPassword, salt, hashIterations);
  1. Store the encrypted password in the database. When saving passwords, do not store plaintext passwords directly in the database, but store encrypted passwords instead.

  2. When the user logs in, compare whether the plaintext password entered by the user is consistent with the encrypted password stored in the database. If they are consistent, the authentication passes; otherwise, the authentication fails.

It should be noted that the selection of the encryption algorithm and the setting of the number of encryption times need to be adjusted according to actual needs. In addition, the use of a salt value can increase the difficulty of cracking the password. It is recommended to set a random salt value during encryption.

project code

create project

project structure

insert image description here

insert image description here

insert image description here
insert image description here

pom.xml

Shiro's dependencies

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-crypto-hash</artifactId>
	<version>1.11.0</version>
</dependency>

complete pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.10</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>shiroDemo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>shiroDemo</name>
	<description>shiroDemo</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<version>2.7.6</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-jpa</artifactId>
			<version>2.5.6</version>
		</dependency>

		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-core</artifactId>
			<version>5.6.3.Final</version>
		</dependency>


		<dependency>
			<groupId>javax.persistence</groupId>
			<artifactId>javax.persistence-api</artifactId>
			<version>2.2</version>
		</dependency>

		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.3.0</version>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.3.0</version>
		</dependency>
		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.4.2</version>
		</dependency>

		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-core</artifactId>
			<version>1.8.0</version>
		</dependency>

	</dependencies>


	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>



poo

  • User
@Data // 使用 Lombok 简化实体类代码
@AllArgsConstructor
@NoArgsConstructor
public class Users {
    
    
    @TableId(type = IdType.AUTO) // 指定 ID 字段为自增主键
    private Long id;

    @TableField("username") // 指定该字段映射到数据库表中的 username 列
    private String username;

    @TableField("password") // 指定该字段映射到数据库表中的 password 列
    private String password;

    @TableField(value = "salt", exist = false) // 指定该字段不与数据库表中的任何列进行映射
    private byte[] salt;

    public Users(String username, String password, byte[] salt) {
    
    
        this.username = username;
        this.password = password;
        this.salt = salt;
    }

    public Users(String username, String password) {
    
    
        this.username = username;
        setPassword(password);
    }

    public void setPassword(String plaintextPassword) {
    
    
        String algorithmName = "MD5"; // 选择 MD5 算法
        int hashIterations = 1; // 加密次数
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[16];
        random.nextBytes(salt);  // 随机生成盐值

        Object hashedPassword = new SimpleHash(algorithmName, plaintextPassword, new SimpleByteSource(salt), hashIterations);
        this.password = hashedPassword.toString();
        this.salt = salt; // 将盐值保存到对象中
    }

}
  • Result
@Data
public class Result<T> {
    
    
    private Integer code; // 状态码
    private String message; // 提示信息
    private T data; // 响应数据

    public Result() {
    
    }

    public Result(Integer code, String message) {
    
    
        this.code = code;
        this.message = message;
    }

    public Result(Integer code, String message, T data) {
    
    
        this.code = code;
        this.message = message;
        this.data = data;
    }
}
  • LoginRequest
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginRequest {
    
    

    private String username;

    private String password;

}

UserController

@RestController
@RequestMapping("/user")
public class UserController {
    
    

    @Autowired
    private UserService userService;

    // 新增用户
    @PostMapping("")
    public Result<Users> addUser(@RequestBody Users user) {
    
    
        boolean success = userService.register(user.getUsername(), user.getPassword());
        if (success) {
    
    
            return new Result<>(200, "新增用户成功", user);
        } else {
    
    
            return new Result<>(500, "新增用户失败");
        }
    }

    // 根据 ID 删除用户
    @DeleteMapping("/{id}")
    public Result<Void> deleteUserById(@PathVariable Long id) {
    
    
        boolean success = userService.removeById(id);
        if (success) {
    
    
            return new Result<>(200, "删除用户成功");
        } else {
    
    
            return new Result<>(500, "删除用户失败");
        }
    }

    // 更新用户
    @PutMapping("")
    public Result<Users> updateUser(@RequestBody Users user) {
    
    
        boolean success = userService.updateById(user);
        if (success) {
    
    
            return new Result<>(200, "更新用户信息成功", user);
        } else {
    
    
            return new Result<>(500, "更新用户信息失败");
        }
    }

    // 根据 ID 获取用户信息
    @GetMapping("/{id}")
    public Result<Users> getUserById(@PathVariable Long id) {
    
    
        Users user = userService.getById(id);
        if (user != null) {
    
    
            return new Result<>(200, "获取用户信息成功", user);
        } else {
    
    
            return new Result<>(404, "用户不存在");
        }
    }

    // 获取所有用户信息
    @GetMapping("")
    public Result<List<Users>> getAllUsers() {
    
    
        List<Users> userList = userService.list();
        return new Result<>(200, "获取用户信息成功", userList);
    }

    // 用户登录
    @PostMapping("/login")
    public Result<Void> login(@RequestBody LoginRequest request) {
    
    
        boolean success = userService.login(request.getUsername(), request.getPassword());
        if (success) {
    
    
            return new Result<>(200, "登录成功");
        } else {
    
    
            return new Result<>(401, "用户名或密码错误");
        }
    }

}

mapper

  • UserMapper
@Mapper
public interface UserMapper extends BaseMapper<Users> {
    
    
    Users selectByUsername(String username);
}

service

  • UserService
public interface UserService extends IService<Users> {
    
    
    boolean register(String username, String password);
    boolean login(String username, String password);
}
  • UserServiceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, Users> implements UserService {
    
    

    @Override
    public boolean register(String username, String password) {
    
    
        // 生成盐值
        byte[] salt = new byte[16];
        new SecureRandom().nextBytes(salt);

        // 对密码进行加密处理
        String hashedPassword = hash(password, salt);

        // 将用户名、盐值和哈希后的密码保存到数据库中
        Users user = new Users(username, hashedPassword, salt);
        boolean success = save(user);
        return success;
    }

    @Override
    public boolean login(String username, String password) {
    
    
        QueryWrapper<Users> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        Users user = getOne(wrapper);
        if (user == null) {
    
    
            // 如果用户不存在,则认为登录失败
            return false;
        }
        String hashedPassword = user.getPassword();
        byte[] salt = user.getSalt();

        // 对用户输入的密码进行加密处理,并将结果与数据库中的哈希值比较
        String hashedInputPassword = hash(password, salt);
        return hashedPassword.equals(hashedInputPassword);
    }

    @Override
    public boolean saveBatch(Collection<Users> entityList, int batchSize) {
    
    
        return super.saveBatch(entityList, batchSize);
    }

    private String hash(String password, byte[] salt) {
    
    
        int iterations = 10000;
        int keyLength = 256;

        PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, keyLength);
        SecretKeyFactory skf;
        try {
    
    
            skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            byte[] hash = skf.generateSecret(spec).getEncoded();
            return Base64.getEncoder().encodeToString(hash);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
    
    
            throw new RuntimeException(e);
        }
    }
}

This is a specific implementation class UserServiceImpl based on the UserService interface implemented by the MyBatis-Plus framework. This class provides methods for user registration, login, etc., and uses a secure password encryption and authentication mechanism.

register() method: user registration method, the implementation logic is as follows:
a. Generate salt value: First, this method will generate a 16-byte random number as the salt value.

b. Encrypt the password: Next, this method will call the hash() method to encrypt the password entered by the user to obtain the hashed password.

c. Save the username, salt and hashed password to the database: Finally, the method saves the username, salt and hashed password to the database.

login() method: user login method, the implementation logic is as follows:
a. Query user information from the database according to the user name: This method will query the corresponding user information from the database according to the user name.

b. If the user does not exist, it is considered a login failure: if the query result is empty, it means that the user does not exist, and returns false.

c. Encrypt the password entered by the user, and compare the result with the hash value in the database: otherwise, this method will encrypt the password entered by the user, obtain the hashed password, and compare it with the hash value in the database The hash values ​​are compared, if they are equal, the password is correct and return true, otherwise it indicates that the password is wrong and returns false.

saveBatch() method: save user information in batches, and directly call the saveBatch() method of the parent class to implement.

hash() method: the method of encrypting the password, the implementation logic is as follows:

a. Set encryption parameters: This method will set parameters such as encryption algorithm, number of encryption times, and key length.

b. Generate encryption key: Generate an encryption key according to the set parameters, salt value and password.

c. Hash the encryption key: Hash the generated key to get the hashed result.

d. Base64-encode the hash result: Finally, base64-encode the hash result to obtain a hash value represented by a string.

By using the MyBatis-Plus framework, you can avoid manually writing a large number of SQL statements, making the code more concise and easy to read. At the same time, by using a secure password encryption and verification mechanism, the security of user information can be guaranteed.

Item Test (Postman)

  1. Add a new user: Select a POST request, the URL is http://localhost:8081/user, select the raw format for the Body, select JSON for the type, and then enter the following request body data:
{
    
    
    "username": "test",
    "password": "123456"
}

Test Results
insert image description here

  1. Delete user according to ID: select DELETE request, the URL is http://localhost:8081/user/1 (assuming you want to delete the user with ID 1), click the Send button to send the request, if the user is successfully deleted, you should receive the following response:
{
    
    
    "code": 200,
    "message": "删除用户成功"
}

Test Results
insert image description here

  1. Update user: Select PUT request, the URL is http://localhost:8081/user, select raw format in Body, select JSON as type, and enter the following request body data:
{
    
    
    "id": 2,
    "username": "test2",
    "password": "654321"
}

Test Results
insert image description here

  1. Obtain user information according to ID: select GET request, the URL is http://localhost:8081/user/2 (assuming that the user with ID 2 is to be obtained)
    test results
    insert image description here

  2. Get all user information: select GET request, the URL is http://localhost:8081/user
    test result
    insert image description here

  3. User login: select POST request, the URL is http://localhost:8081/user/login, select raw format in Body, select JSON as the type, and then enter the following request body data:

{
    
    
    "username": "test",
    "password": "123456"
}

insert image description here

Guess you like

Origin blog.csdn.net/qq_51447496/article/details/130164242