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类

相关推荐
苹果醋328 分钟前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
盛派网络小助手1 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
∝请叫*我简单先生1 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl
别致的影分身2 小时前
使用C语言连接MySQL
数据库·mysql
过过过呀Glik2 小时前
在 Ubuntu 上安装 MySQL 的详细指南
mysql·ubuntu
荆州克莱2 小时前
mysql中局部变量_MySQL中变量的总结
spring boot·spring·spring cloud·css3·技术
zquwei2 小时前
SpringCloudGateway+Nacos注册与转发Netty+WebSocket
java·网络·分布式·后端·websocket·网络协议·spring
dessler3 小时前
Docker-run命令详细讲解
linux·运维·后端·docker
火烧屁屁啦3 小时前
【JavaEE进阶】初始Spring Web MVC
java·spring·java-ee