[前端学java11-SpringSecurity] 配置 + 内存 + 数据库 = 三种方式实现RBAC

580

导航

[react] Hooks

[封装01-设计模式] 设计原则 和 工厂模式(简单抽象方法) 适配器模式 装饰器模式
[封装02-设计模式] 命令模式 享元模式 组合模式 代理模式

[React 从零实践01-后台] 代码分割
[React 从零实践02-后台] 权限控制
[React 从零实践03-后台] 自定义hooks
[React 从零实践04-后台] docker-compose 部署react+egg+nginx+mysql
[React 从零实践05-后台] Gitlab-CI使用Docker自动化部署

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend

[源码-vue06] Vue.nextTick 和 vm.$nextTick
[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[数据结构和算法01] 二分查找和排序

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[深入21] 数据结构和算法 - 二分查找和排序
[深入22] js和v8垃圾回收机制
[深入23] JS设计模式 - 代理,策略,单例

[前端学java01-SpringBoot实战] 环境配置和HelloWorld服务
[前端学java02-SpringBoot实战] mybatis + mysql 实现歌曲增删改查
[前端学java03-SpringBoot实战] lombok,日志,部署
[前端学java04-SpringBoot实战] 静态资源 + 拦截器 + 前后端文件上传
[前端学java05-SpringBoot实战] 常用注解 + redis实现统计功能
[前端学java06-SpringBoot实战] 注入 + Swagger2 3.0 + 单元测试JUnit5
[前端学java07-SpringBoot实战] IOC扫描器 + 事务 + Jackson
[前端学java08-SpringBoot实战总结1-7] 阶段性总结
[前端学java09-SpringBoot实战] 多模块配置 + Mybatis-plus + 单多模块打包部署
[前端学java10-SpringBoot实战] bean赋值转换 + 参数校验 + 全局异常处理
[前端学java11-SpringSecurity] 配置 + 内存 + 数据库 = 三种方式实现RBAC

(一) 前置知识

(1) 一些单词

security 安全
advice 通知 建议
exception 异常
maintainer 维护者
structure 结构 体系
hierarchy 层级
subtypes 子类

authorize 授权 批准 // authorities
cursor 光标 游标
destructure 解构
granted 授予 同意

structure 结构 => command + 7 // 注意区分 destructure 和 structure
Assignee 代理人,委托人
concurrent 并行的

(2) idea 快捷键

查看完整的继承关系 f4
跳到源码中 f3
快速查看一个类具有的方法 command + 7 => structure

(3) 什么是加盐加密

  • ( 加盐加密 ) 是一种对 ( 系统登陆 - 口令 ) 加密的方式
  • 它的加密方式是将 ( 每一个口令跟一个n位随机数相关联 ),这个n位随机数叫做 ( )

(4) mybatis 和 mybatis-plus 配置冲突

  • 问题原因分析(踩坑)
    • mybais的配置在appliation.yml中指定了config和mapper文件在

      • resources/mybatis/mapper/xxx.xml
    • 而mybatis-plus的mapper文件需要在

      • resources/mapper/xxx.xml
    • 以上两个位置不一样,就很容易导致mapper找不到的情况,一定要注意,踩到这个坑用了几个小时才找到原因

(5) ArrayList

  • 常用api
    • add 新增
    • set 修改
    • remove 删除
    • size 成员数
    • indexOf 索引
    • 和js中的Array差不多
  • ArrayList 和 List 的区别
    • List
      • 集合内的数据类型有且只能是一种,不允许多种
      • 是一个接口,所以不能被构造即不能被实例化 ( 接口和抽象不能被实例化 )
    • ArrayList
      • ArrayList是List接口的一个实现
@SpringBootTest
public class ArrayListTest {
    @Test
    public void arrList() {
        ArrayList<String> addressList = new ArrayList<>();
        addressList.add("beijing");
        addressList.add("shanghai");
        addressList.set(1, "shanghai2");
        addressList.remove(1);
        int beijing = addressList.indexOf("beijing");
        int size = addressList.size();
    }
}

image.png

(6) PasswordEncoder 和 BCryptPasswordEncoder

  • PasswordEncoder
    • spring-security 要求容器中必须要有 PasswordEncoder 实例,所以当自定义登陆逻辑时要求必须给容器注入 PasswordEncoder 的 bean 对象
    • PasswordEncoder - api
      • encode 把参数按照特定的规则进行解析
      • matches 编码前后的密码是否匹配
  • BCryptPasswordEncoder
    • BCryptPasswordEncoder 是spring官方推荐的密码解析器
@Bean // @Bean将该对象放入容器中
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

passwordEncoder().encode("111") // encode

(二) spring-boot-starter-security

(0) RBAC 权限模型

  • RBAC基本概念
    • RBAC 是 ( Role-Based Access Control ) 的缩写,表示 ( 基于角色的访问控制 )
    • 一个 ( 用户 ) 拥有多种 ( 角色 ),一个角色有多种 ( 权限 )
  • 关系
    • 一对多
      • 一个用户对应一个角色,一个角色覆盖多个用户 => 一对多
      • ( 一对多的关系 ),需要把 ( 关系字段 ) 加在 ( 多的那一张表中 ) - 见第三张图
      • 比如学生和老师,是让学生记老师更容易
    • 多对多
      • 一个用户对应多个角色,一个角色覆盖多个用户
      • ( 多对多的关系 ),需要 ( 单独创建一张表 ) 来 ( 维护关系 ) ------ 见第四张图 image.png

image.png

image.png

image.png

image.png

(1) 安装和内存级别的用户认证

(1.1) 安装依赖

<!-- 安全框架 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

(1.2) 初体验

  • 在controller中随便写个controller进行验证
  • 访问url则先会跳到 spring-security 提供的登陆页面,密码会在命令行中提供
  • 密码每次重新启动都会重新生成新的密码
  • 默认的 ( 用户名user ) ( 密码是命令行中的随机字符串 ) image.png

(1.3) 自定义登陆逻辑

  • 在1.2中,用户名只能是admin,密码也只能是打印的字符串
  • 如何实现自定义的登陆逻辑呢
    • 比如
      • 我想输入用户名admin,密码admin,就通过
      • 其他任何用户名和密码都不通过
    • 具体步骤
      • 1.新建 service/TestSecurityService
      • 2.实现 impletes UserDetailsService 接口
      • 3.重写 loadUserByUsername 方法
@Service
@Slf4j
public class TestSecurityService implements UserDetailsService {
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("+++++++++{}", username);
        if (!"admin".equals(username)) { // 如果输入的用户名不是admin就报错
            throw new UsernameNotFoundException("只能用admin登陆");
        }
        String password = new BCryptPasswordEncoder().encode("admin");

        return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,guest"));
    }
}

(1.4) 自定义表单登陆页

  • 不设置的情况下,登陆页是有spring-security提供,我们需要使用自定义的表单
config/SecurityConfig.java
-------

@Configuration
@EnableWebSecurity // 启动 spring-security 安全框架内容
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级别的角色认证
class SecurityConfig extends WebSecurityConfigurerAdapter { // WebSecurityConfigurerAdapter 用来控制安全管理的内容

    @Bean // @Bean将该对象放入容器中
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 自定义认证配置
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder pe = passwordEncoder(); // 调用下面定义的方法,返回PasswordEncoder类型
        auth.inMemoryAuthentication()
                .withUser("woow_wu7") // 用户名
                .password(pe.encode("111")) // 密码,加密
                .roles("admin", "guest"); // 角色

    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 让 spring-security 放行 js css images 文件,不进行拦截
        web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated() // 表示所有请求都要 ( 认证 ) 后才能访问
                .and()
                .formLogin() //-----------------------------【】
                .loginPage("/login.html") // ---------------【】登陆页面 ( 如果不设置下面的loginProcessingUrl =>  登陆页面 和 登陆接口 都是 login )
                .loginProcessingUrl("/security-test") //----【】提交username和password的url
                .usernameParameter("username") // ----------【】自定义html表单中的input的name属性
                .passwordParameter("password") // ----------【】自定义html表单中的input的name属性
                .successForwardUrl("/musics") // -----------【】【服务端跳转】登陆成功的跳转地址,不管是从什么地方跳转到登陆页面的,登陆成功后,都是跳到这里指定的路由
                // .defaultSuccessUrl("/musics") // -----------【】【重定向】登陆成功后,会回到之前的页面
                .permitAll() // ----------------------------【】表示:登陆相关的页面都 放行 不拦截
                // .and()
                // .logout() // 注销登陆
                // .logoutUrl("/logout") // 注销登陆的地址
                // .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "post"))
                .and()
                .csrf().disable(); // 关掉csrf
    }
}


(1.5) 自定义登陆成功后的路由跳转 - successHandler

@Configuration
@EnableWebSecurity // 启动 spring-security 安全框架
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级别的角色认证
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/security-admin/**").hasAnyRole("admin") //《》这个controller需要 admin 角色  
                .antMatchers("/security-guest/**").hasAnyRole("guest") //《》这个controller需要 guest 角色
                .anyRequest().authenticated() // 表示除了上面两个是 ( 角色级别的认证 ) 外,其他所有请求都要 ( 认证级别 ) 后才能访问,即只有上面两个需要登陆和对应的角色,其他都只需要登陆
                .and()
                .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/doLogin")
                .usernameParameter("username") 
                .passwordParameter("password") 
                .successForwardUrl("/musics")
                .successHandler(new AuthenticationSuccessHandler() { // --- 登陆成功后的逻辑处理
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                        httpServletResponse.sendRedirect("/musics"); // --- 登陆成功后跳转 /musics
                    }
                })
                .permitAll()
                .and()
                .csrf().disable(); // 关掉csrf
    }

}

(1.6) 如何修改 ( 用户名 ) 和 ( 密码 )

  • 一共有三种方式修改用户名和密码
    • application.yml 中配置
    • 通过 java代码配置在 内存中
    • 通过java代码从 数据库中 加载
  • 关键词
    • WebSecurityConfigurerAdapter
    • @EnableWebSecurity
(1)  application.yml 中设置用户名/密码
spring:
  security:
    user:
      name: woow_wu7
      password: 123
(2) 在内存中设置用户名/密码
- 2.1 新建一个配置类 ( SecurityConfig ),继承 ( WebSecurityConfigurerAdapter )
- 2.2SecurityConfig 添加两个注解,一个是 ( @Configuration ),一个是 ( @EnableWebSecurity )
    - @Config 表示配置类
    - @EnableWebSecurity 表示开启spring-security安全框架内容
- 2.3 通过参数 auth 设置用户名,密码,角色
@Configuration
@EnableWebSecurity // 启动 spring-security 安全框架内容
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("woo_wu7") // 用户名
                .password("111") // 密码
                .roles("admin"); // 角色
                
        auth.inMemoryAuthentication()
                .withUser("woow_wu8") // 用户名
                .password("222") // 密码
                .roles(); // 角色
    }
}


--------------------------
以上设置后,会报错,因为密码不能是明文的,需要加密
报错信息:There is no PasswordEncoder mapped for the id "null"
报错表示:之所以报错,表示密码需要加密,因为在spring-security5版本中要求密码加密,否则报错
解决报错:在 2.4 中加密密码
--------------------------
(3) 在内存中配置用户名,除了上面的方法外,还可以通过重写 userDetailsService 方法
- 下面两种方法都可以
-------
    // 1
    // 自定义认证配置
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder pe = passwordEncoder(); // 调用下面定义的方法,返回PasswordEncoder类型
        auth.inMemoryAuthentication()
                .withUser("admin") // 用户名
                .password(pe.encode("admin")) // 密码,加密
                .roles("admin") // 角色
                .and()
                .withUser("guest")
                .password("guest")
                .roles("guest");
    }

    // 2
    // 除了上面的方法可以设置用户名和密码外,还可以用下面的方式
    // - 通过重写 ( userDetailsService ) 方法实现
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("admin2").password("admin2").roles("admin").build());
        return manager;
    }

(1.7) 密码加密

  • PasswordEncoder
@Configuration
@EnableWebSecurity // 启动 spring-security 安全框架内容
public class SecurityConfig extends WebSecurityConfigurerAdapter { // WebSecurityConfigurerAdapter 用来控制安全管理的内容

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder pe = passwordEncoder(); // ---------------- 调用下面定义的方法,返回PasswordEncoder类型

        auth.inMemoryAuthentication()
                .withUser("woow_wu7") // 用户名
                .password(pe.encode("111")) // 密码 ---------------- 加密
                .roles(); // 角色

        auth.inMemoryAuthentication()
                .withUser("woow_wu8") // 用户名
                .password(pe.encode("222")) // 密码
                .roles(); // 角色
    }

    @Bean // @Bean将该对象放入容器中
    public PasswordEncoder passwordEncoder() { // ----------------- 加密方法
        return new BCryptPasswordEncoder(); // -------------------- 是一种加密方式
    }
}

(1.8) 添加 ( 角色 ) 信息,并实现 ( 方法级别 ) 的角色验证

  • ( 同一个用户 ) 可以有 ( 不同的角色 )
  • 下面的案列是基于 ( 方法级别 ) 的 ( 角色 ) 认证
  • 关键词
    • @EnableGlobalMethodSecurity
    • prePostEnabled
      • @PreAuthorize
      • @PostAuthorize
  • 开启方法级别的角色认证具体过程
    • 编写@Configuration注册类,并extends继承 WebSecurityConfigurerAdapter
    • 通过@Overvide重写configure方法,AuthenticationManagerBuilder,通过auth参数-auth.inMemoryAuthentication-指定用户的角色
    • 通过@EnableGlobalMethodSecurity(prePostEnabled = true)启用方法级别角色验证
    • 在controller的方法上添加角色信息,通过 @PreAuthorize(value = "hasAnyRole('admin', 'guest')") 指定方法可以访问的角色列表
config/SecurityConfig
-------


/****
 *
 * @EnableGlobalMethodSecurity
 *  作用:启用方法级别的角色认证
 *  参数: ( prePostEnabled=true ) => 表示可以使用 ( @PreAuthorize ) 注解 和 ( @PostAuthorize )
 *
 * **/
@Configuration
@EnableWebSecurity // 启动 spring-security 安全框架内容
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter { // WebSecurityConfigurerAdapter 用来控制安全管理的内容

//    @Override
//    public void configure(WebSecurity web) throws Exception {
//        web.ignoring().antMatchers("/test-security"); // 过滤掉 ( /test-security )
//        super.configure(web);
//    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder pe = passwordEncoder(); // 调用下面定义的方法,返回PasswordEncoder类型

        auth.inMemoryAuthentication()
                .withUser("woow_wu7") // 用户名
                .password(pe.encode("111")) // 密码,加密
                .roles("admin", "guest"); // 角色

        auth.inMemoryAuthentication()
                .withUser("woow_wu8") // 用户名
                .password(pe.encode("222")) // 密码
                .roles("guest"); // 角色
    }

    @Bean // @Bean将该对象放入容器中
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
}
controller/TestSecurityController
-------

// admin 和 guest 都能访问
@GetMapping("/test-security-all")
@PreAuthorize(value = "hasAnyRole('admin', 'guest')") // admin 和 guest 都能访问
public String all() {
    return "@PreAuthorize(value = \"hasAnyRole('admin', 'guest')\")";
};

// admin 才能访问
@GetMapping("/test-security-only")
@PreAuthorize(value = "hasAnyRole('admin')") // 只有 admin 可以访问
public String only() {
    return "@PreAuthorize(value = \"hasAnyRole('admin')\")";
}
  • 当输入用户woow_wu8时,访问 /test-security-only 则出现不允许访问,因为该用户的角色不满足contrller中的角色验证,只有admin角色可以访问该地址 image.png

(1.9) 忽略拦截 - 关闭验证功能

  • 如果一个URL地址不需要拦截,则有两种方式实现
    • 设置该url地址匿名访问
    • 通过 Spring-security 过滤该URL地址,即过滤掉该controller中的方法
(1) 方法1
// 主程序类,主配置类
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class}) 排除security配置,让spring-security 不起作用
public class Application {
}

(2) 前后端分离 - 返回 json 数据

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated() // 表示所有请求都要 ( 认证 ) 后才能访问
                .and()
                .formLogin() //-----------------------------【】
                .loginPage("/login.html") // ---------------【】登陆页面 ( 如果不设置下面的loginProcessingUrl =>  登陆页面 和 登陆接口 都是 login )
                .loginProcessingUrl("/security-test") //----【】提交username和password的url
                //   .usernameParameter("username") // ----------【】自定义html表单中的input的name属性
                //   .passwordParameter("password") // ----------【】自定义html表单中的input的name属性
                //   .successForwardUrl("/musics") // -----------【】【服务端跳转】登陆成功的跳转地址,不管是从什么地方跳转到登陆页面的,登陆成功后,都是跳到这里指定的路由
                //   .defaultSuccessUrl("/musics") // -----------【】【重定向】登陆成功后,会回到之前的页面
                .successHandler((req, resp, authentication) -> { // =============== 登陆成功的回调函数
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter writer = resp.getWriter();
                    writer.write(new ObjectMapper().writeValueAsString(authentication.getPrincipal()));
                    writer.flush();
                    writer.close();
                })
                .failureHandler((req, resp, exception) -> { // ===================== 登陆失败的回调函数
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter writer = resp.getWriter();
                    writer.write(new ObjectMapper().writeValueAsString(exception.getMessage()));
                    writer.flush();
                    writer.close();
                })
                .permitAll() // ----------------------------【】表示:登陆相关的页面都 放行 不拦截
                // .and()
                // .logout() // 注销登陆
                // .logoutUrl("/logout") // 注销登陆的地址
                // .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "post"))
                .and()
                .csrf().disable(); // 关掉csrf
    }

(3) 授权 - 验证不同controller需要的不同角色

  • 注意
    • 除了这里这种方法外
    • 1.6中的方法也可以,即通过 @PreAuthorize
  • 在 ( SecurityConfig ) 的 ( WebSecurityConfigurerAdapter ) 重写的 ( configure ) 方法中指定
    • 指定用户信息:用户名,密码,角色
    • 指定访问路由需要的角色
(1) 在 SecurityConfig 文件中定义 ( 用户登陆时的角色信息 ) 和 ( 角色权限规则 )
------

![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/145438caf77c4d3cb934c3cf01df2586~tplv-k3u1fbpfcp-watermark.image)
@Configuration
@EnableWebSecurity // 启动 spring-security 安全框架
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级别的角色认证
class SecurityConfig extends WebSecurityConfigurerAdapter { // WebSecurityConfigurerAdapter 用来控制安全管理的内容

    @Bean // @Bean将该对象放入容器中
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 自定义认证配置
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder pe = passwordEncoder(); // 调用下面定义的方法,返回PasswordEncoder类型
        auth.inMemoryAuthentication()
                .withUser("admin") // -------------- 用户名
                .password(pe.encode("admin")) // --- 密码,加密
                .roles("admin") // ----------------- 角色
                .and()
                .withUser("guest")
                .password("guest")
                .roles("guest");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/security-admin/**").hasAnyRole("admin") //《》这个controller需要 admin 角色
                .antMatchers("/security-guest/**").hasAnyRole("guest") //《》这个controller需要 guest 角色
                .anyRequest().authenticated() // 表示除了上面两个外,所有请求都要 ( 认证 ) 后才能访问
                .and()
                .formLogin()
                .loginProcessingUrl("/doLogin")
                .permitAll()
                .and()
                .csrf().disable(); // 关掉csrf
    }
}
(2) 测试的controller
-------

@GetMapping("/hello") //----------------- 不需要权限即可访问
public String login() {
    return "hello";
}

@GetMapping("/security-admin") // ------- 需要admin角色权限才能访问
public String getAdmin() {
    return "admin";
}

@GetMapping("/security-guest") // ------- 需要guest角色权限才能访问
public String getGuest() {
    return "guest";
}

@GetMapping("/security-admin-other-method")
@PreAuthorize(value = "hasAnyRole('admin')") // ------ 这种方法也可以
public String getAdminByOtherMethod() {
    return "@PreAuthorize(value = \"hasAnyRole('admin')\")";
}

@GetMapping("/security-guest-other-method")
@PreAuthorize(value = "hasAnyRole('guest')") // ------ 这种方法也可以
public String getGuestByOtherMethod() {
    return "@PreAuthorize(value = \"hasAnyRole('guest')\")";
}
  • (3.1) 先登出,/logout image.png

  • (3.2) 输入用户名,密码,此时的用户已经在内存中携带了角色信息role

    • 我们用admin账号登陆
    • 角色admin可以访问/security-admin
    • 角色guest可以访问/security-guest
    • 任何角色 都可以访问 /hello

image.png

  • (3.3) 访问 /security-admin

    • 可以正常访问 image.png
  • (3.4) 访问 /security-guest

    • 不能访问
    • 角色不匹配 image.png
  • (3.5) 访问 /security-hello

    • 可以访问
    • 因为 admin 和 guest 都可以访问,没有做限制 image.png
  • (3.6) 除了在config中指定controller中的方法需要的角色,还可以在controller的方法中通过 @PreAuthorize 来指定

    • @PreAuthorize
    • 可以访问 image.png

(三) 通过数据库实现RBAC

(3.1) UserDetails 和 UserDetailsService

  • UserDetails
    • 表达你是谁,你有什么角色权限
  • UserDetailsService
    • 表达如何动态加载UserDetails数据

(3.2) 实现 UserDetails 接口

  • 新建 moudle/MyUserDetails
  • implements 实现 UserDetails 接口
  • 并通过 command+1 => implement methods 实现接口中的所有方法
    • getAuthorities 权限列表
    • getPassword 密码
    • getUsername 用户名
    • isAccountNonExpired 是否没过期
    • isAccountNonLocked 是否没被锁定
    • isCredentialsNonExpired 是否没过期
    • isEnabled 账号是否可用
  • 实现了 ( UserDetails ) 的类就具有了 ( 用户名 密码 权限列表 账号是否可用 ) 等信息

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Component
public class MyUserDetails implements UserDetails {

    String password;
    String username;

    boolean accountNonExpired = true; // 是否没过期
    boolean accountNonLocked = true; // 是否没被锁定
    boolean credentialsNonExpired = true; // 是否没过期
    boolean enabled; // 账号是否可用
    Collection<? extends GrantedAuthority> authorities; // 用户的权限集合
}

(3.3) 实现 UserDetailsService 接口

  • 新建 service/MyUserDetailsSerivce
  • implements 实现 UserDetailsService 接口
  • 并通过 command+1 => implement methods 实现接口中的 loadUserByUsername方法
  • 具体流程
    • 1.通过登陆时传入的唯一标识,去数据库查询用户的相关信息
      • 比如:用户名,密码,权限,账户是否可用等信息
    • 2.把查询到的信息组装成一个UserDetails对象,即MyUserDetails对象
    • 3.把MyUserDetails对象以返回值的方式提供给spring-security
    • 4.spring-security根据拿到的用户信息做登陆,认证,鉴权等相关操作
@Component
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    MyUserDetailsServiceMapper myUserDetailsServiceMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        // s 表示登陆时传入的唯一标识,不一定是用户名

        // 1 用户基础数据加载
        MyUserDetails myUserDetails = myUserDetailsServiceMapper.findByUserName(s);
        if (myUserDetails == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }

        // 2 加载用户的角色列表
        List<String> roleCodes = myUserDetailsServiceMapper.findRoleByUserName(s);

        // 3 根据角色列表加载当用用户具有的权限
        List<String> authorities = myUserDetailsServiceMapper.findAuthorityByRoleCodes(roleCodes);

        roleCodes = roleCodes.stream()
                .map(rc -> "ROLE_" + rc)
                .collect(Collectors.toList());

        authorities.addAll(roleCodes);

        myUserDetails.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(
                String.join(",", authorities)
        ));

        return myUserDetails;
    }
}

(3.3) 实现 SecurityCofnig

  • config 中新建 SecurityCofnig
  • implements 实现 WebSecurityConfigurerAdapter 接口
  • 重写 configure 方法
  • 将我们之前写在内存中的用户信息替换成数据库中查询到的用户信息
    • auth.userDetailsService(myUserDetailsService).passwordEncoder(pe)
package com.example.demo.config;

import com.example.demo.service.MyUserDetailsService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.io.PrintWriter;

public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    MyUserDetailsService myUserDetailsService;


    @Bean // @Bean将该对象放入容器中
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    // 自定义认证配置
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder pe = passwordEncoder(); // 调用下面定义的方法,返回PasswordEncoder类型
        auth.userDetailsService(myUserDetailsService)
                .passwordEncoder(pe); // BCrypt加密

        //        auth.inMemoryAuthentication()
        //                .withUser("admin") // 用户名
        //                .password(pe.encode("admin")) // 密码,加密
        //                .roles("admin") // 角色
        //                .and()
        //                .withUser("guest")
        //                .password("guest")
        //                .roles("guest");
    }

    // 除了上面的方法可以设置用户名和密码外,还可以用下面的方式
    // - 通过重写 ( userDetailsService ) 方法实现
    //    protected UserDetailsService userDetailsService() {
    //        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    //        manager.createUser(User.withUsername("admin2").password("admin2").roles("admin").build());
    //        return manager;
    //    }


    @Override
    public void configure(WebSecurity web) throws Exception {
        // 让 spring-security 放行 js css images 文件,不进行拦截
        web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/security-admin/**").hasAnyRole("admin") //《》这个controller需要 admin 角色
                .antMatchers("/security-common/**").hasAnyRole("common") //《》这个controller需要 guest 角色
                .anyRequest().authenticated() // 表示除了上面两个是 ( 角色级别的认证 ) 外,其他所有请求都要 ( 认证级别 ) 后才能访问,即只有上面两个需要登陆和对应的角色,其他都只需要登陆
                .and()
                .formLogin() //----------------------------------【】
                .loginPage("/login.html") // ---------------【】登陆页面 ( 如果不设置下面的loginProcessingUrl =>  登陆页面 和 登陆接口 都是 login )
                .loginProcessingUrl("/doLogin")
                //   .loginProcessingUrl("/security-test") //----【】提交username和password的url
                //   .usernameParameter("username") // ----------【】自定义html表单中的input的name属性
                //   .passwordParameter("password") // ----------【】自定义html表单中的input的name属性
                //   .successForwardUrl("/musics") // -----------【】【服务端跳转】登陆成功的跳转地址,不管是从什么地方跳转到登陆页面的,登陆成功后,都是跳到这里指定的路由
                //   .defaultSuccessUrl("/musics") // -----------【】【重定向】登陆成功后,会回到之前的页面
                .successHandler((req, resp, authentication) -> { // =============== 登陆成功的回调函数
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter writer = resp.getWriter();
                    writer.write(new ObjectMapper().writeValueAsString(authentication.getPrincipal()));
                    writer.flush();
                    writer.close();
                })
                .failureHandler((req, resp, exception) -> { // ===================== 登陆失败的回调函数
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter writer = resp.getWriter();
                    writer.write(new ObjectMapper().writeValueAsString(exception.getMessage()));
                    writer.flush();
                    writer.close();
                })
                .permitAll() // ----------------------------【】表示:登陆相关的页面都 放行 不拦截
                // .and()
                // .logout() // 注销登陆
                // .logoutUrl("/logout") // 注销登陆的地址
                // .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "post"))
                .and()
                .csrf().disable(); // 关掉csrf
    }

}


资料

spring-security juejin.cn/post/684490…
UserDetailService zhuanlan.zhihu.com/p/188747719
RBAC juejin.cn/post/684490…
RBAC www.jianshu.com/p/ce1757360…