First, database verification

  1. The user name and password are not written in the configuration file. If you want different users to use different user names and passwords to log in, the preceding configuration cannot meet requirements
  2. Implementing database validation requires the following built-in objects provided by Security

Two, four built-in classes

2.1 UserDetailsService

  1. The UserDetailsService provides an interface to load specific data, and only provides an internal interface to get user details based on the user name (which can be any information, depending on the implementation)
  2. When nothing is configured, the account and password are generated by Spring Security definitions, and the actual project account and password are queried from the database
  3. To control authentication logic through custom logic, only the implementation is requiredUserDetailsServiceThe interface can return the detailed data of the corresponding user (the returned data cannot be empty).
    • org.springframework.security.core.userdetails.UserDetailsService
public interface UserDetailsService {
	/**
	 * Locates the user based on the username. In the actual implementation, the search
	 * may possibly be case sensitive, or case insensitive depending on how the
	 * implementation instance is configured. In this case, the <code>UserDetails</code>
	 * object that comes back may have a username that is of a different case than what
	 * was actually requested..
	 *
	 * @param username the username identifying the user whose data is required.
	 *
	 * @return a fully populated user record (never <code>null</code>)
	 *
	 * @throws UsernameNotFoundException if the user could not be found or the user has no
	 * GrantedAuthority
	 */
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Copy the code
  1. UsernameNotFoundException user name not found abnormal, through its own logic is needed in loadUserByUsername values from the database, if there is no query to the corresponding data through a user name, Should throw UsernameNotFoundException system, according to this exception to determine whether a user exists

2.2 populated UserDetails

  1. UserDetails is the UserDetails interface, which specifies the information that should be available as a permission class
    • org.springframework.security.core.userdetails.UserDetails
import java.io.Serializable;
import java.util.Collection;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

/**
 * Provides core user information.
 *
 * <p>
 * Implementations are not used directly by Spring Security for security purposes. They
 * simply store user information which is later encapsulated into {@link Authentication}
 * objects. This allows non-security related user information (such as email addresses,
 * telephone numbers etc) to be stored in a convenient location.
 * <p>
 * Concrete implementations must take particular care to ensure the non-null contract
 * detailed for each method is enforced. See
 * {@link org.springframework.security.core.userdetails.User} for a reference
 * implementation (which you might like to extend or use in your code).
 */
public interface UserDetails extends Serializable {

	/**
	 * Returns the authorities granted to the user. Cannot return <code>null</code>.
	 * @return the authorities, sorted by natural key (never <code>null</code>)
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * Returns the password used to authenticate the user.
	 * @return the password
	 */
	String getPassword(a);

	/**
	 * Returns the username used to authenticate the user. Cannot return
	 * <code>null</code>.
	 * @return the username (never <code>null</code>)
	 */
	String getUsername(a);

	/**
	 * Indicates whether the user's account has expired. An expired account cannot be
	 * authenticated.
	 * @return <code>true</code> if the user's account is valid (ie non-expired),
	 * <code>false</code> if no longer valid (ie expired)
	 */
	boolean isAccountNonExpired(a);

	/**
	 * Indicates whether the user is locked or unlocked. A locked user cannot be
	 * authenticated.
	 * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
	 */
	boolean isAccountNonLocked(a);

	/**
	 * Indicates whether the user's credentials (password) has expired. Expired
	 * credentials prevent authentication.
	 * @return <code>true</code> if the user's credentials are valid (ie non-expired),
	 * <code>false</code> if no longer valid (ie expired)
	 */
	boolean isCredentialsNonExpired(a);

	/**
	 * Indicates whether the user is enabled or disabled. A disabled user cannot be
	 * authenticated.
	 * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
	 */
	boolean isEnabled(a);

}
Copy the code
  1. If it is a custom User class, you need to implement this interface. There is also an internal User class that implements this interface
    • org.springframework.security.core.userdetails.User
  2. Permissions can be set through the AuthorityUtils utility class
    • org.springframework.security.core.authority.AuthorityUtils
public abstract class AuthorityUtils {
	public static final List<GrantedAuthority> NO_AUTHORITIES = Collections.emptyList();

	/**
	 * Creates a array of GrantedAuthority objects from a comma-separated string
	 * representation (e.g. "ROLE_A, ROLE_B, ROLE_C").
	 *
	 * @param authorityString the comma-separated string
	 * @return the authorities created by tokenizing the string
	 */
	public static List<GrantedAuthority> commaSeparatedStringToAuthorityList( String authorityString) {
		return createAuthorityList(StringUtils
				.tokenizeToStringArray(authorityString, ","));
	}

	/**
	 * Converts an array of GrantedAuthority objects to a Set.
	 * @return a Set of the Strings obtained from each call to
	 * GrantedAuthority.getAuthority()
	 */
	public static Set<String> authorityListToSet( Collection
        userAuthorities) {
		Set<String> set = new HashSet<String>(userAuthorities.size());

		for (GrantedAuthority authority : userAuthorities) {
			set.add(authority.getAuthority());
		}

		return set;
	}

	public static List<GrantedAuthority> createAuthorityList(String... roles) {
		List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(roles.length);

		for (String role : roles) {
			authorities.add(new SimpleGrantedAuthority(role));
		}

		returnauthorities; }}Copy the code

2.3 GrantedAuthority

  1. GrantedAuthority is the user permission information, which contains only the permission name
/**
 * Represents an authority granted to an {@link Authentication} object.
 *
 * <p>
 * A <code>GrantedAuthority</code> must either represent itself as a <code>String</code>
 * or be specifically supported by an {@link AccessDecisionManager}.
 */
public interface GrantedAuthority extends Serializable {
	/** * If the <code>GrantedAuthority</code> can be represented as a <code>String</code> * and that <code>String</code> is  sufficient in precision to be relied upon for an * access control decision by an {@linkAccessDecisionManager} (or delegate), this * method should return such a <code>String</code>. * <p> * If the <code>GrantedAuthority</code> cannot be expressed  with sufficient precision * as a <code>String</code>, <code>null</code> should be returned. Returning * <code>null</code> will require an <code>AccessDecisionManager</code> (or delegate) * to specifically support the <code>GrantedAuthority</code> implementation, so * returning <code>null</code> should be avoided unless actually required. * *@return a representation of the granted authority (or <code>null</code> if the
	 * granted authority cannot be expressed as a <code>String</code> with sufficient
	 * precision).
	 */
	String getAuthority(a);
}
Copy the code

2.4 BCryptPasswordEncoder

  1. Spring Security requires the container to have an instance of PasswordEncoder, so you must inject PaswordEncoder’s bean object into the container when you customize the login logic
  2. Three core methods
    • String encode(CharSequence rawPassword)
      • Encrypts parameters according to specific rules
    • boolean matches(CharSequence rawPassword, String encodedPassword)
      • Verifies that the specified encoded password matches the original password, returning true if the password matches; If there is no match, return false
      • The first parameter indicates the original password that needs to be resolved, and the second parameter indicates the encrypted password that is stored
    • boolean upgradeEncoding(String encodedPassword)
      • Return true if the parsed password can be parsed again with a more secure result, false otherwise, false by default
  3. BCryptPasswordEncoder is the recommended password parser for Spring Security
  4. BCryptPasswordEncoder is the specific implementation of bcrypt strong Hash method. It is a one-way encryption based on Hash algorithm, and the encryption strength can be controlled through strength
  5. Encryption password and matching example
    • Injection in the IoC
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Slf4j
@Configuration
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(a) {
        return newBCryptPasswordEncoder(); }}Copy the code
  • Encryption and ciphertext matching
import javax.annotation.Resource;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SecurityPasswordController {
    @Resource
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @RequestMapping("/pass")
    public String pass(String rawPass) {
        return "Encryption result:" + bCryptPasswordEncoder.encode(rawPass);
    }

    @RequestMapping("/match")
    public String match(String rawPass, String encodePass) {
        return "Comparison results:"+ bCryptPasswordEncoder.matches(rawPass, encodePass); }}Copy the code

3. Database environment construction

  1. Use MySQL+MyBatis Plus + Druid to add JDBC and data source dependencies
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>

  <! -- Public dependencies -->
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.18</version>
  </dependency>

  <! -- Security Framework core dependencies -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
  </dependency>

  <! -- Database related -->
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.22</version>
  </dependency>
  <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.1</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
  </dependency>
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.5</version>
  </dependency>
</dependencies>
Copy the code
  1. Add data source information to the configuration file, annotate the previously configured user name and password, and execute the database script
    • Source database, can be any database
server:
  port: 8888
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: JDBC: mysql: / 127.0.0.1:3306 / source? useSSL=false
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
# security:
# user:
Configure the user name and password
# name: tianxin
# password: tianxin
# # Configure roles
# roles: admin,normal
Configure MyBatis Plus
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:/mapper/**/*.xml
Copy the code

The user table information is as follows, including at least the fields to be returned in UserDetails above

drop table if exists user;
create table user
(
    id       bigint auto_increment primary key  comment 'User primary key',
    username varchar(50) null comment 'Username',
    password varchar(100) null comment 'User password',
    account_non_expired tinyint null comment 'Account expired',
    account_non_locked tinyint null comment 'Account locked',
    credentials_non_expired tinyint null comment 'Permissions expired',
    enabled varchar(50) null comment 'User Disabled'
) comment 'User Information' default character set 'utf8mb4';

drop table if exists user_role;
create table user_role
(
    id        bigint auto_increment primary key comment 'Role primary key',
    user_id   bigint not null comment 'user id',
    authority varchar(1000) comment 'rights'
) comment 'User rights' default character set 'utf8mb4';

- User information, ciphertext password: tianxin, encrypted by BCryptPasswordEncoder
insert into user(id, username, password, account_non_expired, account_non_locked, credentials_non_expired, enabled)
values(1.'root'.'$2a$10$.9UpYAxTDg/cd8U7wtal5et7TcC7QaInySM1p8tBEp.OO20UvjR/S'.0.0.0.0);
insert into user(id, username, password, account_non_expired, account_non_locked, credentials_non_expired, enabled)
values(2.'admin'.'$2a$10$.9UpYAxTDg/cd8U7wtal5et7TcC7QaInySM1p8tBEp.OO20UvjR/S'.1.1.1.1);

-- Permission information
insert into user_role(user_id, authority) VALUES (1.'ADMIN');
insert into user_role(user_id, authority) VALUES (1.'NORMAL');
insert into user_role(user_id, authority) VALUES (2.'NORMAL');

select * from user;
select * from user_role;
Copy the code
  1. Create a startup class and enable mapper scanning
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan(basePackages = "com.codecoord.security.mapper")
public class SpringbootSecurityApplication {
    public static void main(String[] args) { SpringApplication.run(SpringbootSecurityApplication.class, args); }}Copy the code
  1. To create the UserRole entity class, the permissions class must implement GrantedAuthority and override the parent methods
    • org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.GrantedAuthority;

public class UserRole implements GrantedAuthority {
    private Long id;
    private Long userId;
    private String authority;

    @Override
    public String getAuthority(a) {
        return authority;
    }

    public Long getId(a) {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getUserId(a) {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public void setAuthority(String authority) {
        this.authority = authority; }}Copy the code
  1. Create the User class, which must implement UserDetails and must implement the superclass methods
    • org.springframework.security.core.userdetails.UserDetails
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

@TableName("user")
public class User implements UserDetails {
    private Long id;
    @TableField(exist = false)
    private List<UserRole> authorities;
    private String username;
    private String password;
    private int accountNonExpired;
    private int accountNonLocked;
    private int credentialsNonExpired;
    private int enabled;

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

    @Override
    public String getPassword(a) {
        return password;
    }

    @Override
    public String getUsername(a) {
        return username;
    }

    @Override
    public boolean isAccountNonExpired(a) {
        return accountNonExpired == 0;
    }

    @Override
    public boolean isAccountNonLocked(a) {
        return accountNonLocked == 0;
    }

    @Override
    public boolean isCredentialsNonExpired(a) {
        return credentialsNonExpired == 0;
    }

    @Override
    public boolean isEnabled(a) {
        return enabled == 0;
    }

    public Long getId(a) {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public void setAuthorities(List<UserRole> authorities) {
        this.authorities = authorities;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setAccountNonExpired(int accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

    public void setAccountNonLocked(int accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

    public void setCredentialsNonExpired(int credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }

    public void setEnabled(int enabled) {
        this.enabled = enabled; }}Copy the code
  1. Create Mapper classes for User and UserRole as required by Mybatis Plus
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.codecoord.security.domain.User;
import org.springframework.stereotype.Repository;

@Repository
public interface UserMapper extends BaseMapper<User> {}Copy the code
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.codecoord.security.domain.UserRole;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRoleMapper extends BaseMapper<UserRole> {}Copy the code
  1. Create a UserRoleService and its implementation class to query permission information
import com.baomidou.mybatisplus.extension.service.IService;

public interface UserRoleService extends IService<UserRole> {}Copy the code
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

@Service(value = "userRoleService")
public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper.UserRole> implements UserRoleService {}Copy the code
  1. Create a UserService and its implementation class to query permission information. Notice that the UserService interface needs to inherit UserDetailsService and implement the query method to return specific user information
    • org.springframework.security.core.userdetails.UserDetailsService
    • The implementation class needs to implement the query logic and permission processing
    • If you cannot find the user needs to throw UsernameNotFoundException, rather than direct return null (abnormal) will return null
import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.security.core.userdetails.UserDetailsService;

public interface UserService extends IService<User>, UserDetailsService {}Copy the code
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.util.List;
import javax.annotation.Resource;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service(value = "userService")
public class UserServiceImpl extends ServiceImpl<UserMapper.User> implements UserService {

    @Resource
    private UserRoleService userRoleService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery()
                .eq(User::getUsername, username);
        User user = getOne(queryWrapper);
        if (user == null) {
            throw new UsernameNotFoundException("No user information found");
        }

        LambdaQueryWrapper<UserRole> wrapper = Wrappers.<UserRole>lambdaQuery().eq(UserRole::getUserId, user.getId());
        List<UserRole> userRoles = userRoleService.list(wrapper);
        user.setAuthorities(userRoles);
        returnuser; }}Copy the code
  1. When querying a user by security, you also need to use a password encryptor to encrypt the plaintext password and compare the ciphertext queried by the database. The commonly used one is BCryptPasswordEncoder
    • org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
    • Org. Springframework. Security. The crypto. Password. PasswordEncoder (parent)
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Slf4j
@Configuration
public class SecurityConfig {

    /** * password encryptor */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(a) {
        return newBCryptPasswordEncoder(); }}Copy the code
  1. After the above steps are complete, the injected UserService will be used by default because UserDetailsService is implemented
  2. Start the project normally and log in again. Then use the account and password in the database to log in normally