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

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

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

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

/**
 * 定义一个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个方法如下图

img

1.1 新建用户信息类UserDetails

//只有当"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

/**
 * 当我们定义了此类后,系统默认的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类

image-20231019221142173

2.1 连接数据库

<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>
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类

image-20231020091944626

image-20231020101927297

2.2.1 获取用户实体类

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

@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层,从数据库中获取用户的信息

@Mapper
public interface SysUserDao {

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


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

@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接口

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

@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 ;
    }
}

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

image-20231020110240997

2.3.2 实现UserDetailsService接口

@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博客

image-20231020105535812

2.3.3 安全配置类

@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 控制器
@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";
    }
}
@RestController
@RequestMapping("/teacher")
public class TeacherController {

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

}
3.3.4.1 访问

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

image-20231020140635419

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

image-20231020142807340

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

image-20231020142907797

三、基于数据库的授权

3.1 数据库表

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

用户表

image-20231020150336382

image-20231020150438581

角色表

image-20231020150353283

image-20231020150456440

用户与角色关联表

image-20231020150418327

权限表

image-20231021145545952

image-20231021145604961

权限与角色关联表

image-20231021145635383

image-20231021145648250

3.2 获取权限

3.2.1 权限类 SysMenu

@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

@Mapper
public interface SysMenuDao {
   List<String> queryPermissionByUserId(@Param("userId") Integer userId);
}

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

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

<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

@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

@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

@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登录

image-20231021155010195

查看obama权限

image-20231021155118766

四、总结

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我爱布朗熊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值