这里写目录标题
-
- 资源,权限,角色,用户
-
- RBAC
- 数据表设计
- SpringSecurity配合RBAC补全权限验证
-
- 背景:
- 基本实现步骤
- 实现记住我功能
- 实现SpringSecurity版验证码登录
- 权限管理的使用
资源,权限,角色,用户
权限管理是为了管理用户行为,保护目标系统的安全,权限 = 权利 + 限制
1 资源的定义:
资源就是系统需要保护起来的功能,有url,handler方法,service方法,页面元素…
2 权限的定义
因为一个功能的完成可能涉及到成千上万的资源,如果以资源做为权限分配的基本单位那就很不痛快,我们将n个相关操作的资源封装为一个"权限",再将这个权限分配给有需要的人,这可以看出是对权限操作的简化,资源可以看成一个handler
3 角色
将资源包装为权限是对资源管理的简化,而用户的数量同样不是小数目,一个个的管理是不现实的,我们可以将用户划分不同的角色,即角色就是用户的分类分组。资源封装为权限,权限分配给角色,再给用户指定角色,这也是RABC模型的大纲
4 用户
具有系统使用权限和登录账号的一类人
权限 => 资源
数据库层面n:n
,java层面1:n
(每个权限类封装一个资源列表,但是资源类中不需要考虑权限这个属性,即不需要知道这个资源对应那些权限)
角色 => 权限
数据库层面n:n
,java层面1:n
(角色类封装一个权限列表,但权限类不需要考虑角色这个属性,即不需要考虑权限对应对应那些角色)
用户 => 角色
数据库层面n:n
,java层面n:n
(用户类封装一个角色列表来表示这个用户所具有的角色,也可以查看这个这个角色下的所有用户)
RBAC
(Role-Base-Access Controller) 用户通过角色获得相关的权限,一个用户可以有多种角色,一个角色对于多种权限,一个权限代表一个功能
1 互斥角色:不能同时给用户分配到一起的角色,如会计师和审计师角色
2 约束角色:角色的访问权限和旗下的用户数量是受限的,一个用户拥有的角色数量也受限
先决条件就是,角色拥有A权限的条件是拥有B权限,如用户升到vip6级的条件是要达到vip5级
3 动态分离级别:马云在阿里巴巴内部激活创始人身份,在企业论坛上激活演讲嘉宾角色
数据表设计
五张基础表
用户(admin)表
角色表role
权限表
name命名规范如user:save,表示具有保存角色的权限
用户-角色表
角色-权限表
SpringSecurity配合RBAC补全权限验证
背景:
使用SpringSecurity前,我们已经将auth分配给了role,将role分配给了admin,形参了一条完整的admin-role-auth链,之前的拦截器代码或配置的放行资源信息都注释掉决不能共存
基本实现步骤
1 在springsecurity的配置类中配置放行的资源 ,通过继承WebSecurityConfigurerAdapter然后重写configure(HttpSecurity security),这里面配置以下内容
- 定制放行资源,有登录的首页,退出登录,全部的静态资源img/jquery/css…,除了这些放行到的其它都要进行验证
- 开启form表单登录功能、指定登录的页面、指定处理登录请求的handler、指定登录成功后的跳转页面, 指定from表单的登录框和密码框的name值。注意:此时已经没有真正的登录处理handler方法了,有的只是在configure中进行的登录验证。
- 指定资源访问权限,例如admin主页面只有经理采用访问权限,也可以采用在handler方法上加注解的形式控制
- 开发阶段禁用csrf
- 搭建springsecurity独立的异常处理体系
- 开启退出登录功能、处理方法、退出后的跳转页面
- 配置7天内记住我功能①绑定数据源②绑定登录handler③
2 定义SecurityAdmin,这个类作为SpringSecurity权限处理后的封装结果集来使用,来将admin对象和该对象所具有的权限(角色+权限)绑定,绑定admin对象的username、password、authorites这三类信息,用户认证成功信息会被SecurityAdmin封装到principle变量并被存储在xxx类,调用父类的构造器验证登录用户的合法性并擦除对象密码,进一步提高安全性,这样方便前台打印(前端代码中不要忘记引入springsecurity标签库)。
3 重写configure(AuthenticationManagerBuilder builder)
- 可以在临时在内存中存储登录信息测试代码,项目初始化测试和部署到服务器上的时候必须考虑这一点。 易错点:如果前端引入了springsecurity标签库并且用principle进行了前端参数回显会产生报错,因为principle这个参数是从secuityAdmin中获取到的,内存帮登录信息不涉及securityAdmin
- 测试完毕将内存登录改装成数据库版登录测试
①定义UserDetailsService实现类,步骤如下:
注入adminService/RoleService/AuthService这三个对象 => 接收username(参数)=> 获取对应的admin =>取出adminId => 根据adminId获取该角色的权限集合和角色集合 => 遍历权限和角色集合(角色集合名前面加ROLE_来区分角色与权限)将其封装为SimpleGrantedAuthority加入已创建的权限列表=>将获取的admin对象及它的authorities封装为SecurityAdmin对象并返回
密码加密
1) 装配BCryptPasswordEncoder组件,configure方法中使用该组件进行密码加密
2)淘汰之前写的md5加密方法,并在之前写的保存用户方法中更换掉md5加密方法
4 前端
①form填写handler路径
②前端重点参数
${SPRING_SECURITY_LAST_EXCEPTION } #springsecurity前端错误信息提示,用于登录页面回显
<security:authentication property="principal.originalAdmin.userName"/> #公共区域侧边栏,显示已登录用户的名称信息
③注意,表单的用户名和密码文本框推荐自定义取name名,但要到配置类配置,否则springsecurity无法识别
④引入springsecurity标签库来表示已登录的信息
5 搭建异常处理体系
由下图可知springsecurity的异常是无法被springmvc的异常处理机制处理的,所以分为两部分,一部分是专门处理SpringMVC的dispacherServlet拦截的异常路径,另一部分是处理SpringSecurity拦截的
方法是在configure中搭建springsecurity独立的异常处理体系
总结:SpringSecurity中角色和权限近乎等价,也就是handler处理器所对应的资源除了和auth挂钩也可以和role挂钩,而且也可以将一份资源的访问权限设计为具有xxx角色或xxx权限,例如
.access("hasRole('经理') or hasAnyAuthority('user:get')") #具备经理或用户保存权限可以访问该资源的权限(管理admin信息的权限)
实现记住我功能
基本原理
具体实现方法
用户登录 => 第二步的登录过滤器验证 => 验证成功后相关组件生成Token => 该组件一方面将token写入浏览器的Cookie(cookie名就是前端checkbot的name)另一方面存入数据库中 => 用户下次登录时相关组件一方面查找浏览器cookie另一方面去数据库(persist_login)进行查找 =>对比两个值,相等则跳过springsecurity的登录验证,实现步骤如下
- 建立数据表persistent_ogin
- configure端配置七天内记住我功能,绑定数据源操作组件persistentTokenRepository和用户登录验证组件userDetailsService,配置连接保存时长为七天
- 前端配置记住我的cehckbox组件,name必须是固定的
结论
- 数据库端生成
- 浏览器端也有相应的生成
- 再次登录主页发现不需要验证(我们的设计是将http://ip:port/projectName/admin/to/main/page.html作为主页,用户选择记住我就不需要以登录的形式进行springsecurity验证而是直接就能跳转。反正,用户没有实现记住我,直接进入主页会被springsecurity拦截,造成权限不够而被强制以跳转到登录页的形式进行springsecurity验证)
实现SpringSecurity版验证码登录
1 导入spring-social-config依赖
2 entity编写验证码实体类IamgeCode
private BufferedImage image; //验证码图片
private String code; //code验证码
private LocalDateTime expireTime; //过期时间 单位秒
3 componet的mvc下编写验证码生成处理器ValidateCodeController
- createImageCode:工具方法,用于生成ImageCode类型的验证码信息
- sessionStrategy属性:使用sessionStrategy将生成的验证码对象存储到Session中
- createCode:将生成的验证码对象存储到Session中,并通过IO流将生成的图片输出到登录页面上,在前端页面通过该方法的访问路径加载生成的图片,并通过点击进行看不清刷新
<img src="code/image.html"/>
4 在springsecurity配置configure中放行生成的验证码handler路径资源的访问
5 编写过滤器ValidateCodeFilter进行登录时验证码的校验
- 继承OncePerRequestFilter,重写doFilterInternal
- doFilterInternal方法①如果拦截的页面是post请求的登录页面,那么就拦截下来有待进一步验证,其余静态资源全部放行②合法性验证:判断是否合法,不合法要返回相应的错误信息③验证通过后立即删除session中原先的验证码,防止影响下一次验证码校验
- 将该过滤器注入到WebAppSecurityConfig(相当于将该过滤器组件加入到了springsecurity容器中)并在configure中将该过滤器的执行顺序配置到验证登录的过滤器前面
security.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
权限管理的使用
在WebAppSecurityConfig这个springsecurity配置类上使用注解
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled=true,jsr250Enabled=true)
在配置类的configure中配置资源权限
.antMatchers("/admin/get/page.html").access("hasRole('经理') or hasAnyAuthority('user:get','user:save','user:delete','user:update')").antMatchers("/admin/remove/{adminId}/{pageNum}/{keyword}.html").access("hasRole('经理') or hasAuthority('user:delete')")
使用注解,案例如下
//管理员主页面,分页和查询功能在一块的@RequestMapping("/admin/get/page.html")@PreAuthorize("hasRole('经理') or hasAnyAuthority('user:get','user:save','user:delete','user:update')")public String getPageInfo(){.....}//删除管理员功能@PreAuthorize("hasRole('经理') or hasAuthority('user:delete')")@GetMapping("/admin/remove/{adminId}/{pageNum}/{keyword}.html")public String remove(){....}
经测试无效,原因是我们设置开启springsecurity注解是在WebAppSecurityConfig即springsecurity的ioc容器中配置的注解生效,但是controller是由springmvc的dispatcherServlet负责拦截的,所以最终controller逻辑注解被动态扫描到spring-mvc-web容器中,如果仅仅是在WebAppSecurityConfig开启注解生效是没有效果的,需要在spring-mvc-web也开启注解生效,配置内容如下,记得声明头部引用
<security:global-method-security secured-annotations="enabled"pre-post-annotations="enabled" jsr250-annotations="enabled"/>