Oauth2.0

Oauth2.0认证流程

官方书籍:https://datatracker.ietf.org/doc/html/rfc6749

image-20210818184003737

我们把

  • Client想象成我们的YxinMiracle系统
  • Resource Owner 资源拥有者
  • Authorization Server 认证服务器(qq认证服务器)
  • Resource Server 资源服务器(qq的用户微服务)

我的目的是:

  • 获取Resource Server 资源服务器的资源

官方文档中表述的流程:

   (A) 客户端向资源所有者请求授权。这
        可以直接向资源所有者提出授权请求
        (如图所示),或最好通过授权间接
        服务器作为中介。

   (B) 客户端收到授权许可,这是一个
        代表资源所有者授权的凭证,
        使用本文件中定义的四种授权类型之一表示
        规范或使用扩展授权类型。这
        授权授予类型取决于所使用的方法
        客户端请求授权和支持的类型
        授权服务器。

   (C) 客户端通过身份验证请求访问令牌
        授权服务器并提供授权许可。

   (D) 授权服务器对客户端进行认证并验证
        授权许可,如果有效,则颁发访问令牌。
        
   (E) 客户端从资源中请求受保护的资源
        服务器并通过提供访问令牌进行身份验证。

   (F) 资源服务器验证访问令牌,如果有效,
        服务于请求。

流程:

  1. 认证请求
  2. 认证授权
  3. 来到认证服务器,交出第二步拿到的授权码,向Authorization Server 认证服务器申请Token
  4. 拿到Token之后
  5. 将Token交给Resource Server申请资源
  6. 交还资源

Oauth2.0在项目中的应用

Oauth2是一个标准的开放的授权协议,应用程序可以根据自己的要求去使用Oauth2,本项目计划使用Oauth2实现如 下目标:

1、YxinMiracle系统访问第三方系统的资源

2、外部系统访问YxinMiracle系统的资源

3、YxinMiracle系统前端(客户端) 访问YxinMiracle系统微服务的资源

4、YxinMiracle系统微服务之间访问资源,例如:微服务A访问微服务B的资源,B访问A的资源

Spring security Oauth2认证解决方案

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

image-20210818185758144

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

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

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

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

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

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

Security Oauth2.0

搭建认证服务器

认证服务器的文件结构:

image-20210818190416998

加入依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

配置文件:

server:
  port: 9001
spring:
  application:
    name: user-auth
  redis:
    host: 192.168.211.132
    port: 6379
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.211.132:3306/database?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=UTC
    username: root
    password: 123456
  main:
    allow-bean-definition-overriding: true
eureka:
  instance:
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
auth:
  ttl: 3600  #token存储到redis的过期时间
  clientId: yxinmiracle
  clientSecret: yxinmiracle
  cookieDomain: localhost
  cookieMaxAge: -1
jks:
  name: yxinmiracle.jks
  storepassword: yxinmiracle
  keypassword: yxinmiracle
  alias: yxinmiracle

Oauth2授权模式

Oauth2有以下授权模式:

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

其中常用的有:

授权码模式,密码模式,流程图如图1::

image-20210818184003737

授权码模式的实现

此时我们可以启动认证服务器,访问连接:

Get请求:
http://localhost:9001/oauth/authorize?client_id=yxinmiracle&response_type=code&scop=app&redirect_uri=http://localhost
  • 其中client_id表示的是那一个系统向认证系统发出请求
  • response_type:code表示为授权码模式
  • scop 表示的是使用范围
  • redirect_uri 重定向到的url

访问之后会出现:

image-20210818194117902

这就需要我们输入认证服务器系统的账号,利用认证服务器系统的账号登录我们的yxinmiracle系统,输入账号:

image-20210818194936440

登录后会进行提示:

image-20210818195100668

意思是:您确定要将认证服务器中的账号认证在yxinmiracle系统吗?这就相当于你们利用qq在别的平台登录一样,这个时候我们点击确认,就会返回一个授权码给我们,根据这个授权码我们可以向认证服务器请求TOKEN:

点击确认后重定向到:

image-20210818195344940

http://localhost/?code=lfiS4M

其中lfiS4M就是我们的授权码,我们可以根据这个授权码去获取Token

使用PostMan发送请求:

其中From表单中的数据是这样的:

KEYVALUE
grant_typeauthorization_code
codelfiS4M(获取到的授权码)
redirect_urihttp://localhost

image-20210818200052937

我们的yxinmiracle系统与我们的认证系统也需要有一个认证的联系,这个联系是需要用过请求头中的信息关联的,也就是PostMan中的:

image-20210818200414229

该请求会化作为请求头中的Authorization,如下图所示:

image-20210818200629965

发送请求:

image-20210818200828288

数据(实际上就是一个jwt):

Key解释
access_token令牌
token_type校验方式
refresh_token刷新令牌
expires_in过期时间
scope使用范围
jti唯一id
{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIwNjEyODg0ODcsInVzZXJfbmFtZSI6InpoYW5nc2FuIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJqdGkiOiJiMmNmYjMwYS0yNGU4LTQ2YWMtOGQ4ZC1lN2ZlYjFmZWJjNGQiLCJjbGllbnRfaWQiOiJjaGFuZ2dvdSIsInNjb3BlIjpbImFwcCJdfQ.UpSbU6xPVadYzl2sgzp2s43air8A5cd2wp11l0_U9BWj8TEtpnrj8PfTD_dMF7Al_hw_-kmeXvl4IX3F9wguUMBYmKYikJna8r0KsMUtA3f0YqD6lE6RcUv3EXfHb1WodTPeHUGLn55FEKDUJkVOzi0fYancCElACn9rMs3wEqhCj0AAs8_2Ee0Fg4wRtF9ZGCXuXZxnBrA-IYeJF5v7dL6w4Aa8YzcYuOmjmT3xrKH3FDDWnZql5YLrTlKEbNsR0AVQgQQXOBClym50zxOh_qtEkqMBCZQCIdkcDqFvpn8H7EdB46bx3GDI6rw_Xz_eLoMVt0qpzwx3KxYt7_ZhpQ",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFwcCJdLCJhdGkiOiJiMmNmYjMwYS0yNGU4LTQ2YWMtOGQ4ZC1lN2ZlYjFmZWJjNGQiLCJleHAiOjIwNjEyODg0ODcsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwianRpIjoiYmQ0ZjJmOTMtNTA3ZC00NWQzLWFlOGQtZGVlMWY5M2ExMTFlIiwiY2xpZW50X2lkIjoiY2hhbmdnb3UifQ.IVdKYliRDRJPf6bMxrx0XYDBFU00oOAJLjLC99tbkJIcJ75wKZSMdJU9exLhiqZ22ZLVBaI2KiMdSCP4DRAYuqV7DFL31c3CTsNEOrWktRgbK_z_T4RP_JIoK3pNPWk5uxcZXM6x2rnMU_n6Lu3_CYBh5xr3DC9a5HXtkmmpKzf7S3lXzm4Hq5DJKlqcnhRLLF97IDrqPUkXvcAbErdh0kKKY_bB1MwMl-ascBU8SKBPEFn96-nY0u0Fd4i0kAtQlnQ2TJFSIsOE8GQaKswYaNNJHh2JlyegiI1ByfPEu-iIDpKJv4OxZsMAnh2HmmLeWJbjf-0AhFpyaFtEcPsIZg",
    "expires_in": 431999999,
    "scope": "app",
    "jti": "b2cfb30a-24e8-46ac-8d8d-e7feb1febc4d"
}

令牌校验

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

Get: http://localhost:9001/oauth/check_token?token=

参数:

token:令牌

通过postman发送请求:

image-20210818201804130

结果为:

{
    "user_name": "zhangsan",
    "scope": [
        "app"
    ],
    "active": true,
    "exp": 2061288487,
    "authorities": [
        "ROLE_ADMIN",
        "ROLE_USER"
    ],
    "jti": "b2cfb30a-24e8-46ac-8d8d-e7feb1febc4d",
    "client_id": "yxinmiracle"
}

刷新令牌

刷新令牌是当令牌快过期时重新生成一个令牌,它于授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码 也不需要账号和密码,只需要一个刷新令牌、客户端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认证。

image-20210818211457914

(2)校验令牌

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

Get: http://localhost:9001/oauth/check_token?token=

参数:

token:令牌

使用postman测试如下:

image-20210818211616233

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基础上扩展的用户身份信息

资源服务授权

资源服务授权流程

(1)传统授权流程

image-20210818211909104

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

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

(2)公钥私钥授权流程

image-20210818211925845

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

上图的业务流程如下:

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

公钥私钥

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

公钥私钥原理

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

image-20210818212002223

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

image-20210818212021731

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

image-20210818212605467

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

image-20210818212639303

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

image-20210818212127736

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

image-20210818212133136

生成私钥公钥

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

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

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

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

Keytool 是一个java提供的证书管理工具

-alias:密钥的别名 
-keyalg:使用的hash算法 
-keypass:密钥的访问密码 
-keystore:密钥库文件名,xc.keystore保存了生成的证书 
-storepass:密钥库的访问密码 

使用如图:

image-20210818212928549

之后文件夹中会出现:

image-20210818213200608

(2)查询证书信息

keytool -list -keystore yxinmiracle.jks

(3)删除别名

keytool -delete -alias yxinmiracle -keystore yxinmiracle.jsk

导出公钥

openssl是一个加解密工具包,这里使用openssl来导出公钥信息。

下载地址:http://slproweb.com/products/Win32OpenSSL.html

选择:

image-20210818213457034

安装完后进行环境变量的添加

cmd进入yxinmiracle.jks文件所在目录执行如下命令(如下命令在windows下执行,会把-变成中文方式,请将它改成英文的-):

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

结果:

image-20210818213634134

下面段内容是公钥

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlTaqLMvTUHgaAdffduem
niZ2hee/lKmZ32rDS/862fbLuX4LdHWjkZrrWV2MzFrIYeu3zRr7QxCQy/rtvDKk
f9OKd+K3d2dJ8ljjVwmOAVI48BlBoZU66Ugu8oSepXWWP7d5sJcpCUzfUJc3AlZE
tvpttG+YJjOp2dI4rb7AAK1YD+kw6wi47rmCneYhQUtIq3pCqe/Owudceq0JOH40
R3IvXfsv/YHi5tFENT+f+Q/KMlFVWedRzMyBR12FvzGXiwYlYjcNbb8wAp3UWyuy
HinrbTI90EwjawUKMsjEbirKfN0JSjIdpg5J5EEMUk0XVaqMZZ3RPiRwFOTXmnkv
6wIDAQAB
-----END PUBLIC KEY-----

测试

创建令牌:

package com.yxinmiracle.token;

import com.alibaba.fastjson.JSON;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.util.HashMap;
import java.util.Map;


public class CreateJwtTest {

    /***
     * 创建令牌测试
     */
    @Test
    public void testCreateToken(){
        //证书文件路径
        String key_location="yxinmiracle.jks";
        //秘钥库密码
        String key_password="yxinmiracle";
        //秘钥密码
        String keypwd = "yxinmiracle";
        //秘钥别名
        String alias = "yxinmiracle";

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

        //创建秘钥工厂
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource,key_password.toCharArray());

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

        //获取私钥
        RSAPrivateKey rsaPrivate = (RSAPrivateKey) keyPair.getPrivate();

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

        //生成Jwt令牌
        Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(rsaPrivate)); // 要用私钥来作为秘钥放到jwt中创造令牌

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

结果:

image-20210818214904982

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJZeGluTWlyYWNsZSIsImlkIjoiMSJ9.gXlwor6RS4MkFKtm9wFY9hbzYZmT-OthHZ6cCbWdr50KIOw_MESCDgVFkkL_4PFwBI8unIVYAD5JM_4Fkr3KmrkjObhCkyANe_lhfWHzvBZWUpK2Wsc2B2NF3juePeEUHTGbSIwVbQng0CKNn_3vCK6IIxRMrZUBcqqBbtt6cKawtMK_CzOX3JpI3nqmAf7_5oq3aTKM0l0Lp1AsHiHZiJ_Umk9lqg86KxeDhrJjuS5cQffRGrPNQhs-0HrgqtBsyqvqBp8XHo8J82Sc_rdABF4R1Ynoye4tKDTg6TCnWcFgD3MkyNs5caHy0uch3bHjuYsV44JrnjEUCvWH85GVUA

解析令牌:

package com.yxinmiracle.token;

import org.junit.Test;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;


public class ParseJwtTest {

    /***
     * 校验令牌
     */
    @Test
    public void testParseToken(){
        //令牌
        String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJZeGluTWlyYWNsZSIsImlkIjoiMSJ9.gXlwor6RS4MkFKtm9wFY9hbzYZmT-OthHZ6cCbWdr50KIOw_MESCDgVFkkL_4PFwBI8unIVYAD5JM_4Fkr3KmrkjObhCkyANe_lhfWHzvBZWUpK2Wsc2B2NF3juePeEUHTGbSIwVbQng0CKNn_3vCK6IIxRMrZUBcqqBbtt6cKawtMK_CzOX3JpI3nqmAf7_5oq3aTKM0l0Lp1AsHiHZiJ_Umk9lqg86KxeDhrJjuS5cQffRGrPNQhs-0HrgqtBsyqvqBp8XHo8J82Sc_rdABF4R1Ynoye4tKDTg6TCnWcFgD3MkyNs5caHy0uch3bHjuYsV44JrnjEUCvWH85GVUA";

        //公钥
        String publickey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlTaqLMvTUHgaAdffduemniZ2hee/lKmZ32rDS/862fbLuX4LdHWjkZrrWV2MzFrIYeu3zRr7QxCQy/rtvDKkf9OKd+K3d2dJ8ljjVwmOAVI48BlBoZU66Ugu8oSepXWWP7d5sJcpCUzfUJc3AlZEtvpttG+YJjOp2dI4rb7AAK1YD+kw6wi47rmCneYhQUtIq3pCqe/Owudceq0JOH40R3IvXfsv/YHi5tFENT+f+Q/KMlFVWedRzMyBR12FvzGXiwYlYjcNbb8wAp3UWyuyHinrbTI90EwjawUKMsjEbirKfN0JSjIdpg5J5EEMUk0XVaqMZZ3RPiRwFOTXmnkv6wIDAQAB-----END PUBLIC KEY-----";

        //校验Jwt
        Jwt 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);
    }
}

结果:

image-20210818215115474

小总结

当我们访问/oauth/token生成的令牌是通过这个方法生成的:

/**
 *
 * 创建令牌的方法
 * @return
 */
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
    //设置jwt的转换器 必须要有
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); // 使用jwt的方式创建令牌
    //设置jwt的秘钥库
    //秘钥库的名称
    String keystorepath =jksProperties.getName(); // 从配置文件中读取配置
    //秘钥库的密码
    String storepassword = jksProperties.getStorepassword();
    //读取秘钥对的密码
    String keypassword = jksProperties.getKeypassword();
    //秘钥库的别名
    String alias = jksProperties.getAlias();
    KeyPair keyPair = new KeyStoreKeyFactory(
            new ClassPathResource(keystorepath), //设置加密的加载文件
            storepassword.toCharArray())//设置读取秘钥库文件的密码
            .getKeyPair(alias, keypassword.toCharArray());//设置获取秘钥的密码
    //设置秘钥对象
    converter.setKeyPair(keyPair);
    //使用JWT的令牌转换器
    DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();
    return converter;
}

实战

用户访问其他功能微服务时,必须判断该用户有没有登录,如果有并且写到的token是正确的(正不正确可以使用该功能微服务中的公钥与用户请求携带来的token进行校验,如果说校验成功那么就是登录过的)

image-20210818223207912

首先我们要登录的话就需要访问我们的user微服务

无对接网关

在user微服务中添加一个配置类,并且把公钥放在user微服务的类路径下:

项目结构代码:

image-20210818222234727

配置类代码:

package com.yxinmiracle.user.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Base64;
import java.util.stream.Collectors;

/**
 * @version 1.0
 * @author: YxinMiracle
 * @date: 2021-08-17 16:41
 */
@Configuration
@EnableResourceServer // 标识这是一台资源服务器
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    //公钥
    private static final String PUBLIC_KEY = "public.key";

    /***
     * 定义JwtTokenStore
     * @param jwtAccessTokenConverter
     * @return
     */
    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    /***
     * 定义JJwtAccessTokenConverter 转化器 既能创建令牌 也能解析令牌 用来配置公钥
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setVerifierKey(getPubKey()); // setVerifierKey需要的是一个字符串信息 getPubKey()得到文件中的公钥
        return converter;
    }
    /**
     * 获取非对称加密公钥 Key
     * @return 公钥 Key
     */
    private String getPubKey() {
        Resource resource = new ClassPathResource(PUBLIC_KEY);
        try {
            InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
            BufferedReader br = new BufferedReader(inputStreamReader);
            return br.lines().collect(Collectors.joining("\n"));
        } catch (IOException ioe) {
            return null;
        }
    }

    /***
     * Http安全配置,对每个到达系统的http请求链接进行校验
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //所有请求必须认证通过
        http.authorizeRequests()
                //下边的路径放行
                .antMatchers(
                        "/user/add","/user/load/{id}"). //配置地址放行 用户注册是需要放行的
                permitAll()
                .anyRequest().
                authenticated();    //其他地址需要认证授权
    }
}

此时在没有携带令牌的令牌的情况下进行访问(此时没有开启网关),可以发现没有权限:

image-20210818222524322

​ 此时我们可以进行登录:

image-20210818222625808

​ 然后携带令牌进行访问,但是在访问的时候需要在请求头中添加上Authorization字段,key为bearer Token,如:

image-20210818222806527

​ 再进行请求访问,发现请求成功:

image-20210818222831545

小插曲

为什么是bearer呢?

因为在我们生成令牌的时候,可以发现有一个token类型,是需要bearer方式的。

image-20210818223025605

对接网关

经过网关的方式发送请求,发现网关并不能将用户的令牌携带到微服务去,如图报了401无权限的错误:

image-20210818223805344

解决办法:

在网关内,我们需要将这个数据添加到请求头中,再传到下一个微服务中:

image-20210818223647588

成功请求:

image-20210818223833909

权限配置

在user微服务中编写一个测试方法,只有角色有“ROLE_XXX”的人才能访问这个路径

image-20210818224537755

可以发现在用户登录的时候我们都会给他配置权限:

image-20210818224717232

在进行Token解析的时候也能看见用户拥有什么样的权限:

{
    "user_name": "zhangsan",
    "scope": [
        "app"
    ],
    "active": true,
    "exp": 2061288487,
    "authorities": [
        "ROLE_ADMIN",
        "ROLE_USER"
    ],
    "jti": "b2cfb30a-24e8-46ac-8d8d-e7feb1febc4d",
    "client_id": "yxinmiracle"
}

进行访问,可以发现并不能访问:

image-20210818224921651

将权限设置为ROLE_ADMIN后:

image-20210818224959863

进行访问,成功访问:

image-20210818225016958

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐