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


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

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

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

    /**
     * 定义一个Bean,用户详情服务接口
     * 

    * 系统中默认是有这个UserDetailsService的,也就是默认的用户名(user)和默认密码(控制台生成的) * 如果在yaml文件中配置了用户名和密码,那在系统中的就是yaml文件中的信息 *

    * 我们自定义了之后,就会把系统中的UserDetailsService覆盖掉 */ @Configuration public class MySecurityUserConfig { /** * 根据用户名把用户的详情从数据库中获取出来,封装成用户细节信息UserDetails(包括用户名、密码、用户所拥有的权限) *

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

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

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

    一、自定义用户信息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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    二、基于数据库的认证

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

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

    image-20231019221142173

    2.1 连接数据库

    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
    dependency>
    
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>druid-spring-boot-starterartifactId>
        <version>1.2.15version>
    dependency>
    
    <dependency>
        <groupId>org.mybatis.spring.bootgroupId>
        <artifactId>mybatis-spring-boot-starterartifactId>
        <version>2.1.1version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    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     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    2.2.2 Mapper

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

    @Mapper
    public interface SysUserDao {
    
        /**
         * 根据用户名访问用户信息
         * @param userName 用户名
         * @return 用户信息
         */
        SysUser getByUserName(@Param("userName") String userName);
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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 ;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

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

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    我们之前是这么写的

    这篇文章会有介绍: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();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    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";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    @RestController
    @RequestMapping("/teacher")
    public class TeacherController {
    
        @GetMapping("/query")
        @PreAuthorize("hasAuthority('teacher:query')")//预授权
        public String queryInfo() {
            return "teacher query";
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3.2.2 SysMenuDao

    @Mapper
    public interface SysMenuDao {
       List<String> queryPermissionByUserId(@Param("userId") Integer userId);
    }
    
    • 1
    • 2
    • 3
    • 4

    这个地方涉及到三张表,角色用户关联表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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    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 ;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    3.3 测试

    使用Obama登录

    image-20231021155010195

    查看obama权限

    image-20231021155118766

    四、总结

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

  • 相关阅读:
    Ansible ad-hoc 临时命令
    Hive常见的面试题(十二道)
    尚硅谷尚品项目汇笔记(三)
    PyTorch主要组成模块 | 数据读入 | 模型构建 | 模型初始化 | 损失函数 | 优化器 | 训练与评估
    SOLIDWORKS® 2024 新功能 - 3D CAD
    re:Invent 构建未来:云计算&生成式 AI 诞生科技新局面
    华为云Stack南向开放框架,帮助生态伙伴高效入云
    逻辑回归(Logistic Regression)
    聚观早报 | 国美电器被申请破产清算;首款太阳能汽车投入生产
    2021年3月青少年软件编程(Python)等级考试试卷(一级)
  • 原文地址:https://blog.csdn.net/weixin_51351637/article/details/134063693