基于Session-Cookie
机制的认证是比较原始的一种认证方式,由于HTTP
协议是纯文本,无状态的传输协议,那么在一些需要记录状态的场景就很麻烦,如淘宝的购物车,不同用户登录后看到的购物车数据是不一致的;所以需要一种机制能让服务端知道请求的客户端是谁。这样Cookie就应运而生,在客户端登录成功后,服务端返回的响应报文头中会带上一个set-cookie
,客户端浏览器判断拿到这个请求头后会放在本地,每次访问cookie中指定的路径时都会带上到请求头中。
基于Session-Cookie
会带来一系列的问题。比如:
Session
都保存在内存中,登录用户过多后会对服务端造成较大的压力CSRF
攻击 为解决Session
存储在服务端导致的性能瓶颈的痛点,可以将session
信息放入到缓存或者是数据库中进行存储,服务端在校验登录进来的信息时直接从缓存或者数据库中获取对应的信息进行比对,业内一般将Session
信息放入到Redis中进行保存。
SpringSession
1 引入pom
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session</artifactId> <version>1.2.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId> </dependency>
2 application.yaml配置
spring: redis: database: 1 host: localhost pool: max-active: 20
3 开启@EnableRedisHttpSession
SpringSession原理
先从@EnableRedisHttpSession
注解类分析开始
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented @Import({RedisHttpSessionConfiguration.class}) @Configuration public @interface EnableRedisHttpSession { int maxInactiveIntervalInSeconds() default 1800; String redisNamespace() default "spring:session"; RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE; String cleanupCron() default "0 * * * * *"; }
可以看到该注解用到了Spring
的Import
来加载需要的bean
在RedisHttpSessionConfiguration
配置类中主要的功能有以下:
@Bean public RedisOperationsSessionRepository sessionRepository() { RedisTemplate<Object, Object> redisTemplate = this.createRedisTemplate(); RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(redisTemplate); sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher); if (this.defaultRedisSerializer != null) { sessionRepository.setDefaultSerializer(this.defaultRedisSerializer); } sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds); if (StringUtils.hasText(this.redisNamespace)) { sessionRepository.setRedisKeyNamespace(this.redisNamespace); } sessionRepository.setRedisFlushMode(this.redisFlushMode); int database = this.resolveDatabase(); sessionRepository.setDatabase(database); return sessionRepository; } @Bean public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) { SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository); sessionRepositoryFilter.setServletContext(this.servletContext); sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver); return sessionRepositoryFilter; }
创建一个RedisOperationsSessionRepository
对象提供给SessionRepositoryFilter
操作session的工具。
核心就在SessionRepositoryFilter
过滤器中:
@Order(-2147483598) public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter { // 省略不必要的代码 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository); SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext); SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response); try { filterChain.doFilter(wrappedRequest, wrappedResponse); } finally { wrappedRequest.commitSession(); } } }
可以看到该过滤器的优先级非常的高,那么请求进入服务器第一个就会来到该过滤器。该过滤器将HttpServletRequest
和HttpServletResponse
进行包装然后开始执行下一个过滤器链路,其实包装的本质也就是将HttpServletRequest
的getSession进行了加强,改为通过RedisOperationsSessionRepository
从redis
中获取
由于Session-Cookie
的诸多不便的缺点,现在大部分公司采用的技术方案是Token
。Token
的机制和Cookie
其实差不多,本质都是用来解决HTTP
协议的无状态痛点。此外Token认证的方式还能解决CSRF
攻击的问题。
一般是将用户id和用户名称和进行双向加密后返回给客户端,客户端再每次请求时都需要再请求头中带上Token
。服务端接收到token
后将token
发往SSO服务进行校验,校验通过后会获得登录用户的权限和基本信息,登录失败后将拒绝访问。
此方式和Session-Cookie
的区别在于客户端的存储方式,Cookie
是直接存储在浏览器中,访问的时候直接带过去,在web场景中这样是可以的,但是一旦涉及到移动端此方式就会有问题,所以对于跨平台的接口还是采用Token
的方式兼容性最好
JWT全名为:json web token。WT是由三段信息构成的,将这三段信息文本用
.
链接一起就构成了Jwt字符串。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT构成:
- 头部
- 载荷
- 签证
头部信息header:
加密的算法
{ 'typ': 'JWT', 'alg': 'HS256' }载荷payload:
{ "sub": "1234567890", "name": "John Doe", "admin": true }载荷中有以下几种数据:
保留数据:
iss(Issuser):代表这个JWT的签发主体; sub(Subject):代表这个JWT的主体,即它的所有人; aud(Audience):代表这个JWT的接收对象; exp(Expiration time):是一个时间戳,代表这个JWT的过期时间; nbf(Not Before):是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的; iat(Issued at):是一个时间戳,代表这个JWT的签发时间; jti(JWT ID):是JWT的唯一标识。私有数据:
登录人员的基本信息
签章
Signature
Jwt
的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用
.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jt的第三部分var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); var signature = HMACSHA256(encodedString, 'secret');最后
JWT
总体=base64UrlEncode(header) + '.' + base64UrlEncode(payload) + '.' + signature;
JWT
和普通的Token
区别在于JWT
的载荷是存放在客户端中的,一般的Token
都是客户端存放一个GUID
通过GUID
去服务端中拿到对应的登录人员信息,JWT
就可以将这一步直接省略掉。这样做的好处的可以减少服务端计算的压力。
缺点:
JWT
的有效性和服务端没有依赖关系,所以服务端也无法使JWT
失效。JWT
无法被续签。解决方案
针对JWT
无法被服务端注销问题:
token
存入数据库中,每次拿到token
后和数据库中的token
进行比对,如果没有找到则说明token
已经失效,但是这样做就散失了JWT
的优势,违背了JWT
无状态原则token
存放到黑名单中,每次认证需要先判断是不是已经被拉黑的token
JWT
的Secret
,不推荐。会将所有之前签发的token
全部失效token
针对JWT
无法续签问题:
Token
快过期就签发一个新的token
给客户端。客户端判断接口是否发送新的token
,如果是则用新token来替换旧的token
有效期设置到半夜,保证大部分用户白天都能登录,适用于安全性要求不高的系统token
机制:accessToken
和refreshToken
,accessToken
有效期较短如半个小时,refreshToken
有效期较长如一天。accessToken
用来获取受限的服务资源,当accessToken
失效时,通过refreshToken
来获取新的accessToken
Third-party application
:第三方应用程序,本文中又称"客户端"(client),比如打开知乎,使用第三方登录,选择qq登录,这时候知乎就是客户端。HTTP service
:HTTP服务提供商,本文中简称"服务提供商",即上例的qq。Resource Owner
:资源所有者,本文中又称"用户"(user),即登录用户。User Agent
:用户代理,本文中就是指浏览器。Authorization server
:认证服务器,即服务提供商专门用来处理认证的服务器。一般流行的使用都是OAuth2的授权码模式,授权码模式功能最完整,流程最严密。
/oauth/token?response_type=code&client_id=test&redirect_uri=重定向页面链接
accessToken
和refreshToken
,refreshToken
拥有较长的过期时间,但accessToken
失效后,通过refreshToken
进行刷新。1.3 - What ANSI RBAC is — Apache Directory
权限系统通常基于
RBAC
(Role-Based Access Control)的思想设计,拥有4个关键元素:用户 – 角色 – 权限 – 资源。
- 资源
被安全管理的对象(
Resources
页面、菜单、按钮、订单等)
- 权限
访问和操作资源的许可(
Permit
删除、编辑、审批等)
- 角色
我们通过业务流程确定一个角色,实际是确定角色并角色具备的那些权限的过程,角色所以是权限的集合,是众多权限颗粒组成;
我们通过把权限给这个角色,再把角色给用户,从而实现用户的权限,因此它承担了一个桥梁的作用。
引入角色这个概念,可以帮助我们灵活的扩展,使一个用户可以具备多种角色。
- 用户
系统实际的操作员(
User
)【用户(
user
:谁)】被赋予【角色(role
:具有1-n个权限)】,通过角色关联的【权限(permit
:许可)】去访问/操作【资源(resource
)】
RBAC
解决了什么痛点?
在传统模型中无角色概念,直接对用户进行授权,这样会导致一些问题:
在RBAC模型中一共分为四种:RBAC0
,RBAC1
,RBAC2
,RBAC3
,其中,RBAC1
和RBAC2
是基于RBCA0
升级衍生出来的,而RBAC3
则是融合了RBAC1
和RBAC2的优点
创造的新的模型,一般来说RBAC3
是够用的。
RBAC0定义了能构成RBAC控制系统的最小的元素集合,RBAC0由四部分构成:
当一个用户被指定角色时,该用户就拥有了此角色的所有权限。RBAC0是最基础的权限模型,权限设计时将权限赋予给角色,而不是用户,一个用户可以拥有若干角色,从而使用户可以获得更广泛的权限。就此构造成“用户- 角色- 权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。角色和权限绑定,用户被赋予相应的角色,通过多对多的关系来实现授权和授权的快速变更,从而控制用户对系统的功能使用和数据访问权限。
RBAC1在RBAC0的基础上增加权限继承的改进,为什么要加入权限继承呢?
设想一下这个场景,公司招了一位管理人员进入,那么管理人员的权限就得是下面被管理人员的权限的超集,如果被管理人员的角色和链路很长的话,那么这个角色需要添加很多的权限,非常的麻烦。但是引入了权限继承体系就会变得很简单,管理人员的角色只需要继承所有他下面一级角色就可以了。
角色间的继承关系可分为一般继承(General
)和受限继承(Limited
)。
要求角色继承关系是一个绝对偏序关系(A角色继承于B角色,那么B必须是A权限的一个子集,不可以冗余多余的权限),允许角色间的多继承。
进一步要求角色继承关系是一个树结构,实现角色间的单继承。受限继承则增强了职责关系的分离
他是RBAC
的约束模型,RBAC3
也是在RBAC0
的基础上构建的,在RBAC0
的基础上增加了约束的概念,主要引入了静态职责分离SSD(Static Separation of Duty)和动态职责分离DSD(Dynamic Separation of Duty)。
在设置用户角色权限的时候就应该判断,如果有冲突发生在设置时候就应该拒绝。静态职责分离有以下几种
在角色分配时可以将冲突的角色赋予给同一个用户,但是在用户使用系统时,一次会话中不能同时激活两个角色。
也就是最全面级的权限管理,它是基于RBAC0的基础上,将RBAC1和RBAC2进行整合了,最前面,也最复杂的
RBAC
最简单的表结构设计如下:
一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系
当用户量非常多的时候,需要用系统逐一给用户授权是一件很繁琐的事情,就需要使用用户组,除了可以给用户授权意外还可以给用户组进行授权,然后将用户加入到对应的用户组中,这样用户拥有的权限就等于用户组的权限+用户个人角色的权限之和。
角色组不参与权限控制,当角色量较多时,可以通过树状图来展示角色,这样方便用户去使用。
接下来可以再进一步对权限进行拆分,如对于上传文件的操作,菜单的访问,页面上的按钮都属于权限控制的范围,在设计上将功能操作分为一类,将文件,菜单,页面内容分成资源一类,这样可以把操作和资源进行管理,粒度更细。
全局:
如果需要对权限要求更高还可以变成RBAC3
型的权限设计,即加上权限继承和权限约束。采用受限继承式的方式完成:
只需要在角色表中添加一个父角色ID就可以,当用户登录进入系统时,会通过用户ID找到对应的角色集合,对集合进行一次遍历:递归找到该角色对应父类的权限并加入集合中。再对所有角色进行遍历完成后会得到一个用户所有的权限。这样受限继承式的权限继承就完成了。
如果需要使用普通继承式,则需要再新建一个表:
这样的处理会简单一点,当获取到用户的角色集合时,遍历获取每个角色对应的父角色ID对应的权限,然后放入到权限集合中。
ACL称之为权限控制列表,规定资源可以被哪些主体进行哪些操作,是ACL模型的一种灵活实现
场景:部门隔离 适用资源:客户页面、人事页面
在ACL
权限模型下,权限管理是围绕资源来设定的。我们可以对不同部门的页面设定可以访问的用户。配置形式如下:
ACL配置表 资源: 客户页面 主体: 销售部(组) 操作:增删改查 主体: 王总(用户) 操作: 增删改查 资源: 人事页面 主体: 王总(组) 操作: 增删改查
注:主体可以是用户,也可以是组。
在维护性上,一般在粗粒度和相对静态的情况下,比较容易维护。
在细粒度情况下,比如将不同的客户视为不同的资源,1000个客户就需要配置1000张ACL表。如果1000个客户的权限配置是有规律的,那么就要对每种资源做相同的操作;如果权限配置是无规律的,那么ACL不妨也是一种恰当的解决方案。
在动态情况下,权限经常变动,每添加一名员工,都需要配置所有他需要访问的资源,这在频繁变动的大型系统里,也是很难维护的。
DAC
称之为自主访问控制(DAC
: Discretionary Access Control
)
系统会识别用户,然后根据被操作对象(Subject
)的权限控制列表(ACL: Access Control List
)或者权限控制矩阵(ACL: Access Control Matrix
)的信息来决定用户的是否能对其进行哪些操作,例如读取或修改。DAC
是ACL
的一种实现,强调灵活性。纯粹的ACL,权限由中心管理员统一分配,缺乏灵活性。为了加强灵活性,在ACL的基础上,DAC模型将授权的权力下放,允许拥有权限的用户,可以自主地将权限授予其他用户。
而拥有对象权限的用户,又可以将该对象的权限分配给其他用户,所以称之为自主控制。在文件系统的设计中常用此模式,如微软的NTFS。
DAC
的缺点在于权限控制较于分散,不方便去管理,且也是用户直接和权限挂钩。适用于权限结构简单,用户类型和数量不多的场景。
MAC称之为强制访问控制(
MAC: Mandatory Access Control
)是ACL模型的另一种实现,主要体现在了安全性访问权限有两个规则判断
- 规定资源可以被哪些类别的主体进行哪些操作
- 规定主体可以对哪些等级的资源进行哪些操作
1和2同时满足才可以通过权限认证
场景:保密系统 适用资源:机密档案
MAC
是ACL
的另一种实现,强调安全性。MAC
会在系统中,对资源与主体,都划分类别与等级。比如,等级分为:秘密级、机密级、绝密级;类别分为:军事人员、财务人员、行政人员。比如,一份机密级的财务档案,可以确保只有主体的等级是机密级,且是财务人员才能访问。如果是机密级的行政人员就无法访问。MAC
的优势就是实现资源与主体的双重验证,确保资源的交叉隔离,提高安全性。
资源配置表 资源: 财务文档 主体: 财务人员 等级:机密级 操作:查看 主体配置表 主体: 李女士 类别: 财务人员 等级:机密级
基于属性的权限验证(
ABAC: Attribute-Based Access Control
) 规定哪些属性的主体可以对哪些属性的资源在哪些属性的情况下进行哪些操作,
场景:防火墙 适用资源:端口访问
ABAC中主要的一些参数:
设定一个权限,就是定义一条含有四类属性信息的策略(Policy
)。
例如:
策略表 效果:允许 操作:流入 主体:来自上海IP的客户端 资源:所有以33开头的端口(如3306) 情况:在北京时间 9:00~18:00 效果:禁止 操作:流出 主体:任何 资源:任何 情况:任何
一个请求来到系统中,会逐条的匹配策略。匹配的规则有很多种:
接口需要开放给互联网调用需要做好安全控制,不然服务会被恶意攻击拖垮,如何做好OpenAPI,我认为有以下几点需要:
- 安全认证
- 开发者门户,用于开发应用的注册和操作
- 接口熔断降级处理,一般来说通过Hytrix
- 日志记录,在输入和输出要进行日志记录
首先客户端调用开放接口之前需要去开发者门户注册一个应用账户,应用会得到自身的AppKey
和AppSecret
,其中AppKey
是相当于用户名,AppSecret
相当于密码。OpenApi调用的过程如下:
HTTPS
协议登录获取Access_Token
(以下称之为Token
)和Refresh_Token
Token
认证指客户端请求黑名单接口时,认证中心基于Token
生成签名Token表结构:
接口请求:
请求参数中至少有以下几个属性:
服务端签名验证的具体流程: