SpringBoot整合SpringSecurity系列(2)-数据库验证

703 阅读8分钟

一、数据库验证

  1. 上述在配置文件中写死了用户名和密码,此时如果想要不同用户使用不同用户名和密码登录则上述配置不能满足要求
  2. 实现数据库验证需要使用security提供的以下内置对象

二、四大内置类

2.1 UserDetailsService

  1. UserDetailsService提供了加载特定数据的接口,内部只提供了一个根据用户名(可以是任意信息,取决于实现)获取对应的用户详情信息
  2. 当什么也没有配置时,账号和密码由 Spring Security 定义生成,实际项目中账号和密码都是从数据库中查询
  3. 要通过自定义逻辑控制认证逻辑,只需要实现UserDetailsService接口返回对应用户详情数据即可(返回数据不能为空)
    • 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;
}
  1. UsernameNotFoundException用户名没有发现异常,在 loadUserByUsername 中需要通过自己的逻辑从数据库中取值,如果通过用户名没有查询到对应的数据,应抛出UsernameNotFoundException,系统根据此异常判断用户是否存在

2.2 UserDetails

  1. UserDetails为用户详情接口,里面规定了作为权限类应该具备的信息
    • 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();

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

	/**
	 * 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();

	/**
	 * 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();

	/**
	 * 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();

	/**
	 * 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();

}
  1. 如果为自定义用户类,则需要实现此接口,内部也有User类实现了此接口
    • org.springframework.security.core.userdetails.User
  2. 权限设置可以通过AuthorityUtils工具类
    • 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<? extends GrantedAuthority> 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));
		}

		return authorities;
	}
}

2.3 GrantedAuthority

  1. GrantedAuthority为用户权限信息,里面只有权限名称,用户对应的权限需要实现此类
/**
 * 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 {@link AccessDecisionManager} (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();
}

2.4 BCryptPasswordEncoder

  1. Spring Security 要求容器中必须有 PasswordEncoder 实例,所以当自定义登录逻辑时要求必须给容器注入 PaswordEncoder的bean对象
  2. 三大核心方法
    • String encode(CharSequence rawPassword)
      • 把参数按照特定的规则进行加密
    • boolean matches(CharSequence rawPassword, String encodedPassword)
      • 验证从指定的编码密码与原始密码是否匹配,如果密码匹配,则返回 true;如果不匹配,则返回 false
      • 第一个参数表示需要被解析的原始密码,第二个参数表示存储的加密的密码
    • boolean upgradeEncoding(String encodedPassword)
      • 如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回 false,默认返回 false
  3. BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时大多数都使用这个解析器
  4. BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现,是基于Hash算法实现的单向加密,可以通过strength控制加密强度
  5. 加密密码和匹配示例
    • 注入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() {
        return new BCryptPasswordEncoder();
    }
}
  • 加密、密文匹配
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 "加密结果:" + bCryptPasswordEncoder.encode(rawPass);
    }

    @RequestMapping("/match")
    public String match(String rawPass, String encodePass) {
        return "比对结果:" + bCryptPasswordEncoder.matches(rawPass, encodePass);
    }
}

三、数据库环境搭建

  1. 数据库环境使用MySQL+MyBatis Plus + Druid,添加jdbc以及数据源相关依赖,完整依赖如下
<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>

  <!-- 公共依赖 -->
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.18</version>
  </dependency>

  <!-- security安全框架核心依赖 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
  </dependency>

  <!-- 数据库相关 -->
  <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>
  1. 配置文件中添加数据源信息,并把之前配置的用户名和密码注释,然后执行数据库脚本
    • source数据库,可以是任意数据库
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:
#      # 配置用户名和密码
#      name: tianxin
#      password: tianxin
#      # 配置角色
#      roles: admin,normal
# 配置MyBatis Plus
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:/mapper/**/*.xml

用户表信息如下,至少包含和上面UserDetails中需要返回的字段

drop table if exists user;
create table user
(
    id       bigint auto_increment primary key  comment '用户主键',
    username varchar(50) null comment '用户名',
    password varchar(100) null comment '用户密码',
    account_non_expired tinyint null comment '账户过期',
    account_non_locked tinyint null comment '账户锁定',
    credentials_non_expired tinyint null comment '权限过期',
    enabled varchar(50) null comment '用户禁用'
) comment '用户信息' default character set 'utf8mb4';

drop table if exists user_role;
create table user_role
(
    id        bigint auto_increment primary key comment '角色主键',
    user_id   bigint not null comment '用户id',
    authority varchar(1000) comment '权限'
) comment '用户权限' default character set 'utf8mb4';

-- 用户信息,密文对应密码:tianxin,密码由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);

-- 权限信息
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;
  1. 新建启动类,开启扫描mapper
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);
    }
}
  1. 创建UserRole实体类,权限类必须要实现GrantedAuthority并覆写父类方法
    • 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() {
        return authority;
    }

    public Long getId() {
        return id;
    }

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

    public Long getUserId() {
        return userId;
    }

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

    public void setAuthority(String authority) {
        this.authority = authority;
    }
}
  1. 创建User类,User类必须实现UserDetails,且必须要实现父类方法
    • 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() {
        return password;
    }

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

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

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

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

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

    public Long getId() {
        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;
    }
}
  1. 按照mybatis plus要求新建user和UserRole的Mapper类
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> {

}
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> {

}
  1. 新建UserRoleService和其实现类,用于查询权限信息
import com.baomidou.mybatisplus.extension.service.IService;

public interface UserRoleService extends IService<UserRole> {

}
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 {

}
  1. 新建UserService和其实现类,用于查询权限信息,注意UserService接口需要继承UserDetailsService,需要实现查询方法返回具体用户信息
    • org.springframework.security.core.userdetails.UserDetailsService
    • 实现类里面需要实现查询逻辑和权限处理
    • 如果找不到用户需要抛出UsernameNotFoundException,而不是直接返回null(返回null将会出现异常)
import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.security.core.userdetails.UserDetailsService;

public interface UserService extends IService<User>, UserDetailsService {

}
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("未查询到用户信息");
        }

        LambdaQueryWrapper<UserRole> wrapper = Wrappers.<UserRole>lambdaQuery().eq(UserRole::getUserId, user.getId());
        List<UserRole> userRoles = userRoleService.list(wrapper);
        user.setAuthorities(userRoles);
        return user;
    }
}
  1. security查询用户时,也需要使用密码加密器对明文密码加密后对数据库查询出来的密文进行对比,常用的为BCryptPasswordEncoder
    • org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
    • org.springframework.security.crypto.password.PasswordEncoder(父类)
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() {
        return new BCryptPasswordEncoder();
    }
}

  1. 以上步骤完成之后,注入的的UserService将会被默认使用,因为实现了UserDetailsService
  2. 正常启动项目重新登录,此时使用数据库里面的账号和密码访问即可正常登录