文章目录
一、用户认证分析1、认证 与 授权2、单点登录3、第三方账号登录4、第三方认证5、认证技术方案6、Security Oauth 2.0 入门7、 资源服务授权(1)资源服务授权流程(2)公钥私钥授权流程(3)生成私钥公钥 二、JWT 令牌三、认证开发1、认证服务2、工具封装3、代码实现 四、总结代码: /betterGa/ChangGou
一、用户认证分析
上面流程图 描述了用户要操作的各个微服务,用户查看个人信息 需要访问 用户微服务,下单 需要访问 订单微服务,秒杀抢购商品 需要访问 秒杀微服务。每个服务都需要认证用户的身份,身份认证成功后,需要识别用户的角色,然后授权访问对应的功能,比如管理员 和 普通用户的权限对应的功能 是不一样的。
1、认证 与 授权
身份认证
用户身份认证 即 用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问。常见的用户身份认证表现形式有:用户名密码登录。说通俗点,就相当于 校验用户账号密码是否正确。
用户授权
用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫 用户授权。就是上文说的 “管理员 和 普通用户的权限对应的功能 是不一样的“。
2、单点登录
用户访问的项目中,至少有 3 个微服务需要识别用户身份,如果用户访问每个微服务都登录一次就太麻烦了,为了提高用户的体验,我们需要实现 让用户在一个系统中登录,其他任意受信任的系统都可以访问,这个功能就叫 单点登录 。
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。单点登录 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
3、第三方账号登录
随着国内及国外巨头们的平台开放战略以及移动互联网的发展,第三方登录已经不是一个陌生的产品设计概念了。所谓的第三方登录,是说基于用户在第三方平台上已有的账号和密码来快速完成己方应用的登录或者注册的功能。而这里的第三方平台,一般是已经拥有大量用户的平台,国外的比如Facebook,Twitter等,国内的比如微博、微信、QQ等。
第三方登录优点
(1)相比于本地注册,第三方登录一般来说比较方便、快捷,能够显著降低用户的注册和登录成本,方便用户实现快捷登录或注册。
(2)不用费尽心思地应付本地注册对账户名和密码的各种限制,如果不考虑昵称的重复性要求,几乎可以直接一个账号走遍天下,再也不用在大脑或者什么地方记住N多不同的网站或App的账号和密码,整个世界一下子清静了🤣 。
(3)在第一次绑定成功之后,之后用户便可以实现一键登录,使得后续的登录操作比起应用内的登录来容易了很多。
(4)对于某些喜欢社交,并希望将更多自己的生活内容展示给朋友的人来说,第三方登录可以实现把用户在应用内的活动同步到第三方平台上,省去了用户手动发布动态的麻烦。但对于某些比较注重个人隐私的用户来说,则会有一些担忧。
(5)因为降低了用户的注册或登录成本,从而减少由于本地注册的繁琐性而带来的隐形用户流失,最终提高注册转化率。
(6)对于某些应用来说,使用第三方登录完全可以满足自己的需要,因此不必要设计和开发一套自己的账户体系。
(7)通过授权,可以通过在第三方平台上分享用户在应用内的活动在第三方平台上宣传自己,从而增加产品知名度。
(8)通过授权,可以获得该用户在第三方平台上的好友或粉丝等社交信息,从而后续可以针对用户的社交关系网进行有目的性的营销宣传,为产品的市场推广提供另一种渠道。
4、第三方认证
当需要访问第三方系统的资源时,需要首先通过第三方系统的认证(例如:微信认证),由第三方系统对用户认证通过,并授权资源的访问权限。
5、认证技术方案
单点登录技术方案分布式系统要实现单点登录,通常将认证系统独立抽取出来,并且将用户身份信息存储在单独的存储介质,比如: MySQL、Redis,考虑性能要求,通常存储在 Redis 中:
可以看到,用户第一次登录时,需要到认证系统进行用户名和密码的认证,如果用户名和密码可以通过认证,会把 token 令牌给用户,并把令牌作为 key,用户信息作为 value,存储到 Redis 中。以后用户再访问受信任的其他微服务时,都是携带着令牌的,会到认证系统中,访问 Redis 验证令牌是否有效,有效的话可以访问相应微服务的功能。
单点登录的特点是:
(1)认证系统为独立的系统。
(2)各子系统通过 Http 或其它协议与认证系统通信,完成用户认证。
(3)用户身份信息存储在 Redis 集群。
Java 中有很多用户认证的框架都可以实现单点登录:
(1)Apache Shiro
(2)CAS
(3)Spring security CAS
Oauth2 认证
OAuth 是一个开放授权的标准,允许用户授权第三方移动应用,访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用,OAuth2.0 是 OAuth 协议的延续版本。
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 版本已得到广泛应用。RFC 中 Oauth 协议规范 /html/rfc6749 。
分析一个 Oauth2 认证的例子,网站使用微信认证的过程:
可以看到,首先,用户是自己在微信里 信息的资源拥有者;访问登录页面后,开始给网页授权。网页这时会弹出一个二维码页面,资源拥有者 也就是 用户, 扫描二维码,微信会对资源拥有者的身份进行验证, 验证通过后,会询问用户是否给授权网页访问自己的微信数据,用户点击 “确认登录” 表示同意给客户端授权,微信认证服务器会 颁发一个授权码,并重定向到网站。客户端获取到授权码,会携带授权码,向认证服务器申请令牌 ,此过程用户看不到。认证服务器会验证客户端请求的授权码,如果合法,则给客户端颁发令牌,令牌是客户端访问资源的通行证,这样网页就可以访问微信服务器中,用户的基本信息了。 当客户端拿到令牌后,用户在网页中会看到 “已经登录成功”。
Oauth2.0认证流程如下: 引自 Oauth2.0协议 :
rfc6749 /html/rfc6749
可以看到,对照上面的例子,网页就是 “Client”,换言之,是第三方应用, 资源服务器和认证服务器,可以是一个服务,也可以分开的服务,资源服务器返回受保护资源,如果是分开的,资源服务器 通常要请求 认证服务器 来校验令牌的合法性。
Oauth2 包括以下角色:
(1)客户端,本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:畅购在线 Android 客户端、畅购在线 Web 客户端(浏览器端)、微信客户端等。
(2)资源拥有者,通常为用户,也可以是应用程序,即该资源的拥有者。
(3)授权服务器(也称认证服务器),用来对资源拥有的身份进行认证、对访问资源进行授权。客户端要想访问资源需要通过认证服务器由资源拥有者授 权后方可访问。
(4)资源服务器 存储资源的服务器,比如,畅购网用户管理服务器,存储了畅购网的用户信息等。客户端最终访问资源服务器获取资源信息。
Oauth2 在项目的应用
Oauth2 是一个标准的开放的授权协议,应用程序可以根据自己的要求去使用 Oauth2,本项目使用 Oauth2 实现如 下目标:
(1)畅购访问第三方系统的资源
(2)外部系统访问畅购的资源
(3)畅购前端(客户端) 访问畅购微服务的资源。
(4)畅购微服务之间访问资源,例如:微服务 A 访问微服务 B 的资源,B 访问 A 的资源。这里的资源不是指文件、图片,而是访问彼此的方法。
Spring security Oauth2 认证解决方案
本项目采用 Spring security + Oauth2 完成用户认证及用户授权,Spring security 是一个强大的和高度可定制的身份验证和访问控制框架,Spring security 框架集成了 Oauth2 协议,下图是项目认证架构图:
可以看到,需要单独搭建一个认证服务器。
(1)用户请求认证服务完成认证。
(2)认证服务下发用户身份令牌(使用 JWT),拥有身份令牌表示身份合法。
(3)用户请求资源服务必先经过网关,携带令牌请求资源服务。
(4)网关校验用户身份令牌的合法,不合法表示用户没有登录,如果合法,则放行继续访问。
(5)资源服务获取令牌,根据令牌完成授权。
(6)资源服务完成授权,则响应资源信息。
6、Security Oauth 2.0 入门
本项目认证服务基于 Spring Security Oauth2 进行构建,并在其基础上作了一些扩展,采用 JWT 令牌机制,并自定义了用户身份信息的内容。
(1)搭建认证服务器
oauth2.0 服务搭建的详细流程,还是比较麻烦的。需要将 changgou-user-oauth 的工程导入到项目中去。
可以看到,配置的客户端 id 和 密钥都是 “changgou”,因为还没有连接数据库,所以用户的密码先设置为 “jia”。
(2)application.yml 配置
(3)启动授权认证服务
启动之前,记得先启动 eureka,再启动该授权认证工程。
OAuth2 授权模式
(1)授权码模式(Authorization Code)
(2)隐式授权模式(Implicit)
(3)密码模式(Resource Owner Password Credentials)
(4)客户端模式(Client Credentials)
其中,授权码模式 和 密码模式 比较常用。
授权码模式授权流程
之前例举的网页使用微信认证的过程就是授权码模式,流程如下:
(1)客户端请求第三方授权
(2)用户 (资源拥有者) 同意给客户端授权
(3)客户端获取到授权码,请求认证服务器申请令牌
(4)认证服务器向客户端响应令牌
(5)客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权
(6)资源服务器返回受保护资源 授权码模式授权实现
(1)申请授权码
请求认证服务获取授权码:
在浏览器中进行 Get 请求:
http://localhost:9001/oauth/authorize?client_id=changgou&response_type=code&scop=app&redirect_uri=http://localhost
参数列表如下:
① client_id:客户端id,和授权配置类中设置的客户端id一致。
② response_type:授权码模式固定为 code
③ scop:客户端范围,和授权配置类中设置的 scop 一致。
④ redirect_uri:跳转 uri,当授权码申请成功后会跳转到此地址,并在后边带上 code 参数(授权码)
请求后,会跳转到登录页面:
页面需要输入账号 Username 和 密码 Password,注意,这里的 Username 是客户端 ID,Password 是客户端密钥。点击 Sign in 后, Spring Security 接收到请求,会调用 UserDetailsService 接口的 loadUserByUsername 方法查询用户正确的密码。 当前导入的基础工程中,客户端 ID为 changgou,秘钥也为changgou ,输入即可通过认证,进入授权页面:
点击 Authorize,就会返回授权码:
认证服务会携带授权码跳转 redirect_uri, 也就是 localhost,code=1lg3Lj 就是返回的授权码。
(2)申请令牌
拿到授权码后,接下来需要申请令牌。 Post 请求:http://localhost:9001/oauth/token 参数如下:
① grant_type:授权类型,填写 authorization_code,表示授权码模式
② code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
③ redirect_uri:申请授权码时的跳转 url,一定和申请授权码时用的 redirect_uri 一致。
此链接需要使用 http Basic 认证。 http Basic 认证 是 http 协议定义的一种认证方式,将客户端 id 和 客户端密码 按照 “客户端ID:客户端密码” 的格式拼接,并用 base64 编码,放在 header 中请求服务端,比如:Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA=
是用户名:密码
的 base64 编码。如果认证失败,服务端会返回 401 Unauthorized。
使用 postman 完成测试:
客户端 Id 和 客户端密码 会匹配数据库 oauth_client_details 表中的 客户端 id 及 客户端密码。返回信息如下:
① access_token:访问令牌,携带此令牌访问资源
② token_type:有 MAC Token 与 Bearer Token 两种类型,两种的校验算法不同,RFC 6750建议Oauth2 采用 Bearer Token(/#rfc6750)。
③ refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
④ expires_in:过期时间,单位为秒。
⑤ scope:范围,与定义的客户端范围一致。
⑥ jti:当前 token 的唯一标识
(3)令牌校验
Spring Security Oauth2 提供校验令牌的端点,如下:
Get: http://localhost:9001/oauth/check_token?token= [access_token]
将刚刚得到的令牌作为参数,使用 postman 测试如下:
可以看到,令牌过期了,因为设置的是 2 分钟过期,时间比较短。
重新获取令牌,并进行校验:
(4)令牌刷新
令牌刷新 是 当令牌快过期时,重新生成一个令牌,它与 授权码授权 和 密码授权 生成令牌不同,刷新令牌不需要授权码 ,也不需要账号和密码,只需要一个刷新令牌、客户端 id 和 客户端密码。
测试如下: Post:http://localhost:9001/oauth/token
参数:
① grant_type: 固定为 refresh_token
② refresh_token:刷新令牌(注意传的这个参数 不是access_token,而是 refresh_token)
密码授权实现
(1)认证
密码模式(Resource Owner Password Credentials)与 授权码模式 的区别是 申请令牌不再使用授权码,而是直接 通过用户名和密码即可申请令牌。
使用 Post 请求:http://localhost:9001/oauth/token
参数:
① grant_type:密码模式授权填写 password
② username:账号。
③ password:密码
这里的 “账号” 是用户账户,“密码” 是用户密码。并且此链接需要使用 http Basic 认证,也就是说,客户端 ID 和 密钥是必须传递的。
运行结果:
(2)令牌校验
和授权码模式相同, 使用
Get: http://localhost:9001/oauth/check_token?token= [access_token]
可以看到,返回的响应结果中,exp 是过期时间,long类型,表示 距离 1970 年的秒数(new Date().getTime() 可得到当前时间距离1970年的毫秒数);jti 是 与令牌对应的唯一标识 companyId、userpic、name、utype、id,这些字段是 本认证服务 在 Spring Security 基础上扩展的用户身份信息。
(3)令牌刷新
和授权码模式不同的是,刷新令牌不需要授权码,也不需要账号和密码,只需要一个刷新令牌、客户端id 和 客户端密码。
测试如下: Post:http://localhost:9001/oauth/token
刷新令牌成功,会重新生成 新的访问令牌 和 刷新令牌,令牌的有效期也比旧令牌长。刷新令牌通常是在令牌快过期时进行刷新 。
7、 资源服务授权
(1)资源服务授权流程
传统授权流程:
可以看到,客户端先去授权服务器申请令牌,申请令牌后,携带令牌访问资源服务器,资源服务器访问授权服务,校验令牌的合法性,授权服务会返回校验结果,如果校验成功,会返回用户信息给资源服务器,资源服务器如果接收到的校验结果通过了,则返回资源给客户端。
传统授权方法的问题是用户每次请求资源服务,资源服务都需要携带令牌访问认证服务去校验令牌的合法性,并根 据令牌获取用户的相关信息,性能低下。
(2)公钥私钥授权流程
传统的授权模式性能低下,每次都需要请求授权服务校验令牌合法性。可以利用 公钥私钥 🔑 完成对令牌的生成和校验:
可以看到,流程如下:
① 客户端请求认证服务申请令牌
② 认证服务生成令牌认证服务采用非对称加密算法,使用私钥生成令牌。
③ 客户端携带令牌访问资源服务,客户端在 Http header 中添加: Authorization:Bearer 令牌。
④ 资源服务请求认证服务校验令牌的有效性资源服务接收到令牌,使用公钥校验令牌的合法性。
⑤ 令牌有效,资源服务向客户端响应资源信息
(✨注意:加密用公钥,解密用私钥;签名用私钥,验证用公钥。)
(3)生成私钥公钥
Spring Security 提供对 JWT 的支持,使用 Spring Security 提供的 JwtHelper 来创建 JWT 令牌,校验 JWT 令牌 等操作。 这里 JWT 令牌,我们采用 非对称算法 进行加密,所以我们要先生成 公钥 和 私钥。
① 生成密钥证书,以下命令采用 RSA 算法,生成密钥证书,每个证书包含 公钥 和 私钥
创建一个文件夹,在该文件夹下执行如下命令行:
keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou
Keytool 是一个 Java 提供的证书管理工具
alias:密钥的别名keyalg:使用的 hash 算法keypass:密钥的访问密码keystore:密钥库文件名,xc.keystore 保存了生成的证书storepass:密钥库的访问密码
然后就会在文件夹下生成一个证书 changgou.jks ,需要把它拷到工程中。
② 查询证书信息
keytool -list -keystore changgou.jks
③ 删除别名
keytool -delete -alias changgou -keystore changgou.jsk
(4) 导出公钥
openssl 是一个加解密工具包,这里使用 openssl 来导出公钥信息。
安装 openssl:/products/Win32OpenSSL.html
安装 Win64OpenSSL-1_1_0g.exe
配置 openssl 的 path 环境变量:
cmd 进入 changgou.jks 文件所在目录执行如下命令:
keytool -list -rfc --keystore changgou.jks | openssl x509 -inform pem -pubkey
将上边的公钥拷贝到文本 public.key 文件中,合并为一行,将它放到需要实现授权认证的工程中:
二、JWT 令牌
(1)创建令牌数据
在 changgou-user-oauth 工程中创建测试类 com.changgou.token.CreateJwtTest,使用它来创建令牌信息,代码如下:
/*** 令牌的创建和解析*/public class CreateJWTTest {/*** 创建令牌,用私钥加盐,加密算法是非对称加密*/@Testpublic void testCreateToken(){// 加载证书ClassPathResource resource=new ClassPathResource("changgou.jks");// 读取证书数据KeyStoreKeyFactory keyStoreKeyFactory=new KeyStoreKeyFactory(resource,"changgou".toCharArray());// 获取一对密钥KeyPair keyPair = keyStoreKeyFactory.getKeyPair("changgou","changgou".toCharArray());// 获取私钥RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate();// 创建令牌,使用私钥加盐,非对称加密Map<String,Object> payload=new HashMap<>();payload.put("key1","value1");payload.put("key2","value2");payload.put("key3","value3");Jwt jwt = JwtHelper.encode(JSON.toJSONString(payload), new RsaSigner(aPrivate));// 获取令牌String token=jwt.getEncoded();System.out.println(token);}}
运行结果:
(2)解析令牌
创建令牌后,可以对 JWT 令牌进行解析,这里解析需要用到公钥,我们可以将之前生成的公钥 public.key 拷贝出来,用字符串变量 token 存储,然后通过公钥进行验证。
在 changgou-user-oauth 创建测试类 com.changgou.token.ParseJwtTest 实现解析校验令牌数据,代码如下:
public class ParseJwtTest {/**** 解析令牌*/@Testpublic void testParseToken(){//令牌String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkxIjoidmFsdWUxIiwia2V5MiI6InZhbHVlMiIsImtleTMiOiJ2YWx1ZTMifQ.M2-nMoL6drCxN2D-4bD7tUzBZK7FQwEVQ_677fmRUVfe7AOX4fUErfvBvZ3TXkKIvflWgE1ESnxJ0GYKaLre4qLn8JMyd0f6z1TGZ_0RQS5PJER8DcMxELtGtfllAjOa2wlM1Ui9pfB1IghlnRZzwsUma3sVwasOy9iIa4fiG8nI7MUDqaZSzWReO7IGKNvvIXvnW7NMKeFUolJDd84SNR-mLoJfaIH7Temcch0Xhk9ci01ly41eeOyJOvkzVlJQTNojwZGp3yL8QGA_hkxjw7LM3jViaJA0v-7NHVkkcEhvPsRHH2P4Xo3i2wfLcFonOsvnXhMTnr74_7j83gFh0w";//公钥String publickey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhCDQMSUvJ+5YcpTCPijbPWhuot8tOm2IPSZa96Mnr561Y6v4ckJejf58GneFrdn6eBaYXaoo4Cyge9LYzevHpPijBO4cnCt3dCBf2k1c/7adbhwfwVAE9sUsYSFbgOq4mobYXIMwcbdNeO0+Z+AVhUhv+nEBS+fUNkdV55WwvRDKG3pnuNnyMMBDj0XclJjDOfz2NNGignsVIiefPXhE0OdkAL6vIX89U9G5wUUbL87aPOCrvqEpF4jJKyDPQa1bRVATOo8EFWSmkhiAzTQwATvq716ZTTzpjrXJRQ/m4jNuSl0OT0rIDfjxjlfg1shQTXBCW/kHxmaCZ1BQrBQqywIDAQAB-----END PUBLIC KEY-----";//校验JwtJwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));//获取Jwt原始内容 载荷String claims = jwt.getClaims();System.out.println(claims);//jwt令牌String encoded = jwt.getEncoded();System.out.println(encoded);}}
运行结果:
三、认证开发
用户登录的流程图如下:
执行流程:
(1)用户登录,请求认证服务
(2)认证服务认证通过,生成 jwt 令牌,将 jwt 令牌及相关信息写入 cookie
(3)用户访问资源页面,带着 cookie 到网关
(4)网关从 cookie 获取 token,如果存在 token,则校验 token 合法性,如果不合法则拒绝访问,否则放行
(5)用户退出,请求认证服务,删除 cookie 中的 token
1、认证服务
认证服务需要实现的功能如下:
UserDetails 是用户信息的载体,其实就是令牌的载荷。
2、工具封装
在 changgou-user-oauth 工程中添加如下工具对象,方便操作令牌信息。
(1)创建com.changgou.oauth.util.AuthToken 类,存储用户令牌数据,代码如下:
public class AuthToken implements Serializable{//令牌信息String accessToken;//刷新token(refresh_token)String refreshToken;//jwt短令牌String jti;//...get...set}创建com.changgou.oauth.util.CookieUtil类,操作Cookie,代码如下:public class CookieUtil {/*** 设置cookie** @param response* @param namecookie名字* @param value cookie值* @param maxAge cookie生命周期 以秒为单位*/public static void addCookie(HttpServletResponse response, String domain, String path, String name,String value, int maxAge, boolean httpOnly) {Cookie cookie = new Cookie(name, value);cookie.setDomain(domain);cookie.setPath(path);cookie.setMaxAge(maxAge);cookie.setHttpOnly(httpOnly);response.addCookie(cookie);}/*** 根据cookie名称读取cookie* @param request* @return map<cookieName,cookieValue>*/public static Map<String,String> readCookie(HttpServletRequest request, String ... cookieNames) {Map<String,String> cookieMap = new HashMap<String,String>();Cookie[] cookies = request.getCookies();if (cookies != null) {for (Cookie cookie : cookies) {String cookieName = cookie.getName();String cookieValue = cookie.getValue();for(int i=0;i<cookieNames.length;i++){if(cookieNames[i].equals(cookieName)){cookieMap.put(cookieName,cookieValue);}}}}return cookieMap;}}
(2)创建 com.changgou.oauth.util.UserJwt 类,封装 SpringSecurity 中 User 信息以及用户自身基本信息,之后放到令牌的载荷中:
public class UserJwt extends User {private String id; //用户IDprivate String name; //用户名字private String company; //公司private String address; //地址//...get...set}
3、代码实现
可以看到,我们现在实现一个认证流程,用户从页面输入账号密码,到认证服务的 Controller层,Controller 层调用 Service层,Service 层调用 OAuth2.0 的认证地址,进行密码授权认证操作,如果账号密码正确了,就返回令牌信息给 Service 层,Service 将令牌信息给 Controller 层,Controller 层将数据存入到 Cookie 中,再响应用户。使用密码授权,和之前用 postman 测试密码授权模式的实现 过程是相似的, 不同的是,因为用户是不知道 客户端 id 和 密码的,所以用户只需要输入 账号、密码,授权方式 grant_type=password、客户端 id 和密码(通过 BASE64 编码 )都需要程序给。
先提供登录的 Service 接口:
public interface LoginService {/*** 模拟用户的行为 发送请求 申请令牌 返回*/AuthToken login(String username, String password, String clientId, String clientSecret, String grandType);}
实现:
@Servicepublic class LoginServiceImpl implements LoginService {@Autowiredprivate RestTemplate restTemplate;@Autowiredprivate LoadBalancerClient loadBalancerClient;@Overridepublic AuthToken login(String username, String password, String clientId, String clientSecret, String grandType) {//1.定义url (申请令牌的url)//参数 : 微服务的名称spring.appplication指定的名称ServiceInstance choose = loadBalancerClient.choose("user-auth");String url =choose.getUri().toString()+"/oauth/token";//2.定义头信息 (有client id 和client secr)MultiValueMap<String,String> headers = new LinkedMultiValueMap<>();headers.add("Authorization","Basic "+Base64.getEncoder().encodeToString(new String(clientId+":"+clientSecret).getBytes()));//3. 定义请求体 有授权模式 用户的名称 和密码MultiValueMap<String,String> formData = new LinkedMultiValueMap<>();formData.add("grant_type",grandType);formData.add("username",username);formData.add("password",password);//4.模拟浏览器 发送POST 请求 携带 头 和请求体 到认证服务器/*** 参数1 指定要发送的请求的url* 参数2 指定要发送的请求的方法 PSOT* 参数3 指定请求实体(包含头和请求体数据)*/HttpEntity<MultiValueMap> requestentity = new HttpEntity<MultiValueMap>(formData,headers);ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestentity, Map.class);//5.接收到返回的响应(就是:令牌的信息)Map body = responseEntity.getBody();//封装一次.AuthToken authToken = new AuthToken();//访问令牌(jwt)String accessToken = (String) body.get("access_token");//刷新令牌(jwt)String refreshToken = (String) body.get("refresh_token");//jti,作为用户的身份标识String jwtToken= (String) body.get("jti");authToken.setJti(jwtToken);authToken.setAccessToken(accessToken);authToken.setRefreshToken(refreshToken);//6.返回return authToken;}
这里使用 LoadBalancerClient 是为了从微服务的配置中获取 url 等信息,定义请求体,就对应着之前密码授权实现中 请求的 grant_type、username、password 参数;头信息也是选择 Basic Auth 后,生成的 VAULE ,其实就是 ”Basic“ 和 客户端id 、密码 Base64 编码后的字符串 拼接起来的值:
提供登录的 Controller 层:
@RestController@RequestMapping("/user")public class UserLoginController {@Autowiredprivate LoginService loginService;@Value("${auth.clientId}")private String clientId;@Value("${auth.clientSecret}")private String clientSecret;private static final String GRAND_TYPE = "password";//授权模式 密码模式@Value("${auth.cookieDomain}")private String cookieDomain;//Cookie生命周期@Value("${auth.cookieMaxAge}")private int cookieMaxAge;/*** 密码模式 认证.** @param username* @param password* @return*/@RequestMapping("/login")public Result<Map> login(String username, String password) {//登录 之后生成令牌的数据返回AuthToken authToken = loginService.login(username, password, clientId, clientSecret, GRAND_TYPE);//设置到cookie中saveCookie(authToken.getAccessToken());return new Result<>(true, StatusCode.OK,"令牌生成成功",authToken);}private void saveCookie(String token){HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();CookieUtil.addCookie(response,cookieDomain,"/","Authorization",token,cookieMaxAge,false);}}
运行结果:
创建 com.changgou.oauth.service.AuthService 接口,并添加授权认证方法:
public interface AuthService {/**** 授权认证方法*/AuthToken login(String username, String password, String clientId, String clientSecret);}
实现:
@Servicepublic class AuthServiceImpl implements AuthService {@Autowiredprivate LoadBalancerClient loadBalancerClient;@Autowiredprivate RestTemplate restTemplate;/**** 授权认证方法* @param username* @param password* @param clientId* @param clientSecret* @return*/@Overridepublic AuthToken login(String username, String password, String clientId, String clientSecret) {//申请令牌AuthToken authToken = applyToken(username,password,clientId, clientSecret);if(authToken == null){throw new RuntimeException("申请令牌失败");}return authToken;}/***** 认证方法* @param username:用户登录名字* @param password:用户密码* @param clientId:配置文件中的客户端ID* @param clientSecret:配置文件中的秘钥* @return*/private AuthToken applyToken(String username, String password, String clientId, String clientSecret) {//选中认证服务的地址ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");if (serviceInstance == null) {throw new RuntimeException("找不到对应的服务");}//获取令牌的urlString path = serviceInstance.getUri().toString() + "/oauth/token";//定义bodyMultiValueMap<String, String> formData = new LinkedMultiValueMap<>();//授权方式formData.add("grant_type", "password");//账号formData.add("username", username);//密码formData.add("password", password);//定义头MultiValueMap<String, String> header = new LinkedMultiValueMap<>();header.add("Authorization", httpbasic(clientId, clientSecret));//指定 restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {@Overridepublic void handleError(ClientHttpResponse response) throws IOException {//当响应的值为400或401时候也要正常响应,不要抛出异常if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {super.handleError(response);}}});Map map = null;try {//http请求spring security的申请令牌接口ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(path, HttpMethod.POST,new HttpEntity<MultiValueMap<String, String>>(formData, header), Map.class);//获取响应数据map = mapResponseEntity.getBody();} catch (RestClientException e) {throw new RuntimeException(e);}if(map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null) {//jti是jwt令牌的唯一标识作为用户身份令牌throw new RuntimeException("创建令牌失败!");}//将响应数据封装成AuthToken对象AuthToken authToken = new AuthToken();//访问令牌(jwt)String accessToken = (String) map.get("access_token");//刷新令牌(jwt)String refreshToken = (String) map.get("refresh_token");//jti,作为用户的身份标识String jwtToken= (String) map.get("jti");authToken.setJti(jwtToken);authToken.setAccessToken(accessToken);authToken.setRefreshToken(refreshToken);return authToken;}/**** base64编码* @param clientId* @param clientSecret* @return*/private String httpbasic(String clientId,String clientSecret){//将客户端id和客户端密码拼接,按“客户端id:客户端密码”String string = clientId+":"+clientSecret;//进行base64编码byte[] encode = Base64Utils.encode(string.getBytes());return "Basic "+new String(encode);}}
可以看到,实现获取令牌数据,这里认证获取令牌采用的是密码授权模式,用的是RestTemplate 向 OAuth 服务发起认证请求。
提供 AuthController,编写用户登录授权方法:
@RestController@RequestMapping(value = "/userx")public class AuthController {//客户端ID@Value("${auth.clientId}")private String clientId;//秘钥@Value("${auth.clientSecret}")private String clientSecret;//Cookie存储的域名@Value("${auth.cookieDomain}")private String cookieDomain;//Cookie生命周期@Value("${auth.cookieMaxAge}")private int cookieMaxAge;@AutowiredAuthService authService;@PostMapping("/login")public Result login(String username, String password) {if(StringUtils.isEmpty(username)){throw new RuntimeException("用户名不允许为空");}if(StringUtils.isEmpty(password)){throw new RuntimeException("密码不允许为空");}//申请令牌AuthToken authToken = authService.login(username,password,clientId,clientSecret);//用户身份令牌String access_token = authToken.getAccessToken();//将令牌存储到cookiesaveCookie(access_token);return new Result(true, StatusCode.OK,"登录成功!");}/**** 将令牌存储到cookie* @param token*/private void saveCookie(String token){HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();CookieUtil.addCookie(response,cookieDomain,"/","Authorization",token,cookieMaxAge,false);}}
四、总结
(1)认证主要是认证用户身份是否合法,合法的话才能继续访问系统,常见的认证方式,是校验用户的账号、密码是否正确。
(2)授权是系统允许用户访问资源,比如 管理员 和 普通用户访问资源的权限是不一样的。
(3)单点登录是 在多个应用系统中,用户只需要登录一次,就可以访问所有互相信任的应用系统。
分布式系统如果想实现单点登录,需要提供认证系统,用户第一次登录时,到认证系统中进行用户名和密码的认证,通过认证的话,可以把令牌 token 作为 key,用户信息作为 value,存在 Redis 中。之后用户再访问受信任的其他微服务时,都是带着令牌的,认证系统会到 Redis 中校验令牌是否有效,有效则可以访问该服务。就是说,用户想要访问受信任的系统,认证系统就会校验令牌的,通过的话才能访问。
(4)第三方登录是基于用户在第三方平台上已有的账号、密码 来快速完成登录或注册功能。
(5)OAuth 是一个开放授权的标准,允许用户授权第三方移动应用,访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用。
OAuth 有几种授权模式,常用的是 授权码模式、密码模式。
授权码模式举例:
比如,我是用户,我授权某应用访问微信上的信息。在我访问登录页面后,应用会询问我是否授权访问我的微信数据,我确认授权后,微信认证服务器会颁发一个授权码,并重定向到页面,应用就获取到授权码,接下来会携带授权码,向微信认证服务器申请令牌,认证服务器会验证授权码,如果合法,就颁发令牌,这样网页就能访问到我的微信信息了,我也就“登录成功”了。(这里有几个问题:
① 访问服务上的信息有什么用?
比如获取用户信息——昵称和头像,就可以登录;比如获取好友信息,之后可以分享给好友。
② 为什么微信认证服务器不直接发给应用令牌呢?
因为默认浏览器是不安全的,应用用授权码换令牌的过程是访问微信认证服务器,而不是通过浏览器,也就是说,不能让浏览器拿到令牌。
③ 为什么要给个令牌,而不是直接访问资源呢?
因为以后应用可能还要访问资源,之后可以重用这个令牌。)
通过“令牌”,可以实现授权的功能,又保证安全可控,这就是 Oauth 2.0 的优点。
(6)传统的授权中存在一个问题,就是 每次请求资源服务时,都需要携带令牌,认证服务每次都需要校验令牌的合法性,性能低下,本项目采用私钥签名,公钥验证的方式。(公钥是随意发布,大家都可见的;私钥是仅自己可见的。加密的话,一定是希望只有我自己能解密,其他人解密不了,所以是公钥加密,私钥解密;签名的话,一定是希望只有我能发布这个签名,别人冒充不了,所以是私钥签名,公钥验证。)
本项目基于 Spring security+Oauth2.0 完成用户认证和授权,并自定义了用户身份信息内容。
Spring security 是一个身份验证和访问控制框架,它集成了 Oauth 2.0 协议。Oauth2.0协议的核心是颁发令牌(和校验令牌)。
搭建好服务,使用 @EnableAuthorizationServer ,模拟AuthorizationServer,因为 Spring security 提供了对 JWT 的支持,所以可以生成符合JWT规范的令牌, 先使用 Java 提供的证书管理工具 Keytool ,生成密钥证书 changgou.jks,证书里包含公钥和私钥,把它放在项目的 resources 下,并把它的路径作为 applicaiton.yml 中encrypt: key-store: location:
属性的值,这样后续可以使用私钥加盐、非对称加密算法,创建 JWT 令牌。 openssl 可以导出公钥信息,之后可以使用 @EnableResourceServer(见下一篇博客) ,使用公钥来验证令牌。
本项目中,用户登录时,认证服务采用密码授权的方式,获取到 access_token 访问令牌、refresh_token 刷新令牌、过期时间、令牌唯一标识 jti 信息。再把令牌写入 cookie 和 Http headers 中。后续访问资源页面时,网关会将 Http headers 中的令牌信息进行传递(cookie 也在的)。获取到令牌之后,可以把它存在 Redis 中,之后网关从 Http Headers 中获取令牌后,看看 Redis 中是否有这条记录,令牌相当于入场券,有令牌记录说明用户被授予入场券了,允以访问。(这只是个思路,实际项目中并没有实现,项目中主要是在令牌中使用角色信息作载荷,用Authorization Server 配合具体的方法做角色校验,见下一篇博客。)