SpringSecurity入门学习(2)整合MyBatis-Plus将用户存入数据库

前面学习了Security简单的认证和授权,而我们使用的登录用户是在内存中或者配置文件中定义的,而实际项目中我们都是在数据库中定义用户,接下来我们开始学习如何将用户数据保存到数据库。

Security整合Mybatis-Plus将用户存入数据库

SpringSecurity支持多种不同的数据源,这些不同的数据源最终都将被封装成UserDetailsService的实例,可以自己封装,也可以使用系统默认提供的UserDetailsService实例,例如前面介绍的InMemoryUserDetailsManager;在UserDetailsService的实现类中,除了InMemoryUserDetailsManager之外,还有一个JdbcUserDetailsManager,使用JdbcUserDetailsManager可以让我们通过 JDBC 的方式将数据库和SpringSecurity连接起来,但是JdbcUserDetailsManager使用起来不是很方便,感兴趣的小伙伴可以自己去了解一下。

这里我们自己封装UserDetailsService实例,为了操作方便我们使用Mybatis-Plus来完成数据库操作,数据库使用H2Database,大家可以使用MySql都是一样的。

1.创建项目

可以参考我这篇文章SpringBoot整合MyBatis-Plus+Druid,来整合SpringBoot、MyBatis-Plus、H2Database

创建新的SpringBoot项目,引入以下相关依赖


    org.springframework.boot
    spring-boot-starter-web


    com.h2database
    h2
    runtime


    org.projectlombok
    lombok
    true


    com.alibaba
    fastjson
    1.2.70


    com.baomidou
    mybatis-plus-boot-starter
    3.3.2


    com.baomidou
    mybatis-plus-generator
    3.3.2


    org.apache.velocity
    velocity-engine-core
    2.2


    cn.hutool
    hutool-all
    5.3.5

配置application.properties配置文件

# DataSource Config
spring.datasource.driver-class-name=org.h2.Driver
# 配置本地数据库地址,按需修改
# DATABASE_TO_LOWER=TRUE;-要求数据库名称小写  CASE_INSENSITIVE_IDENTIFIERS=TRUE;-要求对字段的大小写不敏感
spring.datasource.url=jdbc:h2:D:/symon/project/db/security3;CASE_INSENSITIVE_IDENTIFIERS=TRUE;
spring.datasource.username=symon
spring.datasource.password=123456
# 指定Schema (DDL)脚本
spring.datasource.schema=classpath:db/schema.sql
# 指定Data (DML)脚本
spring.datasource.data=classpath:db/data.sql
# 指定schema要使用的Platform
spring.datasource.platform=h2
# 是否启用h2控制台
spring.h2.console.enabled=true
# 配置h2控制台访问地址,http://localhost:8080/h2
spring.h2.console.path=/h2

# MyBatis-Plus配置
mybatis-plus.mapper-locations=classpath:mapper/*.xml

在resources下创建db文件夹,并创建schema.sql和data.sql脚本,分别用来填写建表语句和初始化几条数据

#schema.sql
CREATE TABLE IF NOT EXISTS sys_user
(
  ID bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  USERNAME varchar(50)  NOT NULL COMMENT '用户名',
  PASSWORD varchar(128) NOT NULL COMMENT '密码',
  STATUS INT(2) NOT NULL COMMENT '状态 0锁定 1有效',
  CREATE_TIME datetime(0) NOT NULL COMMENT '创建时间',
  MODIFY_TIME datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (ID)
);

CREATE TABLE IF NOT EXISTS sys_role  (
  ID bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  ROLE_CODE varchar(100) NOT NULL COMMENT '角色标识',
  ROLE_NAME varchar(100) NOT NULL COMMENT '角色名称',
  CREATE_TIME datetime(0) NOT NULL COMMENT '创建时间',
  MODIFY_TIME datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (ID)
);

CREATE TABLE IF NOT EXISTS sys_user_role  (
  ID bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户角色关联ID',
  USER_ID bigint(20) NOT NULL COMMENT '用户ID',
  ROLE_ID bigint(20) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (ID)
) ;

#data.sql
DELETE FROM sys_user;
INSERT INTO sys_user VALUES (1, 'symon', '$2a$10$Dm9tmMfvISVWTmrCM9WwUeSwgdwYSgU48zkqbhEcuDgl40SJuDYsu', 1, '2020-07-21 20:39:22', '2020-07-21 20:44:42');
INSERT INTO sys_user VALUES (2, 'test', '$2a$10$Dm9tmMfvISVWTmrCM9WwUeSwgdwYSgU48zkqbhEcuDgl40SJuDYsu', 1, '2020-07-21 20:39:22', '2020-07-21 20:44:42');
INSERT INTO sys_user VALUES (3, 'test1', '$2a$10$Dm9tmMfvISVWTmrCM9WwUeSwgdwYSgU48zkqbhEcuDgl40SJuDYsu', 0, '2020-07-21 20:39:22', '2020-07-21 20:44:42');

DELETE FROM sys_role;
INSERT INTO sys_role VALUES (1, 'root', '管理员', '2020-07-21 20:39:22', '2020-07-21 20:44:42');
INSERT INTO sys_role VALUES (2, 'user', '普通用户', '2020-07-21 20:39:22', '2020-07-21 20:44:42');

DELETE FROM sys_user_role;
INSERT INTO sys_user_role VALUES (1, 1, 1);
INSERT INTO sys_user_role VALUES (2, 2, 2);
INSERT INTO sys_user_role VALUES (3, 3, 2);

启动项目,之后访问登录h2控制台http://localhost:8080/h2,即可看到表已经创建成功

SpringSecurity入门学习(2)整合MyBatis-Plus将用户存入数据库_第1张图片

 

然后终止项目,使用MyBatis-Plus的代码生成器快速生成代码,可参考我这篇文章SpringBoot整合MyBatis-Plus+Druid

生成之后如下,我们可以看到实体类、mapper、service已经自动生成好了

SpringSecurity入门学习(2)整合MyBatis-Plus将用户存入数据库_第2张图片

 

2.配置

创建SysUserDetails类实现UserDetails接口中的方法

public class SysUserDetails implements UserDetails {
    private SysUserDTO user;
    public SysUserDetails(SysUserDTO user) {
        this.user = user;
    }
    @Override
    public Collection getAuthorities() {
        List authorities = new ArrayList<>();
        if (!CollectionUtils.isEmpty(user.getRoleList())) {
            for (SysRole sysRole : user.getRoleList()) {
                authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRole.getRoleCode()));
            }
        }
        return authorities;
    }
    @Override
    public String getPassword() {return user.getPassword();}
    @Override
    public String getUsername() {return user.getUsername();}
    @Override
    public boolean isAccountNonExpired() {return true;}
    @Override
    public boolean isAccountNonLocked() {return true;}
    @Override
    public boolean isCredentialsNonExpired() {return true;}
    @Override
    public boolean isEnabled() {return Objects.equals(1, user.getStatus());}
}

其中:

      SysUserDTO的内容如下:

@Data
public class SysUserDTO {
    private Long id;
    private String username;
    private String password;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime modifyTime;
    //用户关联角色
    private List roleList;
}

      UserDetails中有四个字段,accountNonExpired、accountNonLocked、credentialsNonExpired、enabled 这四个字段分别用来描述用户的状态,表示账户是否没有过期、账户是否没有被锁定、密码是否没有过期、以及账户是否可用。

      getAuthorities 方法返回用户的角色信息,我们在这个方法中把自己的 Role 稍微转化一下即可。

创建SysUserDetailService实现UserDetailsService接口中的loadUserByUsername方法

@Service
public class SysUserDetailService implements UserDetailsService {
    @Autowired
    private SysUserService sysUserService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUserDTO user = sysUserService.getUserDetail(username);
        if (user == null){
            throw new UsernameNotFoundException("用户名不存在!");
        }
        return new SysUserDetails(user);
    }
}

其中:

      SysUserService是刚刚自动生成的类,其中有一个手动创建的方法,其内容如下

@Service
public class SysUserServiceImpl extends ServiceImpl implements SysUserService {
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private SysRoleMapper sysRoleMapper;
    @Override
    public SysUserDTO getUserDetail(String username) {
        Wrapper wrapper = new QueryWrapper().eq("username", username);
        SysUser sysUser = sysUserMapper.selectOne(wrapper);
        if (sysUser != null) {
            SysUserDTO user = BeanUtil.copyProperties(sysUser, SysUserDTO.class);
            List roleList = sysRoleMapper.selectByUserId(sysUser.getId());
            user.setRoleList(roleList);
            return user;
        } else {
            return null;
        }
    }
}

      getUserDetail方法中的SysRoleMapper及其映射xml文件的的方法的内容如下,该方法查询用户关联的角色

List selectByUserId(@Param("userId") Long userId);

      UserDetailsService中的loadUserByUsername方法的参数就是用户在登录的时候传入的用户名,根据用户名去查询用户信息(查出来之后,系统会自动进行密码比对)

然后引入security依赖,并配置SecurityConfig,可以采用之前的配置,不过需要稍稍修改一下


    org.springframework.boot
    spring-boot-starter-security

SecurityConfig配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    public static final String CONTENT_TYPE = "application/json;charset=utf-8";
    @Autowired
    private SysUserDetailService sysUserDetailService;
    
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(10);//BCryptPasswordEncoder加密
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(sysUserDetailService).passwordEncoder(passwordEncoder());
    }
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/h2/**");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and()
                .authorizeRequests()
                .antMatchers("/user/**").hasRole("user")
                .antMatchers("/root/**").hasRole("root")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .successHandler((req, resp, auth) -> {
                    resp.setContentType(CONTENT_TYPE);
                    PrintWriter out = resp.getWriter();
                    out.write(JSON.toJSONString(ResponseDTO.success("登录成功!")));
                    out.flush();
                    out.close();
                })
                .failureHandler((req, resp, exception) -> {
                    resp.setContentType(CONTENT_TYPE);
                    PrintWriter out = resp.getWriter();
                    String exeMsg = "登录失败!";
                    if (exception instanceof LockedException) {
                        exeMsg = "账户已被锁定!";
                    } else if (exception instanceof CredentialsExpiredException) {
                        exeMsg = "密码已过期!";
                    } else if (exception instanceof AccountExpiredException) {
                        exeMsg = "账户已过期!";
                    } else if (exception instanceof DisabledException) {
                        exeMsg = "账户已被禁用!";
                    } else if (exception instanceof BadCredentialsException) {
                        exeMsg = "用户名或者密码输入错误,请重新输入!";
                    }
                    out.write(JSON.toJSONString(ResponseDTO.error(exeMsg)));
                    out.flush();
                    out.close();
                })
                .and()
                .logout()
                .logoutSuccessHandler((req, resp, auth) -> {
                    resp.setContentType(CONTENT_TYPE);
                    PrintWriter out = resp.getWriter();
                    out.write(JSON.toJSONString(ResponseDTO.success("注销成功!再见!")));
                    out.flush();
                    out.close();
                })
                .permitAll()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint((req, resp, exception) -> {
                    resp.setContentType(CONTENT_TYPE);
                    PrintWriter out = resp.getWriter();
                    out.write(JSON.toJSONString(ResponseDTO.unauthenticated("未登录,请重新登录!")));
                    out.flush();
                    out.close();
                })

                .accessDeniedHandler((req, resp, exception) -> {
                    resp.setContentType(CONTENT_TYPE);
                    PrintWriter out = resp.getWriter();
                    out.write(JSON.toJSONString(ResponseDTO.unauthorized("无权限!")));
                    out.flush();
                    out.close();
                })
                .and().csrf().disable();
    }

    @Bean
    RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
        hierarchy.setHierarchy("ROLE_root > ROLE_user");
        return hierarchy;
    }
}

其中:

      configure(AuthenticationManagerBuilder auth)方法我们使用了自己定义的UserDetailService,并使用了BCryptPasswordEncoder将密码加密

3.启动测试

启动之前,要创建测试类

@RestController
public class TestController {
    @GetMapping("/test")
    public String test() {
        return "Hello World!";
    }
    @GetMapping("/user/test")
    public String user(){
        return "user权限";
    }
    @GetMapping("/root/test")
    public String root(){
        return "root权限";
    }
}

启动类中要记得添加MapperScan

@SpringBootApplication
@MapperScan("com.example.security3.dao")
public class Security3Application {
    public static void main(String[] args) {
        SpringApplication.run(Security3Application.class, args);
    }
}

然后启动项目,对接口进行测试

注意,这里我初始化的密码是123456加密之后的,可自行加密

public static void main(String[] args) {
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(10);
    System.out.println(bCryptPasswordEncoder.encode("123456"));
}

另外test1用户在初始化时STATUS设置了0,在登陆验证时UserDetails的isEnabled返回了false,所以认为该账户被禁用,抛出DisabledException异常,被failureHandler捕获之后返回了自定义的结果。

    @Override
    public boolean isEnabled() {return Objects.equals(1, user.getStatus());}

 

SpringSecurity入门学习(2)整合MyBatis-Plus将用户存入数据库_第3张图片

你可能感兴趣的:(SpringBoot)