3.SpringSecurity基于数据库的认证与授权

文章目录

  • SpringSecurity基于数据库的认证与授权
  • 一、自定义用户信息UserDetails
    • [1.1 新建用户信息类UserDetails](#1.1 新建用户信息类UserDetails)
    • [1.2 UserDetailsService](#1.2 UserDetailsService)
  • 二、基于数据库的认证
    • [2.1 连接数据库](#2.1 连接数据库)
    • [2.2 获取用户信息](#2.2 获取用户信息)
      • [2.2.1 获取用户实体类](#2.2.1 获取用户实体类)
      • [2.2.2 Mapper](#2.2.2 Mapper)
      • [2.2.3 Service](#2.2.3 Service)
    • [2.3 认证](#2.3 认证)
      • [2.3.1 实现UserDetails接口](#2.3.1 实现UserDetails接口)
      • [2.3.2 实现UserDetailsService接口](#2.3.2 实现UserDetailsService接口)
      • [2.3.3 安全配置类](#2.3.3 安全配置类)
      • [2.3.4 测试程序](#2.3.4 测试程序)
        • [2.3.4.1 控制器](#2.3.4.1 控制器)
        • [3.3.4.1 访问](#3.3.4.1 访问)
  • 三、基于数据库的授权
    • [3.1 数据库表](#3.1 数据库表)
    • [3.2 获取权限](#3.2 获取权限)
      • [3.2.1 权限类 SysMenu](#3.2.1 权限类 SysMenu)
      • [3.2.2 SysMenuDao](#3.2.2 SysMenuDao)
      • [3.2.3 SysMenuServiceImpl](#3.2.3 SysMenuServiceImpl)
      • [3.2.4 修改 UserDetailsService](#3.2.4 修改 UserDetailsService)
      • [3.2.5 修改 UserDetails](#3.2.5 修改 UserDetails)
    • [3.3 测试](#3.3 测试)
  • 四、总结

SpringSecurity基于数据库的认证与授权

承接:2.SpringSecurity - 处理器简单说明-CSDN博客

我们之前学习的用户的信息都是配置在代码中,如下段代码所示

java 复制代码
/**
 * 定义一个Bean,用户详情服务接口
 * <p>
 * 系统中默认是有这个UserDetailsService的,也就是默认的用户名(user)和默认密码(控制台生成的)
 * 如果在yaml文件中配置了用户名和密码,那在系统中的就是yaml文件中的信息
 * <p>
 * 我们自定义了之后,就会把系统中的UserDetailsService覆盖掉
 */
@Configuration
public class MySecurityUserConfig {
    /**
     * 根据用户名把用户的详情从数据库中获取出来,封装成用户细节信息UserDetails(包括用户名、密码、用户所拥有的权限)
     * <p>
     * UserDetails存储的是用户的用户名、密码、去权限信息
     */
    @Bean
    public UserDetailsService userDetailsService() {
//      用户细节信息,创建两个用户
//      此User是SpringSecurity框架中的public class User implements UserDetails, CredentialsContainer
        UserDetails user1 = User.builder()
                .username("zhangjingqi-1")
                .password(passwordEncoder().encode("123456"))
//               配置用户角色
                .roles("student") //角色到系统中会变成权限的,比如这里会变成ROLE_student,ROLE_manager
                .build();

        UserDetails user2 = User.builder()
                .username("zhangjingqi-2")
                .password(passwordEncoder().encode("123456"))
//              配置权限
                .authorities("teacher:query")
                .build();

        UserDetails user3 = User.builder()
                .username("admin")
                .password(passwordEncoder().encode("123456"))
//              配置权限
                .authorities("teacher:query","teacher:add","teacher:update","teacher:delete")
                .build();

//      InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService,其中UserDetailsManager继承UserDetailsService
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(user1);
        userDetailsManager.createUser(user2);
        userDetailsManager.createUser(user3);
        return userDetailsManager;
    }

    /**
     * 配置密码加密器
     * NoOpPasswordEncoder.getInstance() 此实例表示不加密
     * BCryptPasswordEncoder() 会加密
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

但是我们并不希望上面这样写,我们希望把用户的信息存入到数据库

一、自定义用户信息UserDetails

用户实体类需要实现UserDetails接口,并实现该接口中的7个方法, UserDetails 接口的7个方法如下图

1.1 新建用户信息类UserDetails

java 复制代码
//只有当"accountNonExpired"、"accountNonLocked"、"credentialsNonExpired"、"enabled"都为true时,账户才能使用
//之前我们创建的时候,直接User.builder()创建,之后InMemoryUserDetailsManager对象createUser
public class SecurityUser implements UserDetails {

    /**
     * @return 权限信息
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    /**
     * @return 用户密码,一定是加密后的密码
     */
    @Override
    public String getPassword() {
        //明文为123456
        return "$2a$10$KyXAnVcsrLaHMWpd3e2xhe6JmzBi.3AgMhteFq8t8kjxmwL8olEDq";
    }

    /**
     * @return 用户名
     */
    @Override
    public String getUsername() {
        return "thomas";
    }

    /**
     * @return 账户是否过期
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * @return 账户是否被锁住
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * @return 凭据是否过期
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * @return 账户是否可用
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

1.2 UserDetailsService

java 复制代码
/**
 * 当我们定义了此类后,系统默认的UserDetailsService不会起作用,下面UserServiceImpl会起作用
 */
@Service
public class UserServiceImpl implements UserDetailsService {

    /**
     * 根据用户名获取用户详情UserDetails
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SecurityUser securityUser= new SecurityUser();//我们自己定义的
        if(username==null || !username.equals(securityUser.getUsername())){
//          SpringSecurity框架中自带的异常
            throw new UsernameNotFoundException("该用户不存在或用户名不正确");
        }
//      执行到这里,说明username是没有问题的
//      用户密码对不对,框架会帮我们进行判断
        return securityUser;
    }

}

二、基于数据库的认证

我们观察到在1.1UserDetails中,我们把用户名和密码是写死的,但是这种情况下是不合理的,包括权限在内,我们都需要从数据库中取出来

将信息从数据库取出来后,可以将信息封装成一个UserDetails类

2.1 连接数据库

xml 复制代码
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.15</version>
</dependency>

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>
yaml 复制代码
spring:
  #数据源
  datasource:
    #德鲁伊连接池
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/springsecurity?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: root
      
 mybatis:
  #SQL映射文件的位置
  mapper-locations: classpath:mapper/**/*.xml
  # 指定实体类起别名,(实体类所在的包的包路径,那么包中的所有实体类别名就默认是类名首字母小写)
  type-aliases-package: com.zhangjingqi.entity
  configuration:
    #开启驼峰命名法
    map-underscore-to-camel-case: true
    #日志功能
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl     

2.2 获取用户信息

判断用户是否正确,我们可以从sys_user中取出用户相关信息,将用户的相关信息取出来后封装成一个UserDetails类

2.2.1 获取用户实体类

获取用户信息的实体类,这里不建议SysUser实体类实现UserDetails接口,因为会显得很乱

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SysUser implements Serializable {

    private static final long serialVersionUID = -5352627792860514242L;
    
    private Integer userId;

    private String username;

    private String password;

    private String sex;

    private String address;

    private Integer enabled;

    private Integer accountNoExpired;

    private Integer credentialsNoExpired;

    private Integer accountNoLocked;

}

2.2.2 Mapper

封装dao层,也就是Mapper层,从数据库中获取用户的信息

java 复制代码
@Mapper
public interface SysUserDao {

    /**
     * 根据用户名访问用户信息
     * @param userName 用户名
     * @return 用户信息
     */
    SysUser getByUserName(@Param("userName") String userName);


}
sql 复制代码
<mapper namespace="com.zhangjingqi.dto.SysUserDao">

    <select id="getByUserName" resultType="com.zhangjingqi.entity.SysUser">
         select user_id,username,password,sex,address,enabled,account_no_expired,credentials_no_expired,account_no_locked
        from sys_user 
        where username = #{userName};
    </select>
    
</mapper>

2.2.3 Service

java 复制代码
@Slf4j
@Service
public class SysUserServiceImpl implements SysUserService {
    
    @Autowired
    private SysUserDao sysUserDao;

    @Override
    public SysUser getByUserName(String userName) {
        return sysUserDao.getByUserName(userName);
    }
}

2.3 认证

2.3.1 实现UserDetails接口

我们之前是写死的用户信息,但是现在是从数据库中进行获取的

java 复制代码
@Data
public class SecurityUser implements UserDetails {

    private static final long serialVersionUID = -1314948905954698478L;

    private final SysUser sysUser ;


    public SecurityUser(SysUser sysUser) {
        this.sysUser = sysUser;
    }


    /**
     * @return 权限信息
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    /**
     * @return 用户密码,一定是加密后的密码
     */
    @Override
    public String getPassword() {
        return sysUser.getPassword();
    }

    /**
     * @return 用户名
     */
    @Override
    public String getUsername() {
        return sysUser.getUsername();
    }

    /**
     * @return 账户是否过期
     */
    @Override
    public boolean isAccountNonExpired() {
        return sysUser.getAccountNoExpired() != 0;
    }

    /**
     * @return 账户是否被锁住
     */
    @Override
    public boolean isAccountNonLocked() {
        return sysUser.getAccountNoLocked() !=0;
    }

    /**
     * @return 凭据是否过期
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return sysUser.getCredentialsNoExpired() !=0 ;
    }

    /**
     * @return 账户是否可用
     */
    @Override
    public boolean isEnabled() {
        return sysUser.getEnabled() !=0 ;
    }
}

这个地方我们之前是这么写的

2.3.2 实现UserDetailsService接口

java 复制代码
@Slf4j
@Service
public class SecurityUserDetailsService implements UserDetailsService {

    @Autowired
    private SysUserService sysUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

//      1.从数据库获取用户的详情信息
        SysUser sysUser = sysUserService.getByUserName(username);

        if (null == sysUser){
//          这个异常信息是SpringSecurity中封装的
            throw new UsernameNotFoundException("用户没有找到");
        }

//      2.封装成UserDetails类,SecurityUser类实现了UserDetails接口
        SecurityUser securityUser = new SecurityUser(sysUser);

        return securityUser;
    }
}

我们之前是这么写的

这篇文章会有介绍:1.SpringSecurity -快速入门、加密、基础授权-CSDN博客

2.3.3 安全配置类

java 复制代码
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 重写 configure(HttpSecurity http)方法
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()//授权http请求
                .anyRequest()//任何请求
                .authenticated();//需要验证

        http.formLogin().permitAll(); //SpringSecurity的表单认证
    }


    /**
     * @return 密码加密器
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}

2.3.4 测试程序

2.3.4.1 控制器
java 复制代码
@RestController
@RequestMapping("/student")
public class StudentController {

    @GetMapping("/query")
    private String query(){
        return "query student";
    }

    @GetMapping("/add")
    private String add(){
        return "add student";
    }

    @GetMapping("/delete")
    private String delete(){
        return "delete student";
    }

    @GetMapping("/update")
    private String update(){
        return "export student";
    }
}
java 复制代码
@RestController
@RequestMapping("/teacher")
public class TeacherController {

    @GetMapping("/query")
    @PreAuthorize("hasAuthority('teacher:query')")//预授权
    public String queryInfo() {
        return "teacher query";
    }

}
3.3.4.1 访问

用下面这个人的信息进行登录

访问一下localhost:8080/student/query,发现可以访问

再访问一下localhost:8080/teacher/query,发现是不能访问的,原因是thomas用户没有/teacher/query路径的权限

三、基于数据库的授权

3.1 数据库表

首先我们很明确的就是用户和角色的关系是多对多的

用户表

角色表

用户与角色关联表

权限表

权限与角色关联表

3.2 获取权限

3.2.1 权限类 SysMenu

java 复制代码
@Data
public class SysMenu implements Serializable {
    
    private static final long serialVersionUID = 597868207552115176L;
    
    private Integer id;
    private Integer pid;
    private Integer type;
    private String name;
    private String code;
}

3.2.2 SysMenuDao

java 复制代码
@Mapper
public interface SysMenuDao {
   List<String> queryPermissionByUserId(@Param("userId") Integer userId);
}

这个地方涉及到三张表,角色用户关联表sys_role_user、角色权限关联表sys_role_menu、权限表sys_menu

我们要实现通过用户获取对应的权限

sql 复制代码
<mapper namespace="com.zhangjingqi.dto.SysMenuDao">


    <select id="queryPermissionByUserId" resultType="java.lang.String">
        SELECT distinct sm.code
        FROM sys_role_user sru
                 inner join sys_role_menu srm
                            on sru.rid = srm.rid
                 inner join sys_menu sm on srm.mid = sm.id
        where sru.uid = #{userId}
          and sm.delete_flag = 0

    </select>
</mapper>

3.2.3 SysMenuServiceImpl

java 复制代码
@Service
public class SysMenuServiceImpl implements SysMenuService {
    @Autowired
    private SysMenuDao sysMenuDao;

    @Override
    public List<String> queryPermissionByUserId(Integer userId) {
        return sysMenuDao.queryPermissionByUserId(userId);
    }
}

3.2.4 修改 UserDetailsService

java 复制代码
@Slf4j
@Service
public class SecurityUserDetailsService implements UserDetailsService {

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private SysMenuService sysMenuService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

//      1.从数据库获取用户的详情信息
        SysUser sysUser = sysUserService.getByUserName(username);

        if (null == sysUser){
//          这个异常信息是SpringSecurity中封装的
            throw new UsernameNotFoundException("用户没有找到");
        }

//      2.获取该用户的权限
        List<String> permissionList = sysMenuService.queryPermissionByUserId(sysUser.getUserId());

//        List<SimpleGrantedAuthority> simpleGrantedAuthorities = permissionList.stream().map(permission -> new SimpleGrantedAuthority(permission) ).collect(Collectors.toList());
//      将集合的泛型转换成SimpleGrantedAuthority (GrantedAuthority类的子类即可)
        List<SimpleGrantedAuthority> simpleGrantedAuthorities = permissionList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());


//      2.封装成UserDetails类,SecurityUser类实现了UserDetails接口
        SecurityUser securityUser = new SecurityUser(sysUser);
        securityUser.setSimpleGrantedAuthorities(simpleGrantedAuthorities);

        return securityUser;
    }
}

3.2.5 修改 UserDetails

java 复制代码
@Data
public class SecurityUser implements UserDetails {

    private static final long serialVersionUID = -1314948905954698478L;

    private final SysUser sysUser ;

//  用户权限
    private List<SimpleGrantedAuthority> simpleGrantedAuthorities;

    public SecurityUser(SysUser sysUser) {
        this.sysUser = sysUser;
    }


    /**
     *  这个集合中对象的类型必须是GrantedAuthority类或其子类
     * @return 权限信息
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return simpleGrantedAuthorities;
    }

    /**
     * @return 用户密码,一定是加密后的密码
     */
    @Override
    public String getPassword() {
        return sysUser.getPassword();
    }

    /**
     * @return 用户名
     */
    @Override
    public String getUsername() {
        return sysUser.getUsername();
    }

    /**
     * @return 账户是否过期
     */
    @Override
    public boolean isAccountNonExpired() {
        return sysUser.getAccountNoExpired() != 0;
    }

    /**
     * @return 账户是否被锁住
     */
    @Override
    public boolean isAccountNonLocked() {
        return sysUser.getAccountNoLocked() !=0;
    }

    /**
     * @return 凭据是否过期
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return sysUser.getCredentialsNoExpired() !=0 ;
    }

    /**
     * @return 账户是否可用
     */
    @Override
    public boolean isEnabled() {
        return sysUser.getEnabled() !=0 ;
    }
}

3.3 测试

使用Obama登录

查看obama权限

四、总结

认证与授权,需要我们实现UserDetails用户详情类和UserDetailsService类

相关推荐
DuelCode2 分钟前
Windows VMWare Centos Docker部署Springboot 应用实现文件上传返回文件http链接
java·spring boot·mysql·nginx·docker·centos·mybatis
优创学社26 分钟前
基于springboot的社区生鲜团购系统
java·spring boot·后端
why技术9 分钟前
Stack Overflow,轰然倒下!
前端·人工智能·后端
幽络源小助理13 分钟前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring
猴哥源码14 分钟前
基于Java+springboot 的车险理赔信息管理系统
java·spring boot
ai小鬼头1 小时前
AIStarter如何助力用户与创作者?Stable Diffusion一键管理教程!
后端·架构·github
简佐义的博客2 小时前
破解非模式物种GO/KEGG注释难题
开发语言·数据库·后端·oracle·golang
爬山算法2 小时前
MySQL(116)如何监控负载均衡状态?
数据库·mysql·负载均衡
Code blocks2 小时前
使用Jenkins完成springboot项目快速更新
java·运维·spring boot·后端·jenkins