基本概念
认证: 用户认证就是判断一个用户的身份是否合法的过程 ,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。
会话:用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。
授权:授权是用户认证通过后根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。授权可简单理解为who对what(which)进行how操作,
who,即主体( subject), 主体一般是指用户,也可以是程序,需要访问系统中的资源。
what,即资源( resource), 如系统菜单、页面、按钮、代码方法、系统商品信息、系统订单信息等。系统菜单、页面、按钮、代码方法都属于 系统功能资源,对于web系统每个功能资源通常对应一个url ;系统商品信息系统订单信息都属于 实体资源(数据资源), 实体资源由资源类型和资源实例组成,比如商品信息为资源类型,商品编号为001的商品为资源实例。
how ,权限/许可( permission), 规定了用户对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个代码方法的调用权限、编号为001的用户的修改权限等,通过权限可知用户对哪些资源都有哪些操作许可。
- 授权的数据模型
- 资源和权限可以合并一起
- rbac
基于角色的访问控制 if (主体.hasrole()){}
基于资源的访问控制 if(主体.haspermission()){}
spring boot security
对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源,通过filter实现
2.1 结构原理
初始化spring security时,初始化一个类型为org.springframework.security.web.filterchainproxy的过滤器,
外部的请求会经过此类。filterchainproxy是一个代理,真正起作用的是filterchainproxy中securityfilterchain所包含的各个filter,同时这些filter作为bean被spring管理,它们是spring security核心,并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(authenticationmanager)和 决策管理器(accessdecisionmanager)进行处理
spring security功能的实现主要是由一系列过滤器链相互配合完成。
2.2 认证过程
1. 用户提交用户名、密码被securityfilterchain中的 usernamepasswordauthenticationfilter 过滤器获取到,封装为请求authentication,通常情况下是usernamepasswordauthenticationtoken这个实现类。
2. 然后过滤器将authentication提交至认证管理器(authenticationmanager)进行认证
3. 认证成功后, authenticationmanager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除) authentication 实例。
4. securitycontextholder 安全上下文容器将第3步填充了信息的 authentication ,通过
securitycontextholder.getcontext().setauthentication(…)方法,设置到其中。
authenticationmanager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为providermanager。
而spring security支持多种认证方式,因此providermanager维护着一个list<authenticationprovider> 列表,存放多种认证方式,最终实际的认证工作是由authenticationprovider完成。不同的认证方式使用不同的authenticationprovider。如使用用户名密码登录时,使用daoauthenticationprovide
daoauthenticationprovider,它的内部又维护着一个userdetailsservice负责userdetails的获取。最终authenticationprovider将userdetails填充至authentication。
2.3 密码处理
spring security提供了很多内置的passwordencoder,能够开箱即用,使用某种passwordencoder只需要进行如
- 下声明即可
常见的密码编码器有:
nooppasswordencoder、bcryptpasswordencoder、pbkdf2passwordencode、scryptpasswordencoder
2.4 授权过程
已认证用户访问受保护的web资源将被securityfilterchain中的 filtersecurityinterceptor 的子类拦截。
- filtersecurityinterceptor会从 securitymetadatasource 的子类defaultfilterinvocationsecuritymetadatasource 获取要访问当前资源所需要的权限collection<configattribute>
- securitymetadatasource其实就是读取“安全拦截机制”
- filtersecurityinterceptor会调用 accessdecisionmanager 进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。
此处会从安全上下文中获取用户:
authentication authentication = securitycontextholder.getcontext().getauthentication();
再进行比对和判断。
- decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
- authentication:要访问资源的访问者的身份
- object:要访问的受保护资源,web请求对应filterinvocation
- confifigattributes:是受保护资源的访问策略,通过securitymetadatasource获取。
spring security内置了三个基于投票的accessdecisionmanager实现类
- affirmativebased 只要有一个赞成票,则表示同意用户访问
- consensusbased:赞成票多余反对票,则表示同意用户访问
- unanimousbased:只要有一个反对票,则表示反对用户访问
spring security为防止csrf(cross-site request forgery跨站请求伪造)的发生,限制了除了get以外的大多数方法,
解决方法1:
屏蔽csrf控制,即spring security不再限制csrf。
httpsecurity.csrf().disable()
解决方法2:
表单添加隐藏域:
<input type="hidden" name="${_csrf.parametername}" value="${_csrf.token}"/>
2.5 实际应用
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security提供会话管理,认证通过后将身份信息放入securitycontextholder上下文,securitycontext与当前线程进行绑定,方便获取用户身份。
authentication authentication = securitycontextholder.getcontext().getauthentication();
2.5.1 会话控制
- spring security会为每个登录成功的用户会新建一个session
httpsecurity.sessionmanagement().sessioncreationpolicy(sessioncreationpolicy.if_required)
- session超时、session失效后,通过spring security 设置跳转的路径。
httpsecurity.sessionmanagement() .expiredurl("/login‐view?error=expired_session") .invalidsessionurl("/login‐view?error=invalid_session");
- session退出
httpsecurity.logout() .logouturl("/logout") .logoutsuccessurl("/login‐view?logout") .logoutsuccesshandler(logoutsuccesshandler) .addlogouthandler(logouthandler) .invalidatehttpsession(true)
定制的 logoutsuccesshandler ,实现用户退出成功时的处理。如果指定了这个选项那么logoutsuccessurl() 的设置被忽略;
添加一个 logouthandler ,用于实现用户退出时的清理工作.默认 securitycontextlogouthandler 会被添加为最后一个 logouthandler;
指定是否在退出时让 httpsession 无效。 默认设置为 true。
2.5.2 web授权
使用 http.authorizerequests() 对web资源进行授权保护
httpsecurity .authorizerequests() .antmatchers("/r/r1").hasauthority("p1") ( .antmatchers("/r/r2").hasauthority("p2") .antmatchers("/r/r3").access("hasauthority('p1') and hasauthority('p2')") .antmatchers("/r/**").authenticated() .anyrequest().permitall() .and() .formlogin()
保护url常用的方法有:
- authenticated() 保护url,需要用户登录
- permitall() 指定url无需保护,一般应用与静态资源文件
- hasrole(string role) 限制单个角色访问,角色将被增加 “role_” .所以”admin” 将和 “role_admin”进行比较.
- hasauthority(string authority) 限制单个权限访问
- hasanyrole(string… roles)允许多个角色访问.
- hasanyauthority(string… authorities) 允许多个权限访问.
- access(string attribute) 该方法使用 spel表达式, 所以可以创建复杂的限制.
- hasipaddress(string ipaddressexpression) 限制ip地址或子网
2.5.3.方法授权
以在任何 @configuration 实例上使用 @enableglobalmethodsecurity 注释来启用基于注解的安全性。
@preauthorize,@postauthorize, @secured三类注解作用域服务层方法上,限制访问的访问
例如:
匿名访问
@secured("is_authenticated_anonymously") @preauthorize("isanonymous()")
方法可匿名访问,底层使用webexpressionvoter投票器
- @preauthorize
@preauthorize("hasauthority('p_transfer') and hasauthority('p_read_account')")
同时拥有p_transfer和p_read_account权限才能访问,底层使用webexpressionvoter投票器
另外,还有注解@postauthorize
分布式系统认证方案
软件的架构由单体结构演变为分布式架构,具有分布式架构的系统叫分布式系统,分布式系统的运行通常依赖网络,它将单体结构的系统分为若干服务,服务之间通过网络交互来完成用户的业务处理,当前流行的微服务架构就是分布式系统架构。
3.1 分布式认证需求
分布式系统的每个服务都会有认证、授权的需求,考虑分布式系统共享性的特点,需要由独立的认证服务处理系统认证授权的请求;考虑分布式系统开放性的特点,不仅对系统内部服务提供认证,对第三方系统也要提供认证。分布式认证的需求总结如下:
统一认证授权
提供独立的认证服务,统一处理认证授权。无论是不同类型的用户,还是不同种类的客户端(web端,h5、app),均采用一致的认证、权限、会话机制,实现统一认证授权。要实现统一则认证方式必须可扩展,支持各种认证需求,比如:用户名密码认证、短信验证码、二维码、人脸识别等认证方式,并可以非常灵活的切换。
应用接入认证
应提供扩展和开放能力,提供安全的系统对接机制,并可开放部分api给接入第三方使用,一方应用(内部系统服务)和三方应用(第三方应用)均采用统一机制接入。
3.2 选型分析
3.2.1 基于session认证
每个应用服务都需要在session中存储用户身份信息,通常的做法有下面几种:
- session复制:多台应用服务器之间同步session,使session保持一致,对外透明。
- session黏贴:当用户访问集群中某台服务器后,强制指定后续所有请求均落到此机器上。
- session集中存储:将session存入分布式缓存中,所有服务器应用实例统一从分布式缓存中存取session。
基于session认证的认证方式,可以更好的在服务端对会话进行控制,且安全性较高。但是,session机制方式基于cookie,在复杂多样的移动客户端上不能有效的使用,并且无法跨域,另外随着系统的扩展需提高session的复制、黏贴及存储的容错性。
3.2.2 基于token认证
基于token的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把token 存在任意地方,并且可以实现web和app统一认证机制。其缺点也很明显,token由于自包含信息,因此一般数据量较大,而且每次请求都需要传递,因此比较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理负担。
3.2.3 技术方案
根据 选型分析,采用token认证,它的优点是:
- 适合统一认证的机制,客户端、一方应用、三方应用都遵循一致的认证机制。
- token认证方式对第三方应用接入更适合,因为它更开放,可使用当前有流行的开放协议oauth2.0、jwt等。
- 一般情况服务端无需存储会话信息,减轻了服务端的压力。
3.3 oauth2.0
oauth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。例如,第三方登录,用户 授权 电商网站 访问 用户存储在微信平台的用户信息
oaauth2.0包括以下角色:
客户端
本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:android客户端、web客户端(浏览器端)、微信客户端等。
资源拥有者
通常为用户,也可以是应用程序,即该资源的拥有者。
资源服务器
存储资源的服务器,例如微信平台用户信息资源。
服务提供商会给准入的接入方一个身份,用于接入时的凭据:
- client_id:客户端标识
- client_secret:客户端秘钥
授权服务器(也称认证服务器)
用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌,作为客户端访问资源服务器的凭据。例如:微信认证服务器。
授权服务器对oauth2.0中的两个角色进行认证授权,分别是资源拥有者、客户端
spring cloud security oauth2
spring-security-oauth2是对oauth2的一种实现,并且跟spring security相辅相成,与springcloud体系的集成也非常便利。其服务实现包括两个服务:
认证授权服务 (authorization server)
应包含对接入端以及登入用户的合法性进行验证并颁发token等功能,对令牌的请求端点由 spring mvc 控制器进行实现,下面是配置一个认证服务必须要实现的endpoints:
- authorizationendpoint 服务于认证请求。默认 url:/oauth/authorize 。
- tokenendpoint 服务于访问令牌的请求。默认 url:/oauth/token 。
资源服务 (resource server),应包含对资源的保护功能,对非法请求进行拦截,对请求中token进行解析鉴权等,下面的过滤器用于实现 oauth 2.0 资源服务:
oauth2authenticationprocessingfilter用来对请求给出的身份令牌解析鉴权。
4.1 授权服务器配置
可以使用
@configuration @enableauthorizationserver
注解并继承authorizationserverconfifigureradapter来配置oauth2.0 授权服务器。
- authorizationserverconfifigureradapter要求配置
- clientdetailsserviceconfifigurer:用来配置客户端详情服务(clientdetailsservice),客户端详情信息在这里进行初始化
- authorizationserverendpointsconfifigurer:用来配置令牌(token)的访问端点和令牌服务(tokenservices)。
- authorizationserversecurityconfifigurer:用来配置令牌访问端点的安全约束
4.1.1 客户端详情
clientdetailsserviceconfifigurer能够使用内存或者jdbc来实现客户端详情,clientdetailsservice负责查找clientdetails,而clientdetails有几个重要的属性
客户端详情(client details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户端详情存储在一个关系数据库的表中,就可以使用 jdbcclientdetailsservice)或者通过实现
clientregistrationservice接口(同时你也可以实现 clientdetailsservice 接口)来管理。
此次客户端详情读取数据库的配置:
4.1.2令牌管理服务
authorizationservertokenservices 接口定义了一些操作可以对令牌进行一些必要的管理,这个接口的实现,则需要继承 defaulttokenservices,里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储,其持久化令牌委托一个 tokenstore 接口来实现,tokenstore 这个接口有一个默认的实现,它就是 inmemorytokenstore。
inmemorytokenstore
:
这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,更易于调试。
jdbctokenstore
:
这是一个基于jdbc的实现版本,令牌会被保存进关系型数据库。
使用这个版本的实现时,你可以在不同的服务器之间共享令牌信息,使用这个版本的时候需要把"spring-jdbc"这个依赖加入到classpath。
- jwttokenstore:
这个版本的全称是 json web token(jwt),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。
另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。
jwttokenstore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 defaulttokenservices 所扮演的角色是一样的。
4.1.3 令牌访问端点
4.1.3.1 认证管理器
authorizationserverendpointsconfifigurer通过设定以下属性决定支持的授权类型(grant types):
authenticationmanager
认证管理器,选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个 authenticationmanager 对象。
userdetailsservice
设置了这个属性,需要有一个 userdetailsservice 接口的实现
authorizationcodeservices
用来设置授权码服务(即 authorizationcodeservices 的实例对象),主要用于 "authorization_code" 授权码类型模式。
implicitgrantservic
这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。
tokengranter
授权将会交由自己来完全掌控,并且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途
4.1.3.2 令牌访问点
框架的默认url访问链接如下列表
- /oauth/authorize:授权端点。
- /oauth/token:令牌端点。
- /oauth/confifirm_access:用户确认授权提交端点。
- /oauth/error:授权服务错误信息端点。
- /oauth/check_token:用于资源服务访问的令牌解析端点。
- /oauth/token_key:提供公有密匙的端点,如果你使用jwt令牌的话。
authorizationserverendpointsconfifigurer 这个配置对象有一个叫做 pathmapping() 的方法用来配置端点url链接,它有两个参数:
- 第一个参数:string 类型的,这个端点url的默认链接。
- 第二个参数:string 类型的,你要进行替代的url链接。
4.1.4 令牌访问端点安全约束
- authorizationserversecurityconfigurer: :用来配置令牌端点(token endpoint)的安全约束
4.1.5 web安全配置
4.1.6 授权类型
4.1.6.1授权码模式
资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息
/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
浏览器出现向授权服务器授权页面,之后用户同意授权
授权服务器将授权码(authorizationcode)经浏览器发送给client
客户端拿着授权码向授权服务器索要访问access_token
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5pgfcd&redirect_uri=http://www.baidu.com
授权服务器返回令牌(access_token)
参数列表如下:
- client_id:客户端准入标识。
- response_type:授权码模式固定为code。
- scope:客户端权限。
- client_secret:客户端秘钥。
- grant_type:授权类型,填写authorization_code,表示授权码模式
- code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
- redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
4.1.6.2 简化模式
资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息
/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
浏览器出现向授权服务器授权页面,之后用户同意授权
授权服务器将授权码将令牌(access_token)以hash的形式存放在重定向uri的fargment中发送给浏览器。fragment 主要是用来标识 uri 所标识资源里的某个资源,在 uri 的末尾通过 (#)作为 fragment 的开头,其中 # 不属于 fragment 的值。
4.1.6.3 密码模式
资源拥有者将用户名、密码发送给客户端
客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token)
/uaa/oauth/token?
client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123
授权服务器将令牌(access_token)发送给client
4.1.6.4 客户端模式
客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)
确认客户端身份无误后,将令牌(access_token)发送给client
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
4.2资源服务器配置
@configuration @enableresourceserver public class resouceserverconfig extends resourceserverconfigureradapter
@enableresourceserver 注解自动增加了一个类型为 oauth2authenticationprocessingfilter 的过滤器链
resourceserversecurityconfifigurer中主要包括:
- tokenservices:resourceservertokenservices 类的实例,用来实现令牌服务。
- tokenstore:tokenstore类的实例,指定令牌如何访问,与tokenservices配置可选
- resourceid:这个资源服务的id,这个属性是可选的,但是推荐设置并在授权服务中进行验证。
其他的拓展属性例如 tokenextractor 令牌提取器用来提取请求中的令牌。
httpsecurity配置这个与spring security类似:
请求匹配器,用来设置需要进行保护的资源路径,默认的情况下是保护资源服务的全部路径。
通过http.authorizerequests()来设置受保护资源的访问规则
其他的自定义权限保护规则通过 httpsecurity 来进行配置。
remotetokenservices 资源服务器通过 http 请求来解码令牌,每次都请求授权服务器端点 /oauth/check_token
此处使用jwt校验令牌
配置安全拦截机制
@enableresourceserver 注解自动增加了一个类型为 oauth2authenticationprocessingfilter 的过滤器链,此过滤器作用,主要是用来解析网关传过来的用户信息字符串,并存入安全上下文中
4.3 网关
网关整合 oauth2.0作为oauth2.0的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当前登录用户信息(jsontoken)给微服务
public class authfilter extends zuulfilter
校验客户端token,验证接入客户端权限,并解析后明文token,转发请求到微服务
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论