Spring Security、Oauth2与JWT

Spring Security、Oauth2与JWT

安全框架基础

认证、会话与授权

用户身份认证:用户身份认证即用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问。常见的用户身份认证表现形式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。

会话:用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于时session方式、基于token方式等。基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持cookie;基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。

用户授权:用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫用户授权。认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,控制不同的用户能够访问不同的资源。

单点登录

分布式系统要实现单点登录,通常将认证系统独立抽取出来,并且将用户身份信息存储在单独的存储介质,比如: MySQL、Redis,考虑性能要求,通常存储在Redis中,如下图:

1595747774885

Java中有很多用户认证的框架都可以实现单点登录,如Apache Shiro、CAS、Spring security CAS等。单点登录的特点是:

1、认证系统为独立的系统。
2、各子系统通过Http或其它协议与认证系统通信,完成用户认证。 
3、用户身份信息存储在Redis集群。

分布式认证方案

Session认证

在分布式的环境下,基于session的认证会出现一个问题,每个应用服务都需要在session中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用服务需要将session信息带过去,否则会重新认证。

image-20210306130122129

这个时候,通常的做法有下面几种:

Session复制:多台应用服务器之间同步session,使session保持一致,对外透明。
Session黏贴:当用户访问集群中某台服务器后,强制指定后续所有请求均落到此机器上。
Session集中存储:将Session存入分布式缓存中,所有服务器应用实例统一从分布式缓存中存取Session。

总体来讲,基于session认证的认证方式,可以更好的在服务端对会话进行控制,且安全性较高。但是,session机制方式基于cookie,在复杂多样的移动客户端上不能有效的使用,并且无法跨域,另外随着系统的扩展需提高session的复制、黏贴及存储的容错性。

Token认证

基于token的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把token存在任意地方,并且可以实现web和app统一认证机制。其缺点也很明显,token由于自包含信息,因此一般数据量较大,而且每次请求都需要传递,因此比较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理负担。

image-20210306130559315

Oauth2

Oauth2简介

分布式系统的每个服务(系统)都会有认证、授权的需求,如果每个服务都实现一套认证授权逻辑会非常冗余,考虑分布式系统共享性的特点,需要由独立的认证服务来处理系统认证授权的请求第三方认证技术方案,最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的接口协议。Oauth协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用Oauth认证服务,任何服务提供商都可以实现自身的Oauth认证服务,因而Oauth是开放的。业界提供了Oauth的多种实现如PHP、JavaScript、Java、Ruby等各种语言开发包,大大节约了程序员的时间,因而Oauth是简易的。互联网很多服务如Open API,很多大公司如Google、Yahoo、Microsoft等都提供了Oauth认证服务,这些都足以说明Oauth标准逐渐成为开放资源授权的标准。Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。 例子:

1595748981566

Spring security+Oauth2认证解决方案:

1595749301734

执行流程:

1、用户请求认证服务完成认证。 
2、认证服务下发用户身份令牌,拥有身份令牌表示身份合法。 
3、用户携带令牌请求资源服务,请求资源服务必先经过网关。
4、网关校验用户身份令牌的合法,不合法表示用户没有登录,如果合法则放行继续访问。 
5、资源服务获取令牌,根据令牌完成授权。
6、资源服务完成授权则响应资源信息。 

Oauth2模式简介

Oauth2有以下授权模式: 授权码模式(Authorization Code)、隐式授权模式/简化模式(Implicit)、密码模式(Resource Owner Password Credentials)、客户端模式(Client Credentials),其中授权码模式和密码模式应用较多。各模式优缺点对比:

1)授权码模式是四种模式中最安全的一种模式。一般用于Web服务器端应用或第三方的原生App调用资源服务的时候。因为在这种模式中access_token不会经过浏览器或移动端的App,而是直接从服务端去交换,这样就最大限度的减小了令牌泄漏的风险。
2)密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用。
3)客户端模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任,而client本身也是安全的。因此这种模式一般用来提供给我们完全信任的服务器端服务。比如,合作方系统对接,拉取一组用户信息。客户端模式适应于没有用户参与的,完全信任的一方或合作方服务器端程序接入。
4)一般来说,简化模式用于第三方单页面应用。

Oauth2授权码模式

image-20210306140440315

1、资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器重定向到授权服务器,重定向时会附加客户端的身份信息,如浏览器输入:

http://localhost:40400/auth/oauth/authorize?client_id=XcWebApp&response_type=code&scop=app&redirect_uri=http://www.xuecheng.com

http://localhost:8005/oauth/authorize?client_id=XcWebApp&response_type=code&scop=app&redirect_uri=http://www.baidu.com

参数解析如下:
1)oauth/authorize之前的路径是服务名
2)client_id:客户端id,和授权配置类中设置的客户端id一致。
3)response_type:授权码模式固定为code。
4)scop:客户端范围,和授权配置类中设置的scop一致。
5)redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。

2、浏览器出现向授权服务器授权页面,之后让用户同意授权。

3、授权服务器将授权码(AuthorizationCode)转经浏览器发送给client(通过redirect_uri)。

4、客户端拿着授权码向授权服务器索要access_token。

申请授权码过程

1)输入设置好的账号和密码

1595753049990

2)点击授权

1595753079081

3)页面跳转完成,后面跟着的是授权码

1595753112844

申请令牌过程

1)postman发送post请求测试:

http://localhost:40400/auth/oauth/token

参数如下:
grant_type:授权类型,填写authorization_code,表示授权码模式 
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。 

1595753143407

此请求需要使用http Basic认证。 http Basic认证是http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用base64编码,放在header中请求服务端,格式如下:

Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=用户名:密码的base64编码

认证失败服务端会返回401 Unauthorized。

1595753289010

2)请求成功,返回令牌:

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb21wYW55SWQiOm51bGwsInVzZXJwaWMiOm51bGwsInVzZXJfbmFtZSI6IlhjV2ViQXBwIiwic2NvcGUiOlsiYXBwIl0sIm5hbWUiOm51bGwsInV0eXBlIjpudWxsLCJpZCI6bnVsbCwiZXhwIjoxNTk1Nzk2NTQ1LCJqdGkiOiI3ZDMyZGNiMy04YTFmLTQ3MzgtYjdhNi00MTc2MTZiMjkzNDciLCJjbGllbnRfaWQiOiJYY1dlYkFwcCJ9.ACybd2NUa1m05N7D2Y6PxcF7qEYYGtHqDHznQGug7d2oO0FQQ3BhnxcWVcIBZp0bKrX0lsPgLHvWiYcD94mAp8qripvu9cLnVkKfgeJPtg0Gu49UpzuxWeqMjn5m4Lq5SVcPPodrhTqdzDwQZvZeorz8RHZa5AFETjp1evz3m96UT3VN5-M8QF0UFSPfuHhavx_tEp47rSAyTp7-C9GCudRDlE3Y8EQm-hc6pt7YChQDG3sGa6B_1-fQdD4LoX6_Gkdp30AQdddHfRFyp9zWhT7za5zeHEYO9sUK8H-zftYfLpLI9CCloxllnvW5JqPAbzNzn7K74WoTfU_BucAjaw",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb21wYW55SWQiOm51bGwsInVzZXJwaWMiOm51bGwsInVzZXJfbmFtZSI6IlhjV2ViQXBwIiwic2NvcGUiOlsiYXBwIl0sImF0aSI6IjdkMzJkY2IzLThhMWYtNDczOC1iN2E2LTQxNzYxNmIyOTM0NyIsIm5hbWUiOm51bGwsInV0eXBlIjpudWxsLCJpZCI6bnVsbCwiZXhwIjoxNTk1Nzk2NTQ1LCJqdGkiOiJmMmNiODE1My03YTQzLTQxNmEtOWZhNy04NzYyNjgzNGM4NGEiLCJjbGllbnRfaWQiOiJYY1dlYkFwcCJ9.MUgbC3SJuM22zlJgqgOillOmx0ZC4w1S3qR_3Q4fjiVnox-u9btGOlS0fBXIkKlRK__Pz6wd3sW_lkhLyxbtwFrXCazRMseEz-48BkCWR_XXSG9S0E4NAcy4y605CO5SELmNUQ6mpdXysXCRhXgjAmgoVP-AZg36-ZgUAq9NhSFpa4bBxkZvTdexvwDKibE3jbPyGksT8AvFd233NjvCKDv-sRo1hr8wjWyXRRXPoHFBDUvKpKHqPTHeODEfWA5ePHCtjbwqZP7xxlotUO5oOGah1Q4YkYhxZEAR44Bog-1ilhyd4_NmxxP3DaahXw3UCuSESgzILAlT3bHEuYA0-w",
    "expires_in": 43199,
    "scope": "app",
    "jti": "7d32dcb3-8a1f-4738-b7a6-417616b29347"
}

返回解析:
1)access_token:访问令牌,携带此令牌访问资源。
2)token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用Bearer Token。 
3)refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。 
4)expires_in:过期时间,单位为秒。
5)scope:范围,与定义的客户端范围一致。 

3)测试授权码模式

配置课程微服务权限xc-service-manage-course,没有授权码无法访问接口

http://localhost:31200/course/teachplan/list/297e7c7c62b888f00162b8a7dec20000

1595755600821

postman测试,在http header中添加Authorization>>>Bearer+空格+令牌,可以成功访问:

1595755844237

Oauth2密码模式

image-20210306141307285

1、资源拥有者将用户名、密码发送给客户端

2、客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token),如postman发送post请求测试:

http://localhost:40400/auth/oauth/token

参数如下:
grant_type:密码模式授权填写password
username:用户账号 
password:用户密码 
并且此请求需要使用http Basic认证

1595757281705

Oauth2客户端模式

image-20201213115313783

1、客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)

2、确认客户端身份无误后,将令牌(access_token)发送给client,请求如下:

http://localhost:40400/auth/oauth/token

参数如下:
client_id:客户端准入标识,XcWebApp。
client_secret:客户端秘钥,XcWebApp。
grant_type:授权类型,填写client_credentials表示客户端模式。

Oauth2简化模式

image-20201213120329483

1、资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息。如:

http://localhost:8005/oauth/authorize?client_id=XcWebApp&response_type=token&scop=app&redirect_uri=http://www.baidu.com

参数描述同授权码模式,注意response_type=token,说明是简化模式。

2、浏览器出现向授权服务器授权页面,之后将用户同意授权。

3、授权服务器将授权码、令牌(access_token)以Hash的形式存放在重定向uri的fargment中发送给浏览器。注:fragment主要是用来标识URI所标识资源里的某个资源,在URI的末尾通过(#)作为fragment的开头,其中#不属于fragment的值。如https://domain/index#L18这个URI中L18就是fragment的值。只需要知道JS通过响应浏览器地址栏变化的方式能获取到fragment就行了。

Oauth2效验令牌

Get请求:http://localhost:40400/auth/oauth/check_token?token=令牌

返回参数解析:
1)exp:过期时间,long类型,距离1970年的秒数(new Date().getTime()可得到当前时间距离1970年的毫秒数)。 
2)user_name:用户名
3)client_id:客户端Id,在oauth_client_details中配置 
4)scope:客户端范围,在oauth_client_details表中配置 
5)jti:与令牌对应的唯一标识
6)companyId、userpic、name、utype、id:这些字段是本认证服务在Spring Security基础上扩展的用户身份信息

1595757669276

Oauth2刷新令牌

刷新令牌是当令牌快过期时重新生成一个令牌,它于授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码,也不需要账号和密码,只需要一个刷新令牌、客户端id和客户端密码。刷新令牌成功,会重新生成新的访问令牌和刷新令牌,令牌的有效期也比旧令牌长。刷新令牌通常是在令牌快过期时进行刷新。

Post请求:http://localhost:40400/auth/oauth/token

请求参数:
grant_type:固定为refresh_token
refresh_token:刷新令牌(注意不是access_token,而是上次生成的refresh_token)

1595757985420

JWT

JWT简介

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递Json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。

JWT令牌的优点:
1、JWT基于json,非常方便解析。
2、可以在令牌中自定义丰富的内容,易扩展。 
3、通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。 
4、资源服务使用JWT可不依赖认证服务即可完成授权。

JWT令牌的缺点:JWT令牌较长,占存储空间比较大。

传统效验令牌流程:

1595758140036

JWT效验令牌流程:

1595758264950

JWT令牌结构

image-20210306142718282

JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz。

1)Header:头部包括令牌的类型(即JWT)和使用的哈希算法(如HMAC SHA256或RSA),如:

{
    "alg": "HS256",
    "typ": "JWT"
}

将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。

2)Payload :第二部分是负载内容,也是一个Json对象,它是存放有效信息的地方,它可以存放JWT提供的现成字段,比如:iss(签发者)、exp(过期时间戳)、sub(面向的用户)等,也可自定义字段。 此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。例:

{
    "sub": "1234567890",
    "name": "456",
    "admin": true
}

最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。

3)Signature:第三部分是签名,此部分用于防止JWT内容被篡改。这个部分使用Base64Url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中签名算法进行签名。例:

HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)

base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:签名所使用的密钥。 

生成公钥和私钥

私钥用于生成JWT,公钥用于效验JWT。下边命令生成密钥证书,采用RSA算法,每个证书包含公钥和私钥,随便找个文件夹cmd执行命令:

生成秘钥证书:
keytool -genkeypair -alias xckey -keyalg RSA -keypass xuecheng -keystore xc.keystore -storepass xuechengkeystore

可能读取不到文件,换一种格式生成:
keytool -genkeypair -alias whalechen -keyalg RSA -keypass whalechen -keystore whale.jks -storepass whalechen

keytool是一个java提供的证书管理工具 
-alias:密钥的别名
-keyalg:使用的hash算法 
-keypass:密钥的访问密码
-keystore:密钥库文件名,xc.keystore保存了生成的证书
-storepass:密钥库的访问密码

查询证书信息:
keytool -list -keystore xc.keystore

删除别名:
keytool -delete -alias xckey -keystore xc.keystore

1595760873359

导出公钥信息,openssl是一个加解密工具包,这里使用openssl来导出公钥信息。安装openssl:http://slproweb.com/products/Win32OpenSSL.html,将openssl的bin目录配置到path环境变量下,cmd进入xc.keystore文件所在目录执行如下命令:

keytool -list -rfc --keystore xc.keystore | openssl x509 -inform pem -pubkey

keytool -list -rfc --keystore whale.jks | openssl x509 -inform pem -pubkey

1595764690714

将上边的公钥拷贝到文本文件中(圈起来的所有内容),合并为一行,命名为publickey.txt,即获得公钥。

用户登录认证流程(JWT)

1596025784729

1、用户登录,请求认证服务
2、认证服务认证通过,生成JWT令牌,将JWT令牌及相关信息写入Redis,并且将身份令牌写入cookie
3、用户访问资源页面,带着cookie到网关
4、网关从cookie获取token,并查询Redis校验token,如果token不存在则拒绝访问,否则放行
5、用户退出,请求认证服务,清除redis中的token,并且删除cookie中的token

使用Redis存储用户的身份令牌有以下作用:

1、实现用户退出注销功能,服务端清除令牌后,即使客户端请求携带token也是无效的。
2、由于JWT令牌过长,不宜存储在cookie中,所以将JWT令牌存储在Redis,由客户端请求服务端获取并在客户端存储。

用户登录认证流程(密码模式)

1596025916252

启动nginx,模拟登录请求:

1596034077976

Spring Security

用户授权流程

1596458507522

1、用户认证通过,认证服务向浏览器cookie写入token(身份令牌)。
2、前端携带token请求用户中心服务获取JWT令牌,前端获取到JWT令牌解析,并存储在sessionStorage。
3、前端携带cookie中的身份令牌及JWT令牌访问资源服务,前端请求资源服务需要携带两个token,一个是cookie中的身份令牌,一个是http header中的jwt,前端请求资源服务前在http header上添加JWT请求资源。
4、网关校验token的合法性,用户请求必须携带身份令牌和jwt令牌,网关校验redis中user_token的有效期,已过期则要求用户重新登录。
5、资源服务校验JWT的合法性并进行授权,资源服务校验JWT令牌,完成授权,拥有权限的方法正常执行,没有权限的方法将拒绝访问。 

方法授权

方法授权要完成的是资源服务根据JWT令牌完成对方法的授权,具体流程如下:

1、生成JWT令牌时在令牌中写入用户所拥有的权限,我们给每个权限起个名字,例如某个用户拥有如下权限:course_find_list(课程查询)、course_pic_list(课程图片查询)。

2、在资源服务方法上添加注解PreAuthorize,并指定此方法所需要的权限。例如下边是课程管理接口方法的授权配置,它就表示要执行这个方法需要拥有course_find_list权限。

@PreAuthorize("hasAuthority('course_find_list')") 
@Override
public QueryResult<CourseInfo> findCourseList(@PathVariable("page") int page,                                               @PathVariable("size") int size,                                               CourseListRequest courseListRequest)

3、当请求有权限的方法时正常访问,当请求没有权限的方法时则拒绝访问。

4、如果方法上不添加授权注解表示此方法不需要权限即可访问。

细粒度授权

细粒度授权也叫数据范围授权,即不同的用户所拥有的操作权限相同,但是能够操作的数据范围是不一样的。例子:用户A和用户B都是教学机构,他们都拥有“我的课程”权限,但是两个用户所查询到的数据是不一样的。 项目中细粒度授权,比如: 我的课程,教学机构只允许查询本教学机构下的课程信息。我的选课,学生只允许查询自己所选课。细粒度授权涉及到不同的业务逻辑,通常在service层实现,根据不同的用户进行校验,根据不同的参数查询不同的数据或操作不同的数据。

微服务之间认证

当微服务访问微服务,此时如果没有携带JWT则微服务会在授权时报错。测试课程预览:

1、将课程管理服务和CMS全部添加授权配置。

2、用户登录教学管理前端,进入课程发布界面,点击课程发布,观察课程管理服务端报错如下:

feign.FeignException: status 401 reading CmsPageClient#save(CmsPage); content:
{"error":"unauthorized","error_description":"Full authentication is required to access this
resource"}

分析原因: 由于课程管理访问CMS时没有携带JWT令牌导致。解决方案: 微服务之间进行调用时需携带JWT,通过Feign拦截器进行认证。注:JWT在用户登录时生成,用户请求方法授权时放在头部,后台微服务从头部获取到JWT,在进行服务之间调用时进行传递,在公共包下创建拦截器进行向下传递头部信息。

posted @ 2023-02-01 11:16  肖德子裕  阅读(1704)  评论(0编辑  收藏  举报