【深入】聊聊权限
常见的权限模型
ACL
定义:规定资源可以被哪些主体进行哪些操作。同时,主体可以将资源、操作的权限,授予其他主体。
在ACL的基础上,DAC模型将授权的权力下放,允许拥有权限的用户,可以自主地将权限授予其他用户。

DAC 自主访问控制
定义:规定资源可以被哪些主体进行哪些操作。同时,主体可以将资源、操作的权限,授予其他主体。
在ACL的基础上,DAC模型将授权的权力下放,允许拥有权限的用户,可以自主地将权限授予其他用户。
MAC 强制访问控制
定义:当一个操作,同时满足a与b时,允许操作
a. 规定资源可以被哪些类别的主体进行哪些操作
b. 规定主体可以对哪些等级的资源进行哪些操作
MAC是ACL的另一种实现,强调安全性。MAC会在系统中,对资源与主体,都划分类别与等级。比如,等级分为:秘密级、机密级、绝密级;类别分为:军事人员、财务人员、行政人员。
MAC的优势就是实现资源与主体的双重验证,确保资源的交叉隔离,提高安全性。
RBAC 基于角色的访问控制
定义:当一个操作,同时满足a与b时,允许操作。
a. 规定角色可以对哪些资源进行哪些操作
b. 规定主体拥有哪些角色
RBAC的思想,来源于现实世界的企业结构。比如,销售角色,拥有查看客户信息的权限。当一个销售人员小王入职了,可以把销售角色赋予小王,那么小王就拥有了查看客户的权限。这种方式,避免了ACL模型下,每次新人入职,需要逐个配置资源表的情况。同样,权限变动也变得很方便,只要修改角色,即可实现多用户的权限修改。
ABAC 基于属性的访问控制
定义:规定哪些属性的主体可以对哪些属性的资源在哪些属性的情况下进行哪些操作
ABAC其中的属性就是与主体、资源、情况相关的所有信息。
主体的属性:指的是与主体相关的所有信息,包括主体的年龄、性别、职位等。
资源的属性:指的是与资源相关的所有信息,包括资源的创建时间、创建位置、密级等。
情况的属性:指的是客观情况的属性,比如当前的时间、当前的位置、当前的场景(普通状态、紧急状态)。
操作:含义还是一样,比如增删改查等。
设定一个权限,就是定义一条含有四类属性信息的策略(Policy)。
**一个请求会逐条匹配策略,如果没有匹配到策略,则返回默认效果,默认效果可以根据场景定制,可以是默认拒绝或是默认允许。**另外,匹配方式也可以根据场景定制,可以使用逐条顺序匹配,匹配到策略直接返回。也可以使用完全匹配,匹配所有的策略,如果有一个拒绝(允许),则拒绝(允许)。
Linux权限DAC 安全模型
DAC 的核心内容是:在 Linux 中,进程理论上所拥有的权限与执行它的用户的权限相同。其中涉及的一切内容,都是围绕这个核心进行的。
用户和组 ID 信息控制
用户、组、口令信息
通过 /etc/passwd 和 /etc/group 保存用户和组信息,通过 /etc/shadow 保存密码口令及其变动信息, 每行一条记录。
用户和组分别用 UID 和 GID 表示,一个用户可以同时属于多个组,默认每个用户必属于一个与之 UID 同值同名的 GID 。
对于 /etc/passwd , 每条记录字段分别为 用户名: 口令(在 /etc/shadow 加密保存):UID:GID(默认 UID): 描述注释: 主目录: 登录 shell(第一个运行的程序)
对于 /etc/group , 每条记录字段分别为 组名:口令(一般不存在组口令):GID:组成员用户列表(逗号分割的用户 UID 列表)
对于 /etc/shadow ,每条记录字段分别为:登录名: 加密口令: 最后一次修改时间: 最小时间间隔: 最大时间间隔: 警告时间: 不活动时间:
举例
以下是对用户和组信息的举例。/etc/shadow 中的口令信息为加密存储,不举例。
文件权限控制信息
文件类型
Linux 中的文件有如下类型:
普通文件, 又包括文本文件和二进制文件, 可用 touch 创建;
套接字文件, 用于网络通讯,一般由应用程序在执行中间接创建;
管道文件是有名管道,而非无名管道, 可用 mkfifo 创建;
字符文件和块文件均为设备文件, 可用 mknod 创建;
链接文件是软链接文件,而非硬链接文件, 可用 ln 创建。
访问权限控制组
分为三组进行控制:
user 包含对文件属主设定的权限
group 包含对文件属组设定的权限
others 包含对其他者设定的权限
可设定的权限
下面给出常见(但非全部)的权限值, 包括:
r 表示具有读权限。
w 表示具有写权限。
x 一般针对可执行文件 / 目录,表示具有执行 / 搜索权限。
s 一般针对可执行文件 / 目录,表示具有赋予文件属主权限的权限,只有 user 和 group 组可以设置该权限。
t 一般针对目录,设置粘滞位后,有权限的用户只能写、删除自己的文件, 否则可写、删除目录所有文件。旧系统还表示可执行文件运行后将 text 拷贝到交换区提升速度。
举例
通过 ls -l 可以查看到其文件类型及权限,通过 chmod 修改权限。
举例来说,
RBAC权限模型简介
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。模型中有几个关键的术语:
用户:系统接口及访问的操作者
权限:能够访问某接口或者做某操作的授权资格
角色:具有一类相同操作权限的用户的总称
RBAC权限模型核心授权逻辑如下:
某用户是什么角色?
某角色具有什么权限?
通过角色的权限推导用户的权限
RBAC的演化进程
用户与权限直接关联

想到权限控制,人们最先想到的一定是用户与权限直接关联的模式,简单地说就是:某个用户具有某些权限。如图:
张三具有创建用户和删除用户的权限,所以他可能系统维护人员
李四具有产品记录管理和销售记录管理权限,所以他可能是一个业务销售人员
这种模型能够清晰的表达用户与权限之间的关系,足够简单。但同时也存在问题:
现在用户是张三、李四,以后随着人员增加,每一个用户都需要重新授权
或者张三、李四离职,需要针对每一个用户进行多种权限的回收
一个用户拥有一个角色
在实际的团体业务中,都可以将用户分类。比如对于薪水管理系统,通常按照级别分类:经理、高级工程师、中级工程师、初级工程师。也就是按照一定的角色分类,通常具有同一角色的用户具有相同的权限。这样改变之后,就可以将针对用户赋权转换为针对角色赋权。

一个用户有一个角色
一个角色有多个操作(菜单)权限
一个操作权限可以属于多个角色
我们可以用下图中的数据库设计模型,描述这样的关系。

一个用户一个或多个角色
但是在实际的应用系统中,一个用户一个角色远远满足不了需求。如果我们希望一个用户既担任销售角色、又暂时担任副总角色。该怎么做呢?为了增加系统设计的适用性,我们通常设计:
一个用户有一个或多个角色
一个角色包含多个用户
一个角色有多种权限
一个权限属于多个角色
我们可以用下图中的数据库设计模型,描述这样的关系。

页面访问权限与操作权限
页面访问权限: 所有系统都是由一个个的页面组成,页面再组成模块,用户是否能看到这个页面的菜单、是否能进入这个页面就称为页面访问权限。
操作权限: 用户在操作系统中的任何动作、交互都需要有操作权限,如增删改查等。比如:某个按钮,某个超链接用户是否可以点击,是否应该看见的权限。

为了适应这种需求,我们可以把页面资源(菜单)和操作资源(按钮)分表存放,如上图。也可以把二者放到一个表里面存放,用一个字段进行标志区分。
数据权限
数据权限比较好理解,就是某个用户能够访问和操作哪些数据。
通常来说,数据权限由用户所属的组织来确定。比如:生产一部只能看自己部门的生产数据,生产二部只能看自己部门的生产数据;销售部门只能看销售数据,不能看财务部门的数据。而公司的总经理可以看所有的数据。
在实际的业务系统中,数据权限往往更加复杂。非常有可能销售部门可以看生产部门的数据,以确定销售策略、安排计划等。
所以为了面对复杂的需求,数据权限的控制通常是由程序员书写个性化的SQL来限制数据范围的,而不是交给权限模型或者Spring Security或shiro来控制。当然也可以从权限模型或者权限框架的角度去解决这个问题,但适用性有限。
Spring Security
springsecurity底层实现为一条过滤器链,就是用户请求进来,判断有没有请求的权限,抛出异常,重定向跳转。
springsecurity自带一个登录页。
从登陆入手,登录页替换成我们自己的,对输入的账号密码进行验证。
/*** 表单登陆security* 安全 = 认证 + 授权*/public class SecurityConfig extends WebSecurityConfigurerAdapter {protected void configure(HttpSecurity http) throws Exception {//以下五步是表单登录进行身份认证最简单的登陆环境http.formLogin() //表单登陆 1.and() //2.authorizeRequests() //下面的都是授权的配置 3.anyRequest() //任何请求 4.authenticated(); //访问任何资源都需要身份认证 5}}

如果只实现一个WebSecurityConfigurerAdapter然后重写一下configure方法,效果会默认使用springsecurity的登录页 ,以及项目启动时后台会打印出一个默认的密码,然后使用任意账号就可以进行登录访问指定的资源
如果想要使用自己的登录页 并且用户名密码是自己数据库中的,进一步完善spring security认证体系,首先需要做以下配置。
protected void configure(HttpSecurity http) throws Exception {//以下五步是表单登录进行身份认证最简单的登陆环境http.formLogin() //表单登陆 1.loginPage("/login.html") //指定登陆页面.and() //2.authorizeRequests() //下面的都是授权的配置 3.antMatchers("/login.html").permitAll()//访问此地址就不需要进行身份认证了,防止重定向死循环.anyRequest() //任何请求 4.authenticated(); //访问任何资源都需要身份认证 5}
然后实现UserDetailsService接口进行用户姓名密码校验 (由于springboot2.x中security是5.x版本的,所以这里的密码是默认做了BCrypt加密的,就需要bean一个BCrypt)
public class MyUserDetailService implements UserDetailsService {//注入mapper//...private PasswordEncoder passwordEncoder;private Logger LOG = LoggerFactory.getLogger(MyUserDetailService.class);public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {LOG.error("登陆用户输入的用户名:{}",s);//根据用户名查找用户信息//密码进行bcrypt加密String pwd = "wangkai";//String cryptPwd = BCrypt.hashpw(pwd, BCrypt.gensalt());String cryptPwd = passwordEncoder.encode(pwd);LOG.error("加密后的密码为: {}",cryptPwd);return new User("s",cryptPwd, AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); //账号 密码 权限}}
/*** 表单登陆security* 安全 = 认证 + 授权*/public class SecurityConfig extends WebSecurityConfigurerAdapter {/*** 介绍* springboot2.x引入的security版本是5.x的,这个版本需要提供一个PasswordEncoder实例,不然就会报错* @return*/public PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}protected void configure(HttpSecurity http) throws Exception {//以下五步是表单登录进行身份认证最简单的登陆环境http.formLogin() //表单登陆 1.loginPage("/login.html") //指定登陆页面.and() //2.authorizeRequests() //下面的都是授权的配置 3.antMatchers("/login.html").permitAll()//访问此地址就不需要进行身份认证了,防止重定向死循环.anyRequest() //任何请求 4.authenticated(); //访问任何资源都需要身份认证 5}}
添加登陆页面提交页面,关闭跨站请求伪造攻击,登陆访问资源
<html lang="en"><head><meta charset="UTF-8"><title>登陆</title></head><body><h2>标准登陆页面</h2><h3>表单登陆</h3><form action = "/authentication/form" method ="post"><table><tr><td>用户名:</td><td><input type="text" name="username"></td></tr><tr><td>密码:</td><td><input type="password" name="password"></td></tr><tr><td colspan="2"><button type="submit">登陆</button></td></tr></table></form></body></html>
@Overrideprotected void configure(HttpSecurity http) throws Exception {//以下五步是表单登录进行身份认证最简单的登陆环境http.formLogin() //表单登陆 1.loginPage("/login.html") //指定登陆页面.loginProcessingUrl("/authentication/form")//登陆页面提交的页面 开始使用UsernamePasswordAuthenticationFilter过滤器处理请求.and() //2.authorizeRequests() //下面的都是授权的配置 3.antMatchers("/login.html").permitAll()//访问此地址就不需要进行身份认证了,防止重定向死循环.anyRequest() //任何请求 4.authenticated() //访问任何资源都需要身份认证 5.and().csrf().disable();//关闭跨站请求伪造攻击拦截}
动态配置登录页

.做一个我们自己默认的登录页,如果不想用默认的也可以动态配置。使用到的注解@ConfigurationProperties。
.增加接口/authentication/require
.引导用户进入登录页登陆
@Overrideprotected void configure(HttpSecurity http) throws Exception {//以下五步是表单登录进行身份认证最简单的登陆环境http.formLogin() //表单登陆 1//.loginPage("/login.html") //指定登陆页面.loginPage("/authentication/require").loginProcessingUrl("/authentication/form")//登陆页面提交的页面 开始使用UsernamePasswordAuthenticationFilter过滤器处理请求.and() //2.authorizeRequests() //下面的都是授权的配置 3.antMatchers("/login.html","/authentication/require",securityProperties.getBrowser().getLoginPage()).permitAll()//访问此地址就不需要进行身份认证了,防止重定向死循环.anyRequest() //任何请求 4.authenticated() //访问任何资源都需要身份认证 5.and().csrf().disable();//关闭跨站请求伪造攻击拦截}
public class BrowserSecurityController {private Logger LOG = LoggerFactory.getLogger(BrowserSecurityController.class);//将当前请求缓存到session里private RequestCache requestCache = new HttpSessionRequestCache();private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();private SecurityProperties securityProperties;/*** 当需要身份认证时跳转到这里* @param request* @param response* @return*/(value = "/authentication/require",method = RequestMethod.GET)(code = HttpStatus.UNAUTHORIZED) //未授权状态码public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {//拿到引发跳转的请求SavedRequest savedRequest = requestCache.getRequest(request,response);if(savedRequest != null){String targetUrl = savedRequest.getRedirectUrl();String fileUrl=new URL(targetUrl).getFile();LOG.info("引发跳转的请求是:{}",targetUrl);if(StringUtils.endsWithIgnoreCase(targetUrl,".html") || fileUrl.equals("/")){//调转到登录页 》》这里登录页做成可配置的redirectStrategy.sendRedirect(request,response,securityProperties.getBrowser().getLoginPage());}}return new SimpleResponse("访问资源需要登陆,请访问登陆页面");}}
从配置文件中读取当访问资源需要身份认证调转的页面地址
server.port=8888 #自定义springsecurity 登录页面 security.browser.loginPage = /mylogin.html
package com.example.security.properties;import com.example.security.pojo.SecurityBrowserPojo;import org.springframework.boot.context.properties.ConfigurationProperties;/*** 实现动态配置用户专属登陆页面*/(prefix = "security")public class SecurityProperties {private SecurityBrowserPojo browser = new SecurityBrowserPojo();public SecurityBrowserPojo getBrowser() {return browser;}public void setBrowser(SecurityBrowserPojo browser) {this.browser = browser;}}
public class SecurityBrowserPojo {//设置默认地址private String loginPage = "/login.html";public String getLoginPage() {return loginPage;}public void setLoginPage(String loginPage) {this.loginPage = loginPage;}}
package com.example.security.config.securityconfig;import com.example.security.properties.SecurityProperties;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.context.annotation.Configuration;//设置注解读取生效 (试了下不用配置这里@ConfigurationProperties也可以生效)public class SecurityPropertiesConfig {}
某些时候用户登陆成功,登陆失败的时候可能还需要做一些操作,比如成功登陆增加一积分之类的操作,这里需要做两个handler处理器
/*** 设置通过请求拦截。登陆成功后处理*/("wawAuthenticationSuccessHandler")public class WawAuthenticationSuccessHandler implements AuthenticationSuccessHandler {private Logger LOG = LoggerFactory.getLogger(WawAuthenticationSuccessHandler.class);private ObjectMapper objectMapper;/*** @param authentication 封装认证信息>>用户信息 请求ip之类的* @throws IOException* @throws ServletException*/public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {LOG.info("登陆成功");response.setContentType("application/json;charset=UTF-8");response.getWriter().write(objectMapper.writeValueAsString(authentication));}}
/*** 设置通过请求拦截。登陆失败后处理*/("wawAuthenticationFailHandler")public class WawAuthenticationFailHandler implements AuthenticationFailureHandler{private Logger LOG = LoggerFactory.getLogger(WawAuthenticationFailHandler.class);private ObjectMapper objectMapper;public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {LOG.info("登陆失败");response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());response.setContentType("application/json;charset=UTF-8");response.getWriter().write(objectMapper.writeValueAsString(e));}}
成功与失败的处理器 配置到配置信息中
@Overrideprotected void configure(HttpSecurity http) throws Exception {//以下五步是表单登录进行身份认证最简单的登陆环境http.formLogin() //表单登陆 1//.loginPage("/login.html") //指定登陆页面.loginPage("/authentication/require").loginProcessingUrl("/authentication/form")//登陆页面提交的页面 开始使用UsernamePasswordAuthenticationFilter过滤器处理请求.successHandler(wawAuthenticationSuccessHandler).failureHandler(wawAuthenticationFailHandler).and() //2.authorizeRequests() //下面的都是授权的配置 3.antMatchers("/authentication/require","/login.html",securityProperties.getBrowser().getLoginPage()).permitAll()//访问此地址就不需要进行身份认证了,防止重定向死循环.anyRequest() //任何请求 4.authenticated() //访问任何资源都需要身份认证 5.and().csrf().disable();//关闭跨站请求伪造攻击拦截}
登陆失败就会返回500 登陆异常信息

