zl程序教程

您现在的位置是:首页 >  后端

当前栏目

畅购第9天项目总结(Spring Security Oauth2 JWT)

Spring项目 总结 Security JWT OAuth2
2023-09-11 14:17:55 时间

1. 用户认证分析

在这里插入图片描述

上面流程图描述了用户要操作的各个微服务,用户查看个人信息需要访问客户微服务,下单需要访问订单微服务,秒杀抢购商品需要访问秒杀微服务。每个服务都需要认证用户的身份,身份认证成功后,需要识别用户的角色然后授权访问对应的功能。

1.1 认证与授权

身份认证

  • 用户身份认证: 即用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问。常见的用户身份认证表现形式有:用户名密码登录,指纹打卡等方式。说通俗点,就相当于校验用户账号密码是否正确

用户授权

  • 用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫用户授权。

1.2 单点登录

  • 用户访问的项目中,至少有3个微服务需要识别用户身份,如果用户访问每个微服务都登录一次就太麻烦了,为了提高用户的体验,我们需要实现让用户在一个系统中登录,其他任意受信任的系统都可以访问,这个功能就叫单点登录

  • 单点登录不能等同于一个账号同时只支持一个设备登录在线!

  • 单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。 SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统

1.3 第三方账号登录

随着国内及国外巨头们的平台开放战略以及移动互联网的发展,第三方登录已经不是一个陌生的产品设计概念了。 所谓的第三方登录,是说基于用户在第三方平台上已有的账号和密码来快速完成己方应用的登录或者注册的功能。而这里的第三方平台,一般是已经拥有大量用户的平台,国外的比如Facebook,Twitter等,国内的比如微博、微信、QQ等。

在这里插入图片描述

第三方账号登录优点

  • 相比于本地注册,第三方登录一般来说比较方便、快捷,能够显著降低用户的注册和登录成本,方便用户实现快捷登录或注册。
  • 在第一次绑定成功之后,之后用户便可以实现一键登录,使得后续的登录操作比起应用内的登录来容易了很多。
  • 对于某些应用来说,使用第三方登录完全可以满足自己的需要,因此不必要设计和开发一套自己的账户体系。
  • 通过授权,可以通过在第三方平台上分享用户在应用内的活动在第三方平台上宣传自己,从而增加产品知名度。
  • 通过授权,可以获得该用户在第三方平台上的好友或粉丝等社交信息,从而后续可以针对用户的社交关系网进行有目的性的营销宣传,为产品的市场推广提供另一种渠道。

2. 认证技术方案

2.1 单点登录技术方案

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

在这里插入图片描述

单点登录特点:

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

Java中可以实现单点登录的用户认证的框架:

  • Apache Shiro

  • CAS

  • Spring security CAS

2.2 Oauth2认证

OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本。

2.2.1 Oauth2认证流程

第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的接口协议。

下边分析一个Oauth2认证的例子,黑马程序员网站使用微信认证的过程

在这里插入图片描述

Oauth2包括以下角色

  • 1、客户端 本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:畅购在线Android客户端、畅购在线Web客户端(浏览器端)、微信客户端等。

  • 2、资源拥有者 通常为用户,也可以是应用程序,即该资源的拥有者。

  • 3、授权服务器(也称认证服务器) 用来对资源拥有的身份进行认证、对访问资源进行授权。客户端要想访问资源需要通过认证服务器由资源拥有者授 权后方可访问。

  • 4、资源服务器 存储资源的服务器,比如,畅购网用户管理服务器存储了畅购网的用户信息等。客户端最终访问资源服务器获取资源信息。

2.3 Spring security Oauth2认证解决方案

项目采用 Spring security + Oauth2完成用户认证及用户授权,Spring security 是一个强大的和高度可定制的身份验证和访问控制框架,Spring security 框架集成了Oauth2协议,下图是项目认证架构图:

在这里插入图片描述

  • 1、用户请求认证服务完成认证。

  • 2、认证服务下发用户身份令牌,拥有身份令牌表示身份合法。

  • 3、用户携带令牌请求资源服务,请求资源服务必先经过网关。

  • 4、网关校验用户身份令牌的合法,不合法表示用户没有登录,如果合法则放行继续访问。

  • 5、资源服务获取令牌,根据令牌完成授权。

  • 6、资源服务完成授权则响应资源信息。

3. Oauth2授权模式

Oauth2有以下授权模式:

  • 1.授权码模式(Authorization Code)[常用]
  • 2.隐式授权模式(Implicit)
  • 3.密码模式(Resource Owner Password Credentials) [常用]
  • 4.客户端模式(Client Credentials)

3.1 授权码授权模式实现

上边例举的黑马程序员网站使用QQ认证的过程就是授权码模式,流程如下:

  • 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

参数列表如下:

授权配置类AuthorizationServerConfig.java下文有完整代码!

参数参数介绍
client_id:客户端id,和授权配置类中设置的客户端id一致
response_type:授权码模式固定为code
scop:客户端范围,和授权配置类中设置的scop一致
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)

浏览器访问上面的Get请求,自动跳转到下面页面:

在这里插入图片描述

输入账号和密码,点击Login。 Spring Security接收到请求会调用UserDetailsService接口的loadUserByUsername方法查询用户正确的密码。 输入当前工程中客户端ID为changgou,秘钥也为changgou即可认证通过。

接下来进入授权页面:

在这里插入图片描述

点击Authorize,接下来返回授权码: 认证服务携带授权码跳转redirect_uri,code=aiT1wn就是返回的授权码!

在这里插入图片描述

(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完成,http basic认证:

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BckeSwxx-1611645795044)(畅购第9天,第10天项目总结(Spring Security Oauth2 JWT).assets/20210126123724219.png)]

客户端Id和客户端密码会匹配数据库oauth_client_details表中的客户端id及客户端密码。

点击发送: 申请令牌成功,如下图:

在这里插入图片描述

返回数据如下:

参数参数介绍
access_token:访问令牌,携带此令牌访问资源
token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用
Bearer Token:(http://www.rfcreader.com/#rfc6750)
refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间
expires_in:范围,与定义的客户端范围一致
scope:范围,与定义的客户端范围一致
jti:当前token的唯一标识

(3)令牌校验

Spring Security Oauth2提供校验令牌的端点,如下:

# Post请求:
http://localhost:9001/oauth/check_token?token= [access_token]
参数参数介绍
access_token访问令牌

使用postman测试如下:

在这里插入图片描述

如果令牌校验失败,会出现如下结果:

在这里插入图片描述

如果令牌过期了,会如下如下结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fjapgliu-1611645795047)(畅购第9天,第10天项目总结(Spring Security Oauth2 JWT).assets/20210126124633718.png)]

(4)刷新令牌

刷新令牌是当令牌快过期时重新生成一个令牌,它于授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码 也不需要账号和密码,只需要一个刷新令牌、客户端id和客户端密码。

# Post请求:
http://localhost:9001/oauth/token
参数参数介绍
grant_type:固定为 refresh_token
refresh_token:刷新令牌(注意不是access_token,而是refresh_token)

在这里插入图片描述

3.2 密码授权模式实现

(1)认证

密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接通过用户名和密码即可申请令牌。

# Post请求:
http://localhost:9001/oauth/token
请求参数参数介绍
grant_type:密码模式授权填写password
username:账号
password:密码

并且此链接需要使用 http Basic认证:

请添加图片描述

请添加图片描述

测试结果如下:

请添加图片描述

(2)校验令牌

Spring Security Oauth2提供校验令牌的端点,如下:

# Get请求
http://localhost:9001/oauth/check_token?token=
请求参数参数介绍
token:令牌

使用postman测试如下:

请添加图片描述

返回结果:

{
    "companyId": null,
    "userpic": null,
    "scope": [
        "app"
    ],
    "name": null,
    "utype": null,
    "active": true,
    "id": null,
    "exp": 1990221534,
    "jti": "5b96666e-436b-4301-91b5-d89f9bbe6edb",
    "client_id": "changgou",
    "username": "szitheima"
}
  • exp:过期时间,long类型,距离1970年的秒数(new Date().getTime()可得到当前时间距离1970年的毫秒数)

  • user_name: 用户名

  • client_id:客户端Id,在oauth_client_details中配置

  • scope:客户端范围,在oauth_client_details表中配置

  • jti:与令牌对应的唯一标识 companyId、userpic、name、utype、

  • id:这些字段是本认证服务在Spring Security基础上扩展的用户身份信息

(3)刷新令牌

刷新令牌是当令牌快过期时重新生成一个令牌,它于授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码 也不需要账号和密码,只需要一个刷新令牌、客户端id和客户端密码。

# Post 请求
http://localhost:9001/oauth/token
请求参数参数介绍
grant_type:固定为 refresh_token
refresh_token:刷新令牌(注意不是access_token,而是refresh_token)

请添加图片描述

刷新令牌成功,会重新生成新的访问令牌和刷新令牌,令牌的有效期也比旧令牌长。

刷新令牌通常是在令牌快过期时进行刷新 。

3.3 总结

Oauth2有以下授权模式:

  • 1.授权码模式(Authorization Code)[常用]
  • 2.隐式授权模式(Implicit)
  • 3.密码模式(Resource Owner Password Credentials) [常用]
  • 4.客户端模式(Client Credentials)

授权码授权模式

1.申请授权码 Get:http://localhost:9001/oauth/authorize?client_id=changgou&response_type=code&scop=app&redirect_uri=http://localhost

2.登陆界面输入客户端ID和客户端密钥

3.点击authorize授权,获取授权码code

4.用户授权码获得令牌: Post: http://localhost:9001/oauth/token

密码授权模式

1.客户端ID和客户端密钥需要传递到后台

2.用户账号和密码需要传递到后台

4. 资源服务授权

4.1 资源服务授权流程

(1)传统授权流程

请添加图片描述

资源服务器授权流程如上图,客户端先去授权服务器申请令牌,申请令牌后,携带令牌访问资源服务器,资源服务器访问授权服务校验令牌的合法性,授权服务会返回校验结果,如果校验成功会返回用户信息给资源服务器,资源服务器如果接收到的校验结果通过了,则返回资源给客户端。

传统授权方法的问题是用户每次请求资源服务,资源服务都需要携带令牌访问认证服务去校验令牌的合法性,并根据令牌获取用户的相关信息,性能低下。

(2)公钥私钥授权流程

在这里插入图片描述

传统的授权模式性能低下,每次都需要请求授权服务校验令牌合法性,我们可以利用公钥私钥完成对令牌的加密,如果加密解密成功,则表示令牌合法,如果加密解密失败,则表示令牌无效不合法,合法则允许访问资源服务器的资源,解密失败,则不允许访问资源服务器资源

上图的业务流程如下:

  • 1、客户端请求认证服务申请令牌
  • 2、认证服务生成令牌认证服务采用非对称加密算法,使用私钥生成令牌
  • 3、客户端携带令牌访问资源服务客户端在Http header 中添加: Authorization:Bearer 令牌。
  • 4、资源服务请求认证服务校验令牌的有效性资源服务接收到令牌,使用公钥校验令牌的合法性
  • 5、令牌有效,资源服务向客户端响应资源信息

4.2 公钥私钥

对称加密的时代,加密和解密用的是同一个密钥,这个密钥既用于加密,又用于解密。这样做有一个明显的缺点,如果两个人之间传输文件,两个人都要知道密钥,如果是三个人呢,五个人呢?于是就产生了非对称加密,用一个密钥进行加密(公钥),用另一个密钥进行解密(私钥)

4.2.1 公钥私钥原理

张三有两把钥匙,一把是公钥,另一把是私钥。

请添加图片描述

张三把公钥送给他的朋友们----李四、王五、赵六----每人一把。

在这里插入图片描述

李四要给张三写一封保密的信。ta写完后用张三的公钥加密,就可以达到保密的效果。

请添加图片描述

张三收信后,用私钥解密,就看到了信件内容。这里要强调的是,只要张三的私钥不泄露,这封信就是安全的,即使落在别人手里,也无法解密。

请添加图片描述

张三给李四回信,决定采用"数字签名"。他写完后先用Hash函数,生成信件的摘要(digest)。张三将这个签名,附在信件下面,一起发给李四。

在这里插入图片描述

李四收信后,取下数字签名,用张三的公钥解密,得到信件的摘要。由此证明,这封信确实是张三发出的。李四再对信件本身使用Hash函数,将得到的结果,与上一步得到的摘要进行对比。如果两者一致,就证明这封信未被修改过。

请添加图片描述

4.2.2 生成私钥公钥

Spring Security 提供对JWT的支持,本节我们使用Spring Security 提供的JwtHelper来创建JWT令牌,校验JWT令牌 等操作。 这里JWT令牌我们采用非对称算法进行加密,所以我们要先生成公钥和私钥。

(1)生成密钥证书 下边命令生成密钥证书,采用RSA 算法每个证书包含公钥和私钥

创建一个文件夹,在该文件夹下执行如下CMD命令行:

keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou 

Keytool 是一个java提供的证书管理工具,参数如下:

命令参数参数介绍
-alias:密钥的别名:changgou
-keyalg:使用的hash算法名称:RSA
-keypass:密钥的访问密码:changgou
-keystore:密钥库文件名,xc.keystore保存了生成的证书:changgou.jks
-storepass:密钥库的访问密码:changgou、

执行完成后如下(我这里是在项目的resources目录下执行的该命令):

在这里插入图片描述

(2)查询证书信息

keytool -list -keystore changgou.jks

(3)删除别名 (可以不执行)

keytool -delete -alias changgou -keystore changgou.jsk

4.2.3 导出公钥

到处公钥需要借助openssl,openssl是一个加解密工具包,这里使用openssl来导出公钥信息。

安装 openssl:http://slproweb.com/products/Win32OpenSSL.html

下载后,执行Win64OpenSSL-1_1_0g.exe 文件进行安装!

安装完成后,配置openssl的path环境变量,如下图:

在这里插入图片描述

在这里插入图片描述

cmd进入changgou.jks文件所在目录执行如下命令:

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

执行结果如图:

在这里插入图片描述

下面段内容是公钥:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlv4lApm1ABDfWaXKuYh0
KcYCX4AFLbQMBKIbeFeNHWymqCnDdLsZkrZ2+oDKrx2tMQ0T4yDGoYHw+e2HgQqN
qZNHrHoxAC58wkTwzs4sEBt6Y7nYvO52c7Qdtpmx+nLIQOmUhq2QsDwVd1urw2LS
tOdwW1zwZh8EGbxMbmzrpKiGjKYgQFv9l+tFjDhTtEVgALPCDLG05qAo6TwuFDby
to0rICZ1VlHBf9V9gYE5dtVIyp+2e8MxRQgMbYVYJTr8/+h/nmELfwqLqVbMUqAK
YFbc0a8XOzYvEdS9bwAXG3aNv/5NbhAARWAhb1KS0uMckcXw7tB/8AchdVVocfkN
/wIDAQAB
-----END PUBLIC KEY-----

将上边的公钥拷贝到文本public.key文件中,合并为一行,可以将它放到需要实现授权认证的工程中。

在这里插入图片描述

4.2.4 JWT令牌

(1)创建令牌数据

在changgou-user-oauth2工程中创建测试类com.changgou.token.CreateJwtTest,使用它来创建令牌信息,代码如下:

/**
 * @Auther: csp1999
 * @Date: 2021/01/25/19:30
 * @Description:
 */
public class CreateJwtTest {

    /**
     * 创建令牌测试
     */
    @Test
    public void testCreateToken(){
        // 证书文件路径
        String key_location="changgou.jks";

        // 秘钥库密码
        String key_password="changgou";

        // 秘钥密码
        String keypwd = "changgou";

        // 秘钥别名
        String alias = "changgou";

        // 访问证书路径: 加载证书
        ClassPathResource resource = new ClassPathResource(key_location);

        // 创建秘钥工厂:读取证书数据
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource,key_password.toCharArray());

        // 读取秘钥对(公钥、私钥)
        KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypwd.toCharArray());

        // 获取私钥-> RSA算法
        RSAPrivateKey rsaPrivate = (RSAPrivateKey) keyPair.getPrivate();

        // 定义Payload
        Map<String, Object> tokenMap = new HashMap<>();
        tokenMap.put("id", "1");
        tokenMap.put("name", "itheima");
        tokenMap.put("roles", "ROLE_VIP,ROLE_USER");

        // 加密:生成Jwt令牌
        Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(rsaPrivate));

        // 取出令牌
        String encoded = jwt.getEncoded();

        // 测试输出
        System.out.println(encoded);
        // 结果:
        // eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJpdGhlaW1hIiwiaWQiOiIxIn0.IR9Qu9ZqYZ2gU2qgAziyT38UhEeL4Oi69ko-dzC_P9-Vjz40hwZDqxl8wZ-W2WAw1eWGIHV1EYDjg0-eilogJZ5UikyWw1bewXCpvlM-ZRtYQQqHFTlfDiVcFetyTayaskwa-x_BVS4pTWAskiaIKbKR4KcME2E5o1rEek-3YPkqAiZ6WP1UOmpaCJDaaFSdninqG0gzSCuGvLuG40x0Ngpfk7mPOecsIi5cbJElpdYUsCr9oXc53ROyfvYpHjzV7c2D5eIZu3leUPXRvvVAPJFEcSBiisxUSEeiGpmuQhaFZd1g-yJ1WQrixFvehMeLX2XU6W1nlL5ARTpQf_Jjiw
        
    }

    /***
     * 解析校验令牌测试
     */
    @Test
    public void testParseToken(){
        // 令牌
        String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJpdGhlaW1hIiwiaWQiOiIxIn0.IR9Qu9ZqYZ2gU2qgAziyT38UhEeL4Oi69ko-dzC_P9-Vjz40hwZDqxl8wZ-W2WAw1eWGIHV1EYDjg0-eilogJZ5UikyWw1bewXCpvlM-ZRtYQQqHFTlfDiVcFetyTayaskwa-x_BVS4pTWAskiaIKbKR4KcME2E5o1rEek-3YPkqAiZ6WP1UOmpaCJDaaFSdninqG0gzSCuGvLuG40x0Ngpfk7mPOecsIi5cbJElpdYUsCr9oXc53ROyfvYpHjzV7c2D5eIZu3leUPXRvvVAPJFEcSBiisxUSEeiGpmuQhaFZd1g-yJ1WQrixFvehMeLX2XU6W1nlL5ARTpQf_Jjiw";

        // 公钥
        String publickey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAmt47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnhcP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEmoLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZSxtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv9QIDAQAB-----END PUBLIC KEY-----";

        // 校验Jwt
        Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));

        // 获取Jwt原始内容
        String claims = jwt.getClaims();
        System.out.println(claims);// {"roles":"ROLE_VIP,ROLE_USER","name":"itheima","id":"1"}

        // jwt令牌
        //String encoded = jwt.getEncoded();
        //System.out.println(encoded);
    }
}

5. 搭建认证服务器

5.1 需求分析

用户登录的流程图如下:

请添加图片描述

执行流程:

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

5.2 搭建auth 认证微服务工程

认证服务需要实现的功能如下:

1、登录接口

  • 前端post提交账号、密码等,用户身份校验通过,生成令牌,并将令牌写入cookie。

2、退出接口

  • 校验当前用户的身份为合法并且为已登录状态。 将令牌从cookie中删除。

在这里插入图片描述

application.yml

server:
  # 认证微服务端口
  port: 9001
# spring 相关配置
spring:
  application:
    # 微服务名称
    name: user-auth
  # Redis配置
  redis:
    # Redis数据库索引(默认为0)
    database: 0
    # Redis服务器地址
    host: 8.131.66.136
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password: csp19990129
  # 数据库相关配置
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/changgou_oauth?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=UTC
    username: root
    password: root
  main:
    allow-bean-definition-overriding: true
# eureka相关配置
eureka:
  instance:
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
# auth相关配置
auth:
  # token存储到redis的过期时间
  ttl: 3600
  # client唯一id
  clientId: changgou
  # client密钥
  clientSecret: changgou
  # cookie域名
  cookieDomain: localhost
  cookieMaxAge: -1
# 本地证书、密钥以及证书密码配置
encrypt:
  key-store:
    # 证书路径(resources路径下)
    location: classpath:/changgou.jks
    # 密钥
    # 公钥(提供给每个微服务),可以加密,用于校验令牌的合法性--->私钥(提供给认证微服务),可以解密,用于生成令牌--->非对称加密算法RSA
    # 我们之前做的MD5加密算法,使用的是摘要加密算法,不可逆!
    # AES/DESC 使用的是对称加密,可以加密和解密,加密解密的密钥是相同的!
    secret: changgou
    # 证书别名
    alias: changgou
    # 证书密码
    password: changgou

工具类封装

创建com.changgou.oauth.util.AuthToken类,存储用户令牌数据,代码如下:

AuthToken

/**
 * @Author: csp1999
 * @Date: 2020/5/18 14:52
 * @Description: 用户token令牌封装工具类
 */
public class AuthToken implements Serializable {

    /**
     * 令牌信息
     */
    String accessToken;

    /**
     * 刷新token(refresh_token)
     */
    String refreshToken;

    /**
     * jwt短令牌
     */
    String jti;

    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public String getRefreshToken() {
        return refreshToken;
    }

    public void setRefreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
    }

    public String getJti() {
        return jti;
    }

    public void setJti(String jti) {
        this.jti = jti;
    }
}

创建com.changgou.oauth.util.CookieUtil类,操作Cookie,代码如下:

CookieUtil

/**
 * @Author: csp1999
 * @Date: 2020/5/18 14:52
 * @Description: CookieUtil工具类
 */
public class CookieUtil {

    /**
     * 设置cookie
     *
     * @param response
     * @param name     cookie名字
     * @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;
    }
}

创建com.changgou.oauth.util.UserJwt类,封装SpringSecurity中User信息以及用户自身基本信息,代码如下:

UserJwt

/**
 * @Author: csp1999
 * @Date: 2020/5/18 14:52
 * @Description: 封装用户信息的对象,用于存储到JWT中
 */
public class UserJwt extends User {

    /**
     * 用户ID
     */
    private String id;

    /**
     * 用户名字
     */
    private String name;

    /**
     * 设置公司
     */
    private String company;

    /**
     * 地址
     */
    private String address;

    public UserJwt(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCompany() {
        return company;
    }

    public void setCompany(String company) {
        this.company = company;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

config包下配置类

AuthorizationServerConfig

@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    /**
     * 数据源,用于从数据库获取数据进行认证操作,测试可以从内存中获取
     */
    @Autowired
    private DataSource dataSource;
    /**
     * jwt令牌转换器
     */
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    /**
     * SpringSecurity 用户自定义授权认证类
     */
    @Autowired
    UserDetailsService userDetailsService;

    /**
     * 授权认证管理器
     */
    @Autowired
    AuthenticationManager authenticationManager;

    /**
     * 令牌持久化存储接口
     */
    @Autowired
    TokenStore tokenStore;

    @Autowired
    private CustomUserAuthenticationConverter customUserAuthenticationConverter;

    /**
     * 客户端信息配置
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("changgou")          // 客户端id
                .secret("changgou")                     // 秘钥
                .redirectUris("http://localhost")       // 重定向地址
                .accessTokenValiditySeconds(3600)       // 访问令牌有效期
                .refreshTokenValiditySeconds(3600)      // 刷新令牌有效期
                .authorizedGrantTypes(
                        "authorization_code",           // 根据授权码生成令牌
                        "client_credentials",           // 客户端认证
                        "refresh_token",                // 刷新令牌
                        "password")                     // 密码方式认证
                .scopes("app");                         // 客户端范围,名称自定义,必填
    }

    /**
     * 授权服务器端点配置
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.accessTokenConverter(jwtAccessTokenConverter)
                .authenticationManager(authenticationManager)  // 认证管理器
                .tokenStore(tokenStore)                        // 令牌存储
                .userDetailsService(userDetailsService);       // 用户信息service
    }

    /**
     * 授权服务器的安全配置
     *
     * @param oauthServer
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.allowFormAuthenticationForClients()
                .passwordEncoder(new BCryptPasswordEncoder())
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }

    /**
     * 读取密钥的配置
     *
     * @return
     */
    @Bean("keyProp")
    public KeyProperties keyProperties() {
        return new KeyProperties();
    }

    @Resource(name = "keyProp")
    private KeyProperties keyProperties;

    /**
     * 客户端配置
     */
    @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }

    @Bean
    @Autowired
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    /**
     * JWT令牌转换器
     *
     * @param customUserAuthenticationConverter
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        KeyPair keyPair = new KeyStoreKeyFactory(
                keyProperties.getKeyStore().getLocation(),                          // 证书路径 changgou.jks
                keyProperties.getKeyStore().getSecret().toCharArray())              // 证书秘钥 changgouapp
                .getKeyPair(
                        keyProperties.getKeyStore().getAlias(),                     // 证书别名 changgou
                        keyProperties.getKeyStore().getPassword().toCharArray());   // 证书密码 changgou
        converter.setKeyPair(keyPair);
        // 配置自定义的CustomUserAuthenticationConverter
        DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();
        accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter);
        return converter;
    }
}

CustomUserAuthenticationConverter

@Component
public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {

    @Autowired
    UserDetailsService userDetailsService;

    @Override
    public Map<String, ?> convertUserAuthentication(Authentication authentication) {
        LinkedHashMap response = new LinkedHashMap();
        String name = authentication.getName();
        response.put("username", name);

        Object principal = authentication.getPrincipal();
        UserJwt userJwt = null;
        if (principal instanceof UserJwt) {
            userJwt = (UserJwt) principal;
        } else {
            // refresh_token默认不去调用userdetailService获取用户信息,这里我们手动去调用,得到 UserJwt
            UserDetails userDetails = userDetailsService.loadUserByUsername(name);
            userJwt = (UserJwt) userDetails;
        }
        response.put("name", userJwt.getName());
        response.put("id", userJwt.getId());
        response.put("company", userJwt.getCompany());
        response.put("address", userJwt.getAddress());

        if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
            response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
        }
        return response;
    }
}

UserDetailsServiceImpl

/**
 * 自定义授权认证类
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    ClientDetailsService clientDetailsService;

    /**
     * 自定义授权认证
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 取出身份,如果身份为空说明没有认证
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 没有认证统一采用httpbasic认证,httpbasic中存储了client_id和client_secret,开始认证client_id和client_secret
        if (authentication == null) {
            ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
            if (clientDetails != null) {
                // 秘钥
                String clientSecret = clientDetails.getClientSecret();
                // 静态方式
                return new User(username, new BCryptPasswordEncoder().encode(clientSecret), AuthorityUtils.commaSeparatedStringToAuthorityList(""));
                // 数据库查找方式
                //return new User(username,clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList(""));
            }
        }

        if (StringUtils.isEmpty(username)) {
            return null;
        }

        // 根据用户名查询用户信息
        String pwd = new BCryptPasswordEncoder().encode("szitheima");
        // 创建User对象
        String permissions = "goods_list,seckill_list";

        UserJwt userDetails = new UserJwt(username, pwd, AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));

        //userDetails.setComy(songsi);
        return userDetails;
    }
}

WebSecurityConfig

@Configuration
@EnableWebSecurity
@Order(-1)
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 忽略安全拦截的URL
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                "/user/login",
                "/user/logout");
    }

    /**
     * 创建授权管理认证对象
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        AuthenticationManager manager = super.authenticationManagerBean();
        return manager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    /**
     * 采用BCryptPasswordEncoder对密码进行编码
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .httpBasic()           // 启用Http基本身份验证
                .and()
                .formLogin()           // 启用表单身份验证
                .and()
                .authorizeRequests()   // 限制基于Request请求访问
                .anyRequest()
                .authenticated();      // 其他请求都需要经过验证
    }
}

service层

在这里插入图片描述

如上图,我们现在实现一个认证流程,用户从页面输入账号密码,到认证服务的Controller层,Controller层调用Service层,Service层调用OAuth2.0的认证地址,进行密码授权认证操作,如果账号密码正确了,就返回令牌信息给Service层,Service将令牌信息给Controller层,Controller层将数据存入到Cookie中,再响应用户。

创建com.changgou.oauth.service.AuthService接口,并添加授权认证方法:

/**
 * @Author: csp1999
 * @Date: 2020/7/7 16:23
 * @Description:
 */
public interface LoginService {

    /**
     * 模拟用户的行为 发送请求 申请令牌 返回
     * @param username 用户名
     * @param password 用户密码
     * @param clientId 客户端id
     * @param clientSecret 客户端密钥
     * @param grandType 认证授权类型:password密码授权模式
     * @return
     */
    AuthToken login(String username, String password, String clientId, String clientSecret, String grandType);
}

创建com.changgou.oauth.service.impl.AuthServiceImpl实现类,实现获取令牌数据,这里认证获取令牌采用的是密码授权模式,用的是RestTemplate向OAuth服务发起认证请求,代码如下:

AuthService接口实现类

/**
 * @Author: csp1999
 * @Date: 2020/7/7 16:23
 * @Description:
 */
@Service
public class AuthServiceImpl implements AuthService {

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 授权认证方法
     *
     * @param username
     * @param password
     * @param clientId
     * @param clientSecret
     * @return
     */
    @Override
    public 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("找不到对应的服务");
        }
        // 获取令牌的url
        String path = serviceInstance.getUri().toString() + "/oauth/token";
        // 定义body
        MultiValueMap<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() {
            @Override
            public 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);
    }
}

LoginService接口实现类

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @Override
    public 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;
    }


    public static void main(String[] args) {
        byte[] decode = Base64.getDecoder().decode(new String("Y2hhbmdnb3UxOmNoYW5nZ291Mg==").getBytes());
        System.out.println(new String(decode));
    }
}

controller层

创建控制层com.changgou.oauth.controller.AuthController,编写用户登录授权方法,代码如下:

AuthController

/**
 * @Author: csp1999
 * @Date: 2020/7/7 16:23
 * @Description:
 */
@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;

    @Autowired
    AuthService 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();
        // 将令牌存储到cookie
        saveCookie(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);
    }
}

UserLoginController

/**
 * @Author: csp1999
 * @Date: 2020/7/7 16:23
 * @Description:
 */
@RestController
@RequestMapping("/user")
public class UserLoginController {

    @Autowired
    private LoginService loginService;

    /**
     * 客户端id
     */
    @Value("${auth.clientId}")
    private String clientId;

    /**
     * 客户端密钥
     */
    @Value("${auth.clientSecret}")
    private String clientSecret;

    /**
     * 授权模式 密码模式
     */
    private static final String GRAND_TYPE = "password";

    /**
     * cookie域名
     */
    @Value("${auth.cookieDomain}")
    private String cookieDomain;

    /**
     * Cookie生命周期
     */
    @Value("${auth.cookieMaxAge}")
    private int cookieMaxAge;


    /**
     * 登录方法:
     * 1.密码模式认证-授权方式:grant_type=password
     *
     * @param username 2.账号 szitheima
     * @param password 3.密码 szitheima
     * @return
     */
    @RequestMapping("/login")
    public Result<Map> login(String username, String password) {
        // 调用loginService的login方法进行登录,并返回生成的令牌数据
        AuthToken authToken = loginService.login(username, password, clientId, clientSecret, GRAND_TYPE);


        // 设置到cookie中
        saveCookie(authToken.getAccessToken());
        return new Result<>(true, StatusCode.OK, "令牌生成成功", authToken);
    }

    // 保存token到cookie中
    private void saveCookie(String token) {
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        CookieUtil.addCookie(response, cookieDomain, "/", "Authorization", token, cookieMaxAge, false);
    }
}

5.3 测试

访问:http://localhost:9001/user/login?username=szitheima&password=szitheima

得到如下结果:

在这里插入图片描述

令牌生成成功!大功告成!