服务认证授权:OAuth2.0

目录


配套资料,免费下载
链接:https://pan.baidu.com/s/1la_3-HW-UvliDRJzfBcP_w
提取码:lxfx
复制这段内容后打开百度网盘手机App,操作更方便哦

第一章 OAuth2.0的预备篇

1.1、Base64

1.1.1、Base64的介绍

Base64是一种用64个字符来表示任意二进制数据的编码方式。

1.1.2、Base64的演示

@Test
public void testEncodeAndDecode() {
    
    
    byte[] bytes = "Hello,World".getBytes();

    bytes = Base64.getEncoder().encode(bytes);
    System.out.println("编码后:" + new String(bytes));

    bytes = Base64.getDecoder().decode(bytes);
    System.out.println("解码后:" + new String(bytes));
}
编码后:SGVsbG8sV29ybGQ=
解码后:Hello,World

1.2、JWT

1.2.1、JWT的介绍

JWT,全称JSON Web Tokens,官网地址:https://jwt.io,是一款出色的分布式身份校验方案,可以生成token,也可以解析检验token。

1.2.2、JWT的组成

JWT由三部分组成,它们之间用点(.)连接,这三部分分别是:

  • HEADER:头部,头部主要是用来定义令牌签名使用的算法和令牌类型,是一个JSON串,需要使用Base64Url进行编码。
  • PAYLOAD:载荷,载荷主要用来存放传输的数据,是一个JSON串,需要使用Base64Url进行编码。
  • SIGNATURE:签名,根据你HEADER定义的算法类型,签名主要用于防止数据被篡改。

注意:Base64Url这个算法跟Base64算法基本类似,但有一些小的不同。JWT作为一个令牌(token),有些场合可能会放到URL(比如 api.example.com/?token=xxx)。Base64有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ ,这就是Base64Url算法。

那么最终JWT就是一个字符串,这个字符串由三部分组成,第一部分头部,第二部分载荷,第三部分签名,最终形式:Header.Payload.Signature

HEADER

Header部分是一个JSON串,描述JWT的元数据,通常是下面的样子。

上面代码中,alg属性表示签名的算法(algorithm),默认是HMAC SHA256(写成HS256);typ属性表示这个令牌(token)的类型(type),JWT令牌统一写为JWT。

PAYLOAD

Payload部分也是一个JSON串,用来存放实际需要传递的数据,JWT 规定了7个官方字段,可供选用。

  • iss (issuer):签发人
  • aud (audience):接收者
  • sub (subject):令牌描述
  • iat (Issued At):签发时间
  • exp (expiration time):过期时间
  • nbf (Not Before):生效时间
  • jti (JWT ID):令牌编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

SIGNATURE

由于Header头部和载荷Payload的数据是使用Base64Url进行的编码,因此,可以这么说,只要你拿到了这两部分,就能知道,这两部分的内容,因此,在Payload载荷中是不可以存放用户密码的,同时,为了防止别人造假这两部分的数据,JWT规定第三部分是一个签名,这个签名是通过使用Header头部定义签名算法的类型,对前两部分进行加密,由于前两部分很容易获得,因此,我们还需要加入一个别人不知道的密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后使用Header里面指定的签名算法(默认是HMAC SHA256),按照下面的公式产生签名。

算出签名以后,把Header、Payload、Signature三个部分拼成一个字符串,每个部分之间用点(.)分隔,就可以返回给用户。

1.2.3、JWT的特点

  1. JWT 默认是不加密,但也是可以加密的,生成原始 Token 以后,可以用密钥再加密一次。
  2. JWT 不加密的情况下,不能将秘密数据写入 JWT。
  3. JWT 不仅可以用于认证,也可以用于交换信息,有效使用 JWT,可以降低服务器查询数据库的次数。
  4. JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  5. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  6. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

1.2.4、JWT的演示

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
@Test
public void testJWTDemo() {
    
    
    JwtBuilder jwtBuilder = Jwts.builder();

    /**
     * ===================================接下来准备生成token
     */
    //设置官方规定的字段,根据需求设置
    jwtBuilder.setIssuer("曹晨磊");//令牌颁发者
    jwtBuilder.setIssuedAt(new Date());//令牌颁发时间
    jwtBuilder.setAudience("不知名的客户端");//令牌接收者
    jwtBuilder.setExpiration(new Date(System.currentTimeMillis() + 3600000));//令牌过期时间,1小时以后
    jwtBuilder.setId(UUID.randomUUID().toString());//设置令牌编号

    //设置签名算法和密钥(盐)
    jwtBuilder.signWith(SignatureAlgorithm.HS256, "123456789abcdefg");

    //设置自定义的字段,根据需求设置
    Map<String, Object> claims = new HashMap<>();
    claims.put("age", 24);
    claims.put("money", 1234);
    jwtBuilder.addClaims(claims);

    //生成一个token令牌
    String token = jwtBuilder.compact();
    System.out.println(token);

    /**
     * ===================================接下来准备解析token
     */
    Claims body = Jwts.parser().setSigningKey("123456789abcdefg").parseClaimsJws(token).getBody();
    System.out.println(body);
}
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiLmm7nmmajno4oiLCJpYXQiOjE2MTI3MDE4NzgsImF1ZCI6IuS4jeefpeWQjeeahOWuouaIt-erryIsImV4cCI6MTYxMjcwNTQ3OCwianRpIjoiYjMwOGE0OWUtZGYyNi00M2M5LThmN2UtMjA2YTVjNWI4M2RlIiwibW9uZXkiOjEyMzQsImFnZSI6MjR9.vgCFmoWKsxWaoKJqwyrcoDavQ_wmNjremjrTqkd6ZvA

{iss=曹晨磊, iat=1612701878, aud=不知名的客户端, exp=1612705478, jti=b308a49e-df26-43c9-8f7e-206a5c5b83de, money=1234, age=24}

下面这幅图是我用官网的可视化工具进行解析的:

1.2.5、JWT的隐患

从JWT生成的token组成上来看,要想避免token被伪造,主要就得看签名部分了,而签名部分又有三部分组成,其中头部和载荷的Base64Url编码,几乎是透明的,毫无安全性可言,那么最终守护token安全的重担就落在了加入的密钥(盐)上面了! 试想:如果生成token所用的盐与解析token时加入的盐是一样的。岂不是类似于中国人民银行把人民币防伪技术公开了?大家可以用这个盐来解析token,就能用来伪造token。 这时,我们就需要对盐采用非对称加密的方式进行加密,以达到生成token与校验token方所用的盐不一致的安全效果!

1.3、RSA

1.3.1、RSA的介绍

1976年,两位美国计算机学家Whitfield Diffie和Martin Hellman,提出了一种崭新构思,可以在不直接传递密钥的情况下,完成解密,这被称为"Diffie-Hellman密钥交换算法"。这个算法启发了其他科学家,人们认识到,加密和解密可以使用不同的规则,只要这两种规则之间存在某种对应关系即可,这样就避免了直接传递密钥,而这种新的加密模式被称为"非对称加密算法"。

RSA是1977年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的,当时他们三人都在麻省理工学院工作,RSA就是他们三人姓氏开头字母拼在一起组成的 。从那时直到现在,RSA算法一直是最广为使用的"非对称加密算法"。毫不夸张地说,只要有计算机网络的地方,就有RSA算法。

这种算法非常可靠,密钥越长,它就越难破解。根据已经披露的文献,目前被破解的最长RSA密钥是768个二进制位。也就是说,长度超过768位的密钥,还无法破解(至少没人公开宣布)。因此可以认为,1024位的RSA密钥基本安全,2048位的密钥极其安全。

1.3.2、公钥私钥的介绍

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

我们假设,张三有两把钥匙,一把是公钥,另一把是私钥。

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

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

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

张三给李四回信,决定采用"数字签名"。他写完后先用Hash函数,生成信件的摘要(digest)。然后使用张三自己的私钥进行加密得到签名,张三将这个签名,附在信件上面,一起发给李四,类似JWT。

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

最终总结:既然是加密,那肯定是不希望别人知道我的消息,所以只有我才能解密,所以可得出公钥负责加密,私钥负责解密;同理,既然是签名,那肯定是不希望有人冒充我发消息,只有我才能发布这个签名,所以可得出私钥负责签名,公钥负责验证

1.3.3、公钥私钥的生成

@Test
public void testGenerateKeyPair() throws Exception {
    
    
    // 定义密钥
    String secret = "123456";

    // 固定格式
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    SecureRandom secureRandom = new SecureRandom(secret.getBytes());
    keyPairGenerator.initialize(2048, secureRandom);

    // 生成一对公钥和私钥,KeyPair内部就是由PublicKey和PrivateKey组成
    KeyPair keyPair = keyPairGenerator.genKeyPair();

    // 获取公钥并对公钥进行Base64编码(编码后方便查看,你不编码啥都看不懂)
    byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
    publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
    System.out.println("公钥Base64编码后:" + new String(publicKeyBytes));

    // 获取私钥并对私钥进行Base64编码(编码后方便查看,你不编码啥都看不懂)
    byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
    privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
    System.out.println("私钥Base64编码后:" + new String(privateKeyBytes));
}
公钥Base64编码后:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmtyGIVnd2eDDN+3wQebfUjWd2KtKfuy50T1is1eNkcmI72aYW90FJhgI4vdiNW5G6XDW2MWgIRevqkNZO4tn3oJ5rFEcu2bSY2DuIaDiWdgUI2A1N5JAweZ5GVlxnfqMDMKvI9qT9aEUQanNqAgONtf4AofCiJt8N8ZyzwMiiD7YKjx19Njdwc3MNMscUC4Bcx8QrLcn5wQC27hhTgvcvj09e9V8FlibowSK+nURiQAfSQqMQ1SZNIM0WSobLDTZhgYD/5j/SPh3wok1ne2pYAmj01K8EgvQr/k/Lk9nIfs41FRLljlSbWTOnr2nb7BHmMbKeYG+nlZm7SIBV2tKvwIDAQAB
私钥Base64编码后:MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa3IYhWd3Z4MM37fBB5t9SNZ3Yq0p+7LnRPWKzV42RyYjvZphb3QUmGAji92I1bkbpcNbYxaAhF6+qQ1k7i2fegnmsURy7ZtJjYO4hoOJZ2BQjYDU3kkDB5nkZWXGd+owMwq8j2pP1oRRBqc2oCA421/gCh8KIm3w3xnLPAyKIPtgqPHX02N3Bzcw0yxxQLgFzHxCstyfnBALbuGFOC9y+PT171XwWWJujBIr6dRGJAB9JCoxDVJk0gzRZKhssNNmGBgP/mP9I+HfCiTWd7algCaPTUrwSC9Cv+T8uT2ch+zjUVEuWOVJtZM6evadvsEeYxsp5gb6eVmbtIgFXa0q/AgMBAAECggEASmU+mq8NgSoVHr1T+pTrHBdd6UUA2NDow7h1viqFfFARVNE4yIj5fD93pXGq4HhF4MewrxrhvoQeg/Eu4Qgrsh2ETl/5KZ5P3CYowEcF9ptzsTr61eOQ8JXD/4WUq4w907ODZ/oNsqbbkF/+yIZ2Laq7HpwRvIbVugXACes7n6+sn2SduP3uMFMPvzF12EbJUVsw/oKxAhrHg5QZOdfvQbXRlv0SK2wqH7Ti0TWlhr+QgRLLbJEEk7mGxqx8HhyXhljkueKLfSx+nmH+QdmrvJezY2EdAAwzojKq8FqeZEUBQCnhUk9tMyXuyrfn8TW4C1x//zLitngMi5vNhwxaYQKBgQD7m6+lu+tq6qLT8EyjgnMoXsCEPrEF9YyjN0mQMS5+6qO2u/riZ5um8hak2NZYTVyvXAQ0GSl5DdDiDOfUmGTzkB6VWNF+nAzQkMQfamw66i7rSaoFoSA5pnkBxz6lydkB1/OsBB/dj90i0Ti0v/SmITynwWsU5qU711EtF+K39QKBgQCdkIYjBD2gx3scBrAQMNv6OIhKdNL5vIz9PJXfRxJv/o5HUfbzxXQnTNwOFjuRGDXeTwbUBeZstStKyHnOBUi+8JlhjdbHKIN6nM2JsmJTQGOE1NGoaeKdpxAvETiCpWqquxugetafaz7+5cK5C0aY+CMJzsy4Jbt8j8/ov3grYwKBgQDVWsZOHpTZW8/pMip6uIKYKAjN2y9XY0n3mUlK+Tl5K9TZfnuXAs5teXmUHb9cr3U5yihSWUfeu8V1+gWYNAXet0YH1III/6CqNyfnj+Ho724L3LJNBb2CxVR1GpRYF1pqAspBAlpXEcgt3wZb1y5ItYRuqEf6OD7DCKlwOIHrBQKBgG/3YoqBmfWlq4Mn8Xcf8UHnaFpYmA+lgB74LZxDmgOBxcNCqJVjy/2dbYaJH/0kUitOxxBlvO+k8kWrHntbX+1ndedP7r8JuByqTpi57YsxZ0beILpnvATB0gtQVnLob1sxqRkqEVep01M5HF14eMt9EREIJov5LDkAzQKdBRz3AoGBAO7IOzHp/c8pdsVpWdZNdBXPGZTlFHm0V3MemsgOad00ly11NQg8Nh5l2JXF+iDlgqepXJjoSwk5tVvxVB0MrDS1/efFVHyt8PRR/RbNilNSBjQRkKdBt8mdWEpJpiAPBhr4ln04odTsY/QTFPPrHMcu/pnvHS+NIvPHH84YCF9K

1.4、SSO

1.4.1、SSO的介绍

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

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

1.4.2、SSO的实现

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

  1. Shiro
  2. CAS
  3. Spring Security CAS
  4. Spring Security JWT
  5. Spring Security OAuth2.0

第二章 OAuth2.0的介绍篇

2.1、OAuth2.0的概述

OAuth是Open Authorization的简写,OAuth协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAuth的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAuth是安全的。

OAuth2.0是OAuth协议的延续版本,但不向前兼容(即完全废止了OAuth1.0)。

那么,我们接下来的学习目标是什么,可以参考如下学习列表:

  1. 学习精确到按钮级别的权限控制管理系统的设计(这一块不算是OAuth2.0的内容)
  2. 学习如何为第三方应用授予访问系统资源的权限(类似实现网站上的QQ快速登录)
  3. 学习如何在微服务环境下实现统一单点登录功能
  4. 解决单点登录后如何实现服务与服务之间的调用

2.2、OAuth2.0的模式

2.2.1、授权码模式

模式名称

authorization code

使用流程

说明:【A服务客户端】需要用到【B服务资源服务】中的资源。

  • 第一步:【A服务客户端】将用户自动导航到【B服务认证服务】,这一步用户需要提供一个回调地址,以备 【B服务认证服务】返回授权码使用。
  • 第二步:用户点击授权按钮表示让【A服务客户端】使用【B服务资源服务】,这一步需要用户登录B服务,也就是说用户要事先具有B服务的使用权限。
  • 第三步:【B服务认证服务】生成授权码,授权码将通过第一步提供的回调地址,返回给【A服务客户端】。 注意这个授权码并非通行【B服务资源服务】的通行凭证。
  • 第四步:【A服务认证服务】携带上一步得到的授权码向【B服务认证服务】发送请求,获取通行凭证token。
  • 第五步:【B服务认证服务】给【A服务认证服务】返回令牌token和更新令牌refresh token。

使用场景

授权码模式是OAuth2.0中最安全最完善的一种模式,应用场景最广泛,可以实现服务之间的调用,常见的微信,QQ等第三方登录也可采用这种方式实现。

2.2.2、简化模式

模式名称

implicit

使用流程

说明:简化模式中没有【A服务认证服务】这一部分,全部由【A服务客户端】与B服务交互,整个过程不再有授权码,token直接暴露在浏览器。

  • 第一步:【A服务客户端】将用户自动导航到【B服务认证服务】,这一步用户需要提供一个回调地址,以备 【B服务认证服务】返回token使用,还会携带一个【A服务客户端】的状态标识state。
  • 第二步:用户点击授权按钮表示让【A服务客户端】使用【B服务资源服务】,这一步需要用户登录B服务,也就是说用户要事先具有B服务的使用权限。
  • 第三步:【B服务认证服务】生成通行令牌token,token将通过第一步提供的回调地址,返回给【A服务客户端】。

使用场景

适用于A服务没有服务器的情况。比如:纯手机小程序,JavaScript语言实现的网页插件等。

2.2.3、密码模式

模式名称

resource owner password credentials

使用流程

  • 第一步:直接告诉【A服务客户端】自己的【B服务认证服务】的用户名和密码。
  • 第二步:【A服务客户端】携带【B服务认证服务】的用户名和密码向【B服务认证服务】发起请求获取 token。
  • 第三步:【B服务认证服务】给【A服务客户端】颁发token。

使用场景

此种模式虽然简单,但是用户将B服务的用户名和密码暴露给了A服务,需要两个服务信任度非常高才能使用。

2.2.4、客户端模式

模式名称

client credentials

使用流程

说明:这种模式其实已经不太属于OAuth2.0的范畴了。A服务完全脱离用户,以自己的身份去向B服务索取token。换言之,用户无需具备B服务的使用权也可以。完全是A服务与B服务内部的交互,与用户无关了。

  • 第一步:A服务向B服务索取token。
  • 第二步:B服务返回token给A服务。

使用场景

A服务本身需要B服务资源,与用户无关。

第三章 OAuth2.0的权限控制

3.1、项目的准备与介绍

请到配套资料中的01-基础代码中找到对应的工程代码,并使用idea打开这个工程,如果一切正常,那么你将看到如下界面:

为了学习的方便,我特意重新建了这个工程,这里边有四个项目,他们基本上都是空的,我已经把要用到的相关依赖、配置文件、包结构都准备好了,如果你打开查看,基本都能看懂 ,并没有什么特别的代码,并且这四个项目,在学习的前期,我们基本上就会用到其中的一到两个,oauth2-server-eureka1000注册中心我们只要单纯的启动就可以了,本章也不对注册中心进行介绍,重点学习将在oauth2-server-auth1001oauth2-resource-order1002工程中。

我们本章将要学习精确到按钮级别的权限控制管理系统的设计,大部分都是代码,而且涉及到的语句都是最基本的增删改查,相信不难理解。

3.2、数据库的数据导入

我们采用的数据库是mysql 5.5,我建议你和我保持一致,打开你的图形化界面工具,运行以下sql语句:

/*创建数据库*/
CREATE DATABASE `spring-cloud-oauth2`;

/*使用数据库*/
USE `spring-cloud-oauth2`;

/*导入用户表*/
DROP TABLE IF EXISTS `sys_user` ;
CREATE TABLE `sys_user` (
  `id` INT (11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
  `username` VARCHAR (64) NOT NULL COMMENT '用户姓名',
  `password` VARCHAR (64) NOT NULL COMMENT '用户密码',
  `avatar` VARCHAR (128) NOT NULL COMMENT '用户头像',
  `mobile` VARCHAR (64) NOT NULL COMMENT '用户手机',
  `email` VARCHAR (64) NOT NULL COMMENT '用户邮箱',
  `status` INT (1) NOT NULL DEFAULT '0' COMMENT '用户状态',
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1003 DEFAULT CHARSET=utf8;

/*导入用户数据*/
INSERT  INTO `sys_user`(`id`,`username`,`password`,`avatar`,`mobile`,`email`,`status`) 
VALUES (1000,'zhangsan','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',
'https://qlogo2.store.qq.com/qzone/774908833/774908833/100','15633029014','[email protected]',0);
INSERT  INTO `sys_user`(`id`,`username`,`password`,`avatar`,`mobile`,`email`,`status`) 
VALUES (1001,'lisi','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',
'https://qlogo2.store.qq.com/qzone/774908833/774908833/100','15633029014','[email protected]',0);
INSERT  INTO `sys_user`(`id`,`username`,`password`,`avatar`,`mobile`,`email`,`status`) 
VALUES (1002,'wangwu','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',
'https://qlogo2.store.qq.com/qzone/774908833/774908833/100','15633029014','[email protected]',0);

/*导入角色表*/
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
  `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '角色编号',
  `name` VARCHAR(64) NOT NULL COMMENT '角色名称',
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1003 DEFAULT CHARSET=utf8;

/*导入角色数据*/
INSERT  INTO `sys_role`(`id`,`name`) VALUES (1000,'系统管理员');
INSERT  INTO `sys_role`(`id`,`name`) VALUES (1001,'订单管理员');
INSERT  INTO `sys_role`(`id`,`name`) VALUES (1002,'商品管理员');

/*导入菜单表*/
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
  `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '菜单编号',
  `name` VARCHAR(64) DEFAULT NULL COMMENT '菜单名称',
  `code` VARCHAR(64) DEFAULT NULL COMMENT '菜单权限',
  `type` INT(11) DEFAULT NULL COMMENT '菜单类型(0:目录、1:页面、2:按钮)',
  `icon` VARCHAR(64) DEFAULT NULL COMMENT '菜单图标',
  `url` VARCHAR(64) DEFAULT NULL COMMENT '菜单地址',
  `level` INT(11) DEFAULT NULL COMMENT '菜单级别(用于快速区分当前菜单层级)',
  `path` VARCHAR(256) DEFAULT NULL COMMENT '菜单路径(用于快速找到当前菜单祖辈)',
  `sort` INT(11) DEFAULT NULL COMMENT '菜单排序',
  `status` INT(11) DEFAULT '0' COMMENT '菜单状态(0:正常、1:禁用)',
  `parent_id` INT(11) DEFAULT NULL COMMENT '上级菜单',
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1028 DEFAULT CHARSET=utf8;

/*导入菜单数据*/
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1000,'系统','system',0,'el-icon-folder',NULL,1,NULL,1,0,0);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1001,'用户管理','userMgr',1,'el-icon-menu',NULL,2,NULL,1,0,1000);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1002,'角色管理','roleMgr',1,'el-icon-menu',NULL,2,NULL,2,0,1000);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1003,'菜单管理','menuMgr',1,'el-icon-menu',NULL,2,NULL,3,0,1000);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1004,'用户管理:查询','userMgr:find',2,'el-icon-search',NULL,3,NULL,1,0,1001);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1005,'用户管理:新增','userMgr:add',2,'el-icon-plus',NULL,3,NULL,2,0,1001);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1006,'用户管理:删除','userMgr:delete',2,'el-icon-delete',NULL,3,NULL,3,0,1001);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1007,'用户管理:修改','userMgr:update',2,'el-icon-edit',NULL,3,NULL,4,0,1001);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1008,'角色管理:查询','roleMgr:find',2,'el-icon-search',NULL,3,NULL,1,0,1002);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1009,'角色管理:新增','roleMgr:add',2,'el-icon-plus',NULL,3,NULL,2,0,1002);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1010,'角色管理:删除','roleMgr:delete',2,'el-icon-delete',NULL,3,NULL,3,0,1002);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1011,'角色管理:修改','roleMgr:update',2,'el-icon-edit',NULL,3,NULL,4,0,1002);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1012,'菜单管理:查询','menuMgr:find',2,'el-icon-search',NULL,3,NULL,1,0,1003);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1013,'菜单管理:新增','menuMgr:add',2,'el-icon-plus',NULL,3,NULL,2,0,1003);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1014,'菜单管理:删除','menuMgr:delete',2,'el-icon-delete',NULL,3,NULL,3,0,1003);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1015,'菜单管理:修改','menuMgr:update',2,'el-icon-edit',NULL,3,NULL,4,0,1003);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1016,'订单','order',0,'el-icon-folder',NULL,1,NULL,2,0,0);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1017,'订单管理','orderMgr',1,'el-icon-menu',NULL,2,NULL,1,0,1016);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1018,'订单管理:查询','orderMgr:find',2,'el-icon-search',NULL,3,NULL,1,0,1017);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1019,'订单管理:新增','orderMgr:add',2,'el-icon-plus',NULL,3,NULL,2,0,1017);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1020,'订单管理:删除','orderMgr:delete',2,'el-icon-delete',NULL,3,NULL,3,0,1017);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1021,'订单管理:修改','orderMgr:update',2,'el-icon-edit',NULL,3,NULL,4,0,1017);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1022,'商品','goods',0,'el-icon-folder',NULL,1,NULL,3,0,0);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1023,'商品管理','goodsMgr',1,'el-icon-menu',NULL,2,NULL,1,0,1022);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1024,'商品管理:查询','goodsMgr:find',2,'el-icon-search',NULL,3,NULL,1,0,1023);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1025,'商品管理:新增','goodsMgr:add',2,'el-icon-plus',NULL,3,NULL,2,0,1023);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1026,'商品管理:删除','goodsMgr:delete',2,'el-icon-delete',NULL,3,NULL,3,0,1023);
INSERT  INTO `sys_menu`(`id`,`name`,`code`,`type`,`icon`,`url`,`level`,`path`,`sort`,`status`,`parent_id`) VALUES (1027,'商品管理:修改','goodsMgr:update',2,'el-icon-edit',NULL,3,NULL,4,0,1023);

/*导入用户与角色中间表*/
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
  `uid` INT(11) NOT NULL COMMENT '用户编号',
  `rid` INT(11) NOT NULL COMMENT '角色编号',
  PRIMARY KEY (`uid`,`rid`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

/*导入用户与角色中间表数据*/
INSERT  INTO `sys_user_role`(`uid`,`rid`) VALUES (1000,1000);
INSERT  INTO `sys_user_role`(`uid`,`rid`) VALUES (1000,1001);
INSERT  INTO `sys_user_role`(`uid`,`rid`) VALUES (1000,1002);
INSERT  INTO `sys_user_role`(`uid`,`rid`) VALUES (1001,1001);
INSERT  INTO `sys_user_role`(`uid`,`rid`) VALUES (1002,1002);

/*导入角色与菜单中间表*/
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
  `rid` INT(11) NOT NULL COMMENT '角色编号',
  `mid` INT(11) NOT NULL COMMENT '菜单编号',
  PRIMARY KEY (`rid`,`mid`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

/*导入角色与菜单中间表数据*/
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1000);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1001);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1002);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1003);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1004);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1005);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1006);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1007);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1008);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1009);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1010);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1011);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1012);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1013);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1014);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1000,1015);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1001,1016);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1001,1017);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1001,1018);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1001,1019);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1001,1020);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1001,1021);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1002,1022);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1002,1023);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1002,1024);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1002,1025);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1002,1026);
INSERT  INTO `sys_role_menu`(`rid`,`mid`) VALUES (1002,1027);

数据导入完成以后,请检查oauth2-server-auth1001oauth2-resource-order1002oauth2-resource-goods1003这三个项目的数据源的配置。

3.3、数据库表设计架构

在初始化的数据中,有三个用户分别是:zhangsanlisiwangwu,他们三个用户分别具有以下的角色关系:

  • zhangsan:系统管理员、订单管理员、商品管理员
  • lisi:订单管理员
  • wangwu:商品管理员

在初始化的数据中,有三个角色分别是:系统管理员订单管理员商品管理员,他们三个角色分别具有以下的菜单权限关系:

  • 系统管理员:用户管理(增删改查)、角色管理(增删改查)、菜单管理(增删改查)
  • 订单管理员:订单管理(增删改查)
  • 商品管理员:商品管理(增删改查)

3.4、Domain的编写

首先我们需要导入spring-cloud-starter-oauth2的依赖文件,请把以下依赖拷贝到oauth2-server-auth1001中。

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

我们需要编写实体来与数据库中的字段进行对应,但是为了后边的授权更加方便,我们分别继承Spring Security特有的类,在他们的基础上进行拓展。

com.caochenlei.domain.SysMenu

@Data
public class SysMenu implements GrantedAuthority {
    
    
    private Integer id;
    private String name;
    private String code;
    private Integer type;
    private String icon;
    private String url;
    private Integer level;
    private String path;
    private Integer sort;
    private Integer status;
    private Integer parent_id;

    @Override
    public String getAuthority() {
    
    
        return code;
    }
}

com.caochenlei.domain.SysRole

@Data
public class SysRole implements Serializable {
    
    
    private Integer id;
    private String name;
    private List<SysMenu> sysMenus;
}

com.caochenlei.domain.SysUser

@Data
public class SysUser implements UserDetails {
    
    
    private Integer id;
    private String username;
    private String password;
    private String avatar;
    private String mobile;
    private String email;
    private Integer status;
    private List<SysRole> sysRoles;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        List<SysMenu> authorities = new ArrayList<>();

        for (SysRole sysRole : sysRoles) {
    
    
            List<SysMenu> sysMenus = sysRole.getSysMenus();
            authorities.addAll(sysMenus);
        }

        return authorities;
    }

    /**
     * 是否账号已过期
     */
    @Override
    public boolean isAccountNonExpired() {
    
    
        return status != 1;
    }

    /**
     * 是否账号已被锁
     */
    @Override
    public boolean isAccountNonLocked() {
    
    
        return status != 2;
    }

    /**
     * 是否账号已禁用
     */
    @Override
    public boolean isEnabled() {
    
    
        return status != 3;
    }

    /**
     * 是否密码已过期
     */
    @Override
    public boolean isCredentialsNonExpired() {
    
    
        return status != 4;
    }
}

3.5、Mapper的编写

com.caochenlei.mapper.SysMenuMapper

@Mapper
public interface SysMenuMapper {
    
    
    //根据角色编号查询菜单列表
    @Select("select * from `sys_menu` where id in (" +
            "  select mid from `sys_role_menu` where rid = #{rid}" +
            ")")
    @Results({
    
    
            //主键字段映射,property代表Java对象属性,column代表数据库字段
            @Result(property = "id", column = "id", id = true),
            //普通字段映射,property代表Java对象属性,column代表数据库字段
            @Result(property = "name", column = "name"),
            @Result(property = "code", column = "code"),
            @Result(property = "type", column = "type"),
            @Result(property = "icon", column = "icon"),
            @Result(property = "url", column = "url"),
            @Result(property = "level", column = "level"),
            @Result(property = "path", column = "path"),
            @Result(property = "sort", column = "sort"),
            @Result(property = "status", column = "status"),
            @Result(property = "parent_id", column = "parent_id")
    })
    List<SysMenu> findByRid(Integer rid);
}

com.caochenlei.mapper.SysRoleMapper

@Mapper
public interface SysRoleMapper {
    
    
    //根据用户编号查询角色列表
    @Select("select * from `sys_role` where id in (" +
            "  select rid from `sys_user_role` where uid = #{uid}" +
            ")")
    @Results({
    
    
            //主键字段映射,property代表Java对象属性,column代表数据库字段
            @Result(property = "id", column = "id", id = true),
            //普通字段映射,property代表Java对象属性,column代表数据库字段
            @Result(property = "name", column = "name"),
            //菜单列表映射,根据角色id查询该用户所对应的菜单列表sysMenus
            @Result(property = "sysMenus", column = "id",
                    javaType = List.class,
                    many = @Many(select = "com.caochenlei.mapper.SysMenuMapper.findByRid")
            )
    })
    List<SysRole> findByUid(Integer uid);
}

com.caochenlei.mapper.SysUserMapper

@Mapper
public interface SysUserMapper {
    
    
    //根据用户名称查询用户信息
    @Select("select * from `sys_user` where `username` = #{username}")
    @Results({
    
    
            //主键字段映射,property代表Java对象属性,column代表数据库字段
            @Result(property = "id", column = "id", id = true),
            //普通字段映射,property代表Java对象属性,column代表数据库字段
            @Result(property = "username", column = "username"),
            @Result(property = "password", column = "password"),
            @Result(property = "avatar", column = "avatar"),
            @Result(property = "mobile", column = "mobile"),
            @Result(property = "email", column = "email"),
            @Result(property = "status", column = "status"),
            //角色列表映射,根据用户id查询该用户所对应的角色列表sysRoles
            @Result(property = "sysRoles", column = "id",
                    javaType = List.class,
                    many = @Many(select = "com.caochenlei.mapper.SysRoleMapper.findByUid")
            )
    })
    SysUser findByUsername(String username);
}

3.6、Service 的编写

com.caochenlei.service.SysUserDetailsService

public interface SysUserDetailsService extends UserDetailsService {
    
    

}

com.caochenlei.service.impl.SysUserDetailsServiceImpl

@Service
public class SysUserDetailsServiceImpl implements SysUserDetailsService {
    
    
    //(required = false)可以不写,去掉会报红色波浪线
    @Autowired(required = false)
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        //根据用户名去数据库中查询指定用户,这就要保证数据库中的用户的名称必须唯一,否则将会报错
        SysUser sysUser = sysUserMapper.findByUsername(username);
        //如果没有查询到这个用户,说明数据库中不存在此用户,认证失败,此时需要抛出用户账户不存在
        if (sysUser == null) {
    
    
            throw new UsernameNotFoundException("user not exist.");
        }
        return sysUser;
    }
}

3.7、核心的配置对象

com.caochenlei.config.WebSecurityConfig

@Configuration//说明这是一个配置类
@EnableWebSecurity//开启Web安全保护
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法权限控制
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    @Autowired
    private SysUserDetailsService userDetailsService;

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
    
    
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationProvider daoAuthenticationProvider() {
    
    
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);//指明认证详情的服务
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());//指明密码的加密方式
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);//开启用户找不到异常
        return daoAuthenticationProvider;
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.authenticationProvider(daoAuthenticationProvider());//配置认证提供者
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
    
    
        http.authorizeRequests()
                .anyRequest().authenticated()//所有请求都需要验证
                .and().formLogin().permitAll()//表单登录我们要放行
                .and().csrf().disable();//禁用csrf跨站保护
    }
}

3.8、改造控制层方法

com.caochenlei.controller.OrderController

@RestController
@PreAuthorize("hasAuthority('orderMgr')")
@RequestMapping("/order")
public class OrderController {
    
    
    //查询所有
    @PreAuthorize("hasAuthority('orderMgr:find')")
    @RequestMapping("/findAll")
    public String findAll() {
    
    
        return "order findAll ...";
    }

    //分页查询
    @PreAuthorize("hasAuthority('orderMgr:find')")
    @RequestMapping("/findPage")
    public String findPage() {
    
    
        return "order findPage ...";
    }

    //主键查询
    @PreAuthorize("hasAuthority('orderMgr:find')")
    @RequestMapping("/findById")
    public String findById() {
    
    
        return "order findById ...";
    }

    //新增订单
    @PreAuthorize("hasAuthority('orderMgr:add')")
    @RequestMapping("/add")
    public String add() {
    
    
        return "order add ...";
    }

    //删除订单
    @PreAuthorize("hasAuthority('orderMgr:delete')")
    @RequestMapping("/delete")
    public String delete() {
    
    
        return "order delete ...";
    }

    //修改订单
    @PreAuthorize("hasAuthority('orderMgr:update')")
    @RequestMapping("/update")
    public String update() {
    
    
        return "order update ...";
    }
}

com.caochenlei.controller.GoodstController

@RestController
@PreAuthorize("hasAuthority('goodsMgr')")
@RequestMapping("/goods")
public class GoodstController {
    
    
    //查询所有
    @PreAuthorize("hasAuthority('goodsMgr:find')")
    @RequestMapping("/findAll")
    public String findAll() {
    
    
        return "goods findAll ...";
    }

    //分页查询
    @PreAuthorize("hasAuthority('goodsMgr:find')")
    @RequestMapping("/findPage")
    public String findPage() {
    
    
        return "goods findPage ...";
    }

    //主键查询
    @PreAuthorize("hasAuthority('goodsMgr:find')")
    @RequestMapping("/findById")
    public String findById() {
    
    
        return "goods findById ...";
    }

    //新增订单
    @PreAuthorize("hasAuthority('goodsMgr:add')")
    @RequestMapping("/add")
    public String add() {
    
    
        return "goods add ...";
    }

    //删除订单
    @PreAuthorize("hasAuthority('goodsMgr:delete')")
    @RequestMapping("/delete")
    public String delete() {
    
    
        return "goods delete ...";
    }

    //修改订单
    @PreAuthorize("hasAuthority('goodsMgr:update')")
    @RequestMapping("/update")
    public String update() {
    
    
        return "goods update ...";
    }
}

3.9、启动项目并测试

(1)首先启动项目:oauth2-server-eureka1000

(2)然后启动项目:oauth2-server-auth1001

(3)查看注册中心:http://localhost:1000/

(4)登录用户李四:http://localhost:1001/login,账户:lisi,密码:123456,登录成功会报错不用管,测试地址:http://localhost:1001/order/findAll

(5)登录用户王五:http://localhost:1001/login,账户:wangwu,密码:123456,登录成功会报错不用管,测试地址:http://localhost:1001/order/findAll

(6)我们把之前测试的结果总结一下

  1. 登录成功会报错,这是因为登录成功后,默认会跳转到首页,可是我们根本没有首页,所以会报错,这不是我们学习的重点,我们暂时忽略即可。
  2. 同一个地址,李四能访问,但是王五不能访问,这是因为李四有订单管理的所有菜单权限,而王五只有商品管理的所有菜单权限,要想实现前端按钮级别的的控制,前端的按钮一般是对应后端的一个功能,前端的页面一般是对应后端的一个类,这就是按钮级别的权限管理系统的设计与实现。

第四章 OAuth2.0第三方授权

4.1、搭建认证中心

4.1.1、注入认证管理器

我们的OAuth2.0的实现是基于Spring Security框架的,因此,我们必须要用到Spring Security的认证管理器AuthenticationManager,具体做法如下:

@Configuration//说明这是一个配置类
@EnableWebSecurity//开启Web安全保护
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法权限控制
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    ...
    ...
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
    
    
        return super.authenticationManager();
    }
}

4.1.2、创建并启用配置

com.caochenlei.config.AuthorizationServerConfig

@Configuration//说明这是一个配置类
@EnableAuthorizationServer//开启认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    

}

4.1.3、重写父类的方法

在当前类AuthorizationServerConfig按下重写父类快捷键CTRL+O,弹出对话框,选中该类的直接父类除构造方法之外的三个主要方法,然后重写即可。

@Configuration//说明这是一个配置类
@EnableAuthorizationServer//开启认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
    
        super.configure(clients);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    
    
        super.configure(endpoints);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    
    
        super.configure(security);
    }
}

以下是这三个方法的介绍,接下来的所有配置都是围绕这三个方法进行展开的,千万不要死记硬背,要知道为什么用他以及怎么用他。

  • ClientDetailsServiceConfigurer clients:用来配置第三方客户端接入本系统的信息,例如客户端id、客户端secret、授权方式等等。
  • AuthorizationServerEndpointsConfigurer endpoints:用来配置令牌的访问端点和令牌服务信息,第一步注入的认证管理器就是用在这里。
  • AuthorizationServerSecurityConfigurer security:用来配置令牌端点的安全约束。

4.1.4、配置客户端的详情

@Configuration//说明这是一个配置类
@EnableAuthorizationServer//开启认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
    
        clients.inMemory()//使用内存模式存储第三方客户端的信息,多个客户端之间使用and()方法连接,这里为了方便只写一个客户端
                .withClient("myclient1")//客户端id
                .secret(passwordEncoder.encode("123456"))//客户端密码,这里使用了加密
                .resourceIds("RESOURCE-ORDER", "RESOURCE-GOODS")//该客户端拥有的资源标识,资源标识也是自己随便定义的
                .authorizedGrantTypes(//该客户端允许的授权类型,refresh_token不是四种模式之一,仅用于刷新token
                        "authorization_code",//开启授权码模式,这个字符串就长这样是框架内部固定的
                        "implicit",//开启简化模式,这个字符串就长这样是框架内部固定的
                        "password",//开启密码模式,这个字符串就长这样是框架内部固定的
                        "client_credentials",//开启客户端模式,这个字符串就长这样是框架内部固定的
                        "refresh_token")//该字符串仅仅用于刷新token的时候会使用,跟四种模式没有关系
                .scopes("read", "write")//该客户端允许的授权范围,自己随便定义的,表示可以访问资源服务器的哪些资源
                .autoApprove(false)//是否通过,false代表需要展示授权界面,让用户自己选择是不是通过授权,类似网页上的QQ快速登录界面,可能有点简陋
                .redirectUris("http://www.baidu.com");//返回授权码的回调地址,由于现在没有第三方应用接入,为了方便测试,这里填写百度地址
    }
    ...
    ...
}

4.1.5、配置令牌访问端点

@Configuration//说明这是一个配置类
@EnableAuthorizationServer//开启认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    
    ...
    ...
    //注入认证管理器
    @Autowired
    private AuthenticationManager authenticationManager;

    //注入客户端详情服务
    @Autowired
    private ClientDetailsService clientDetailsService;

    //生成的token储存在内存中
    @Bean
    public TokenStore tokenStore() {
    
    
        return new InMemoryTokenStore();
    }

    //生成的授权码储存在内存中
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
    
    
        return new InMemoryAuthorizationCodeServices();
    }

    @Bean
    public AuthorizationServerTokenServices tokenService() {
    
    
        DefaultTokenServices service = new DefaultTokenServices();//使用默认的token服务
        service.setClientDetailsService(clientDetailsService);//客户端详情服务
        service.setTokenStore(tokenStore());//token生成以后存储在哪里
        service.setSupportRefreshToken(true);//是否支持刷新token,默认值为:false
        service.setReuseRefreshToken(false);//是否拒绝刷新token,默认值为:true
        service.setAccessTokenValiditySeconds(3600);//生成的token默认有效期为1小时,默认值为:43200
        service.setRefreshTokenValiditySeconds(7200);//刷新token的默认有效期为2小时,默认值为:2592000
        return service;
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    
    
        endpoints
                .authenticationManager(authenticationManager)//使用认证管理器
                .tokenServices(tokenService())//生成的token服务
                .authorizationCodeServices(authorizationCodeServices())//生成的授权码存储在哪里
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);//只允许post请求访问token端点
    }
    ...
    ...
}

4.1.6、配置令牌端点安全

@Configuration//说明这是一个配置类
@EnableAuthorizationServer//开启认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    
    ...
    ...
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    
    
        security
                .tokenKeyAccess("permitAll()")//oauth/token_key公开
                .checkTokenAccess("permitAll()")//oauth/check_token公开
                .allowFormAuthenticationForClients();//允许表单认证
    }
}

4.1.7、启动认证的服务器

重新启动项目:oauth2-server-auth1001

4.1.8、测试四种模式使用

4.1.8.1、测试授权码模式

首先要获取授权码,正常来说,一旦获取到授权码就应该要申请token令牌,但是为了让大家更加清楚这个流程,申请授权码和申请token令牌分开来做。

首先打开浏览器,我们要使用一个地址并携带有效参数来获取授权码,第一次获取提示你需要登录,登录以后就会提示你是否授权应用相应的访问权限,如图:

http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=code&scope=read%20write&&redirect_uri=http://www.baidu.com

第二步我们需要使用postman来发送post请求,使用授权码模式来申请token令牌,具体参数如下:

请求地址:http://localhost:1001/oauth/token

请求类型:POST

请求参数:

  • client_id:客户端id
  • client_secret:客户端密码
  • grant_type:授权类型
  • redirect_uri:回调地址
  • code:授权码
  • username:系统内某个用户名称
  • password:系统内某个用户密码

演示效果:

4.1.8.2、测试简化模式

访问地址:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=token&scope=read%20write&redirect_uri=http://www.baidu.com

4.1.8.3、测试密码模式

请求地址:http://localhost:1001/oauth/token

请求类型:POST

请求参数:

  • client_id:客户端id
  • client_secret:客户端密码
  • grant_type:授权类型
  • username:系统内某个用户名称
  • password:系统内某个用户密码

演示效果:

4.1.8.4、测试客户端模式

请求地址:http://localhost:1001/oauth/token

请求类型:POST

请求参数:

  • client_id:客户端id
  • client_secret:客户端密码
  • grant_type:授权类型

演示效果:

4.1.8.5、测试刷新Token

只有授权码模式和密码模式的返回值中含有refresh_token,因此,只有这两种模式可以进行token刷新。

请求地址:http://localhost:1001/oauth/token

请求类型:POST

请求参数:

  • client_id:客户端id
  • client_secret:客户端密码
  • grant_type:授权类型
  • refresh_token:刷新的token值,对应每次生成的refresh_token

演示效果:

4.2、搭建订单资源

4.2.1、导入相关的依赖

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

4.2.2、创建并启用配置

com.caochenlei.config.ResourceServerConfig

@Configuration//说明这是一个配置类
@EnableResourceServer//开启资源服务器
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法权限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    
    
}

4.2.3、重写父类的方法

在当前类ResourceServerConfig按下重写父类快捷键CTRL+O,弹出对话框,选中该类的直接父类除构造方法之外的两个主要方法,然后重写即可。

@Configuration//说明这是一个配置类
@EnableResourceServer//开启资源服务器
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法权限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    
    
        super.configure(resources);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
    
    
        super.configure(http);
    }
}

以下是这两个方法的介绍,接下来的所有配置都是围绕这两个方法进行展开的,千万不要死记硬背,要知道为什么用他以及怎么用他。

  • ResourceServerSecurityConfigurer resources:专门用于配置资源服务。
  • HttpSecurity http:这个方法跟Spring Security的配置一样。

4.2.4、配置资源的信息

@Configuration//说明这是一个配置类
@EnableResourceServer//开启资源服务器
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法权限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    
    @Bean
    public TokenStore tokenStore() {
    
    
        return new InMemoryTokenStore();
    }

    @Bean
    public ResourceServerTokenServices tokenService() {
    
    
        //使用远程服务请求授权服务器校验token
        RemoteTokenServices service = new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:1001/oauth/check_token");//认证服务检查token地址
        service.setClientId("myclient1");//客户端id
        service.setClientSecret("123456");//客户端密码
        return service;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    
    
        resources.resourceId("RESOURCE-ORDER")//当前资源的标识
                .tokenStore(tokenStore())//当前令牌的存储
                .tokenServices(tokenService())//令牌验证服务
                .stateless(true);//禁用当前session会话
    }
    ...
    ...
}

4.2.5、配置资源的权限

@Configuration//说明这是一个配置类
@EnableResourceServer//开启资源服务器
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法权限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    
    ...
    ...
    @Override
    public void configure(HttpSecurity http) throws Exception {
    
    
        http.authorizeRequests()
                //指定不同请求方式访问资源所需要的权限,一般查询是read,其余是write。
                .antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
                .antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
                //允许cors
                .and().cors()
                //禁用csrf
                .and().csrf().disable()
                //禁用session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

4.2.6、拷贝控制器代码

oauth2-server-auth1001OrderController拷贝到oauth2-resource-order1002controller中。

4.2.7、启动订单的资源

启动项目:oauth2-resource-order1002

4.2.8、测试权限的控制

首先来说下,这四种模式生成的token都可以用来访问资源服务,这里以密码模式为例进行演示,访问一个资源的一个控制器方法,首先要看你有没有当前这个资源的权限也就是我们之前配置的scope,如果你有当前资源的权限,我们再来看看你登录的账户到底有没有该方法的权限,这部分是根据数据库用户与角色与菜单之间的关系得出的。

首先使用李四这个账户,myclient1拥有订单资源和商品资源服务的权限,而李四账户本身有订单资源的所有方法权限,登录后获取access_token,如下:

在访问具体方法的时候,需要把token也传递过去,参数头为Authorization,参数值为Bearer+空格+access_token,访问地址是你正常业务的地址,如下:

其次使用王五这个账户,myclient1拥有订单资源和商品资源服务的权限,而王五账户本身没有订单资源的所有方法权限,登录后获取access_token,如下:

在访问具体方法的时候,需要把token也传递过去,参数头为Authorization,参数值为Bearer+空格+access_token,访问地址是你正常业务的地址,如下:

第五章 OAuth2.0对接数据库

我们现在生成的token、刷新token、授权码、授权的权限都是存放在内存中的,本章节将会介绍如何将这些信息存放到数据库中。

5.1、升级认证中心

5.1.1、导入官方数据库表

DROP TABLE IF EXISTS `oauth_client_details` ;
CREATE TABLE `oauth_client_details` (
  `client_id` VARCHAR (255) PRIMARY KEY,
  `resource_ids` VARCHAR (256),
  `client_secret` VARCHAR (256),
  `scope` VARCHAR (256),
  `authorized_grant_types` VARCHAR (256),
  `web_server_redirect_uri` VARCHAR (256),
  `authorities` VARCHAR (256),
  `access_token_validity` INTEGER,
  `refresh_token_validity` INTEGER,
  `additional_information` VARCHAR (4096),
  `autoapprove` VARCHAR (256)
) ENGINE = INNODB DEFAULT CHARSET = utf8 ;

DROP TABLE IF EXISTS `oauth_client_token` ;
CREATE TABLE `oauth_client_token` (
  `token_id` VARCHAR (256),
  `token` BLOB,
  `authentication_id` VARCHAR (255) PRIMARY KEY,
  `user_name` VARCHAR (256),
  `client_id` VARCHAR (256)
) ENGINE = INNODB DEFAULT CHARSET = utf8 ;

DROP TABLE IF EXISTS `oauth_access_token` ;
CREATE TABLE `oauth_access_token` (
  `token_id` VARCHAR (256),
  `token` BLOB,
  `authentication_id` VARCHAR (255) PRIMARY KEY,
  `user_name` VARCHAR (256),
  `client_id` VARCHAR (256),
  `authentication` BLOB,
  `refresh_token` VARCHAR (256)
) ENGINE = INNODB DEFAULT CHARSET = utf8 ;

DROP TABLE IF EXISTS `oauth_refresh_token` ;
CREATE TABLE `oauth_refresh_token` (
  `token_id` VARCHAR (256),
  `token` BLOB,
  `authentication` BLOB
) ENGINE = INNODB DEFAULT CHARSET = utf8 ;

DROP TABLE IF EXISTS `oauth_code` ;
CREATE TABLE `oauth_code` (
  `code` VARCHAR (256),
  `authentication` BLOB
) ENGINE = INNODB DEFAULT CHARSET = utf8 ;

DROP TABLE IF EXISTS `oauth_approvals` ;
CREATE TABLE `oauth_approvals` (
  `userId` VARCHAR (256),
  `clientId` VARCHAR (256),
  `scope` VARCHAR (256),
  `status` VARCHAR (10),
  `expiresAt` TIMESTAMP,
  `lastModifiedAt` TIMESTAMP
) ENGINE = INNODB DEFAULT CHARSET = utf8 ;

INSERT INTO `oauth_client_details` (
  `client_id`,
  `resource_ids`,
  `client_secret`,
  `scope`,
  `authorized_grant_types`,
  `web_server_redirect_uri`,
  `authorities`,
  `access_token_validity`,
  `refresh_token_validity`,
  `additional_information`,
  `autoapprove`
) 
VALUES
  (
    'myclient1',
    'RESOURCE-ORDER,RESOURCE-GOODS',
    '$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',
    'read,write',
    'authorization_code,implicit,password,client_credentials,refresh_token',
    'http://www.baidu.com',
    NULL,
    3600,
    7200,
    NULL,
    'false'
  ) ;

5.1.2、还原配置对象状态

@Configuration//说明这是一个配置类
@EnableAuthorizationServer//开启认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    

}

5.1.3、配置客户端的详情

@Configuration//说明这是一个配置类
@EnableAuthorizationServer//开启认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    
    //数据库连接池对象
    @Autowired
    private DataSource dataSource;

    //客户端的信息来源
    @Bean
    public JdbcClientDetailsService jdbcClientDetailsService() {
    
    
        return new JdbcClientDetailsService(dataSource);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
    
        clients.withClientDetails(jdbcClientDetailsService());
    }
    ...
    ...
}

5.1.4、配置令牌访问端点

@Configuration//说明这是一个配置类
@EnableAuthorizationServer//开启认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    
    ...
    ...
    //注入认证管理器
    @Autowired
    private AuthenticationManager authenticationManager;

    //授权码模式数据来源
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
    
    
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    //生成的token储存在数据库中
    @Bean
    public TokenStore tokenStore() {
    
    
        return new JdbcTokenStore(dataSource);
    }

    //授权信息保存在数据库中
    @Bean
    public ApprovalStore approvalStore() {
    
    
        return new JdbcApprovalStore(dataSource);
    }

    @Autowired
    private SysUserDetailsService userDetailsService;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    
    
        endpoints
                .authenticationManager(authenticationManager)//使用认证管理器
                .authorizationCodeServices(authorizationCodeServices())//授权码服务
                .tokenStore(tokenStore())//token存储在哪里
                .approvalStore(approvalStore())//授权信息存储在哪里
                .userDetailsService(userDetailsService)//使用用户详情服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);//只允许post请求访问token端点
    }
    ...
    ...
}   

5.1.5、配置令牌端点安全

@Configuration//说明这是一个配置类
@EnableAuthorizationServer//开启认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    
    ...
    ...
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    
    
        security
                .tokenKeyAccess("permitAll()")//oauth/token_key公开
                .checkTokenAccess("permitAll()")//oauth/check_token公开
                .allowFormAuthenticationForClients();//允许表单认证
    }
}

5.1.6、重启认证的服务器

重新启动项目:oauth2-server-auth1001

5.1.7、测试四种模式使用

5.1.7.1、测试授权码模式

获取授权码:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=code

获取token:http://localhost:1001/oauth/token

5.1.7.2、测试简化模式

访问地址:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=token&scope=read%20write&redirect_uri=http://www.baidu.com

5.1.7.3、测试密码模式

5.1.7.4、测试客户端模式

5.1.7.5、测试刷新token

5.2、升级订单资源

5.2.1、修改配置的对象

@Configuration//说明这是一个配置类
@EnableResourceServer//开启资源服务器
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法权限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    
    @Autowired
    private DataSource dataSource;

    @Bean
    public TokenStore tokenStore() {
    
    
        return new JdbcTokenStore(dataSource);
    }

    @Bean
    public ResourceServerTokenServices tokenService() {
    
    
        //使用远程服务请求授权服务器校验token
        RemoteTokenServices service = new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:1001/oauth/check_token");//认证服务检查token地址
        service.setClientId("myclient1");//客户端id
        service.setClientSecret("123456");//客户端密码
        return service;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    
    
        resources.resourceId("RESOURCE-ORDER")//当前资源的标识
                .tokenStore(tokenStore())//当前令牌的存储
                .tokenServices(tokenService())//令牌验证服务
                .stateless(true);//禁用当前session会话
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
    
    
        http.authorizeRequests()
                //指定不同请求方式访问资源所需要的权限,一般查询是read,其余是write。
                .antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
                .antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
                //允许cors
                .and().cors()
                //禁用csrf
                .and().csrf().disable()
                //禁用session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

5.2.2、重启订单的资源

重启订单资源oauth2-resource-order1002

5.2.3、测试权限的控制

首先使用李四这个账户,myclient1拥有订单资源和商品资源服务的权限,而李四账户本身有订单资源的所有方法权限,登录后获取access_token,如下:

在访问具体方法的时候,需要把token也传递过去,参数头为Authorization,参数值为Bearer+空格+access_token,访问地址是你正常业务的地址,如下:

其次使用王五这个账户,myclient1拥有订单资源和商品资源服务的权限,而王五账户本身没有订单资源的所有方法权限,登录后获取access_token,如下:

在访问具体方法的时候,需要把token也传递过去,参数头为Authorization,参数值为Bearer+空格+access_token,访问地址是你正常业务的地址,如下:

第六章 OAuth2.0对接公私钥

上边的架构存在一种问题,每一次去访问资源服务的时候,资源服务都需要登录到认证服务,然后对token进行校验,这样无疑降低了系统的性能,能不能我不去认证服务器认证,我也知道你这个token到底合不合法,答案肯定是有的。

我们在这里采用JWT的方式,之前也介绍过JWT,他是可以生成token的,而且,为了安全,我们建议签名算法采用私钥加密,而资源服务只需要使用公钥就可以验证这个token到底是不是合法的,这样,我们就省去了一次访问认证服务去校验token的情况,同时,由于使用了RSA的公钥和私钥,在安全上也得到了保证。

6.1、创建工具类库

把以下这个类复制到工程oauth2-server-auth1001oauth2-resource-order1002中。

com.caochenlei.utils.RsaUtils

package com.caochenlei.utils;

import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.nio.file.Files;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

/**
 * 对Rsa操作进行了简单封装
 *
 * @author CaoChenLei
 */
@Slf4j
public class RsaUtils {
    
    
    private static final int DEFAULT_KEY_SIZE = 2048;

    /**
     * 从文件中获取RSA公钥对象
     *
     * @param filename 公钥保存路径,相对于classpath
     * @return RSA公钥对象
     */
    public static RSAPublicKey getRSAPublicKey(String filename) {
    
    
        return (RSAPublicKey) getPublicKey(filename);
    }

    /**
     * 从文件中获取RSA私钥对象
     *
     * @param filename 公钥保存路径,相对于classpath
     * @return RSA私钥对象
     */
    public static RSAPrivateKey getRSAPrivateKey(String filename) {
    
    
        return (RSAPrivateKey) (getPrivateKey(filename));
    }

    /**
     * 从文件中获取公钥对象
     *
     * @param filename 公钥保存路径,相对于classpath
     * @return 公钥对象
     */
    public static PublicKey getPublicKey(String filename) {
    
    
        try {
    
    
            byte[] bytes = readFile(filename);
            return getPublicKey(bytes);
        } catch (Exception e) {
    
    
            log.error("获取公钥对象失败", e);
            return null;
        }
    }

    /**
     * 从文件中获取私钥对象
     *
     * @param filename 私钥保存路径,相对于classpath
     * @return 私钥对象
     */
    public static PrivateKey getPrivateKey(String filename) {
    
    
        try {
    
    
            byte[] bytes = readFile(filename);
            return getPrivateKey(bytes);
        } catch (Exception e) {
    
    
            log.error("获取私钥对象失败", e);
            return null;
        }
    }

    /**
     * 从文件中获取公钥对象
     *
     * @param bytes 公钥的字节数组形式
     * @return
     * @throws Exception
     */
    public static PublicKey getPublicKey(byte[] bytes) throws Exception {
    
    
        bytes = Base64.getDecoder().decode(bytes);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 从文件中获取私钥对象
     *
     * @param bytes 私钥的字节数组形式
     * @return
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
    
    
        bytes = Base64.getDecoder().decode(bytes);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 根据你指定的密文(盐),生成rsa公钥和私钥,并写入指定文件
     *
     * @param publicKeyFilename  公钥文件的路径
     * @param privateKeyFilename 私钥文件的路径
     * @param secret             生成密钥的密文
     */
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret, int keySize) throws Exception {
    
    
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    public static byte[] readFile(String fileName) throws Exception {
    
    
        return Files.readAllBytes(new File(fileName).toPath());
    }

    public static void writeFile(String destPath, byte[] bytes) throws Exception {
    
    
        File dest = new File(destPath);
        File parentFile = dest.getParentFile();
        if (!parentFile.exists()) {
    
    
            parentFile.mkdirs();
        }
        if (!dest.exists()) {
    
    
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }

    public static void main(String[] args) throws Exception {
    
    
        String publicFile = "D:\\auth_key\\rsa_key.pub";
        String privateFile = "D:\\auth_key\\rsa_key";
        String secret = "123456789";
        //向指定路径写出公钥和私钥
        generateKey(publicFile, privateFile, secret, 2048);
        System.out.println("generateKey done ...");
    }
}

6.2、生成公钥私钥

运行RsaUtilsmain方法,生成公钥和私钥,这里建议你先别改路径,这个secret也就是盐或者说密码,你可以随便改。

注意:如果报错误: 找不到或无法加载主类 com.caochenlei.utils.RsaUtils,请按快捷键CTRL+F9重新编译当前项目,然后在运行就没事了。

6.3、修改认证服务配置

com.caochenlei.config.AuthorizationServerConfig

@Configuration//说明这是一个配置类
@EnableAuthorizationServer//开启认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    
    //数据库连接池对象
    @Autowired
    private DataSource dataSource;

    //客户端的信息来源
    @Bean
    public JdbcClientDetailsService jdbcClientDetailsService() {
    
    
        return new JdbcClientDetailsService(dataSource);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
    
        clients.withClientDetails(jdbcClientDetailsService());
    }

    //注入认证管理器
    @Autowired
    private AuthenticationManager authenticationManager;

    //JWT令牌转换器
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
    
    
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        //用于生成token
        converter.setSigner(new RsaSigner(RsaUtils.getRSAPrivateKey("D:\\auth_key\\rsa_key")));
        //用于校验token(刷新token会用到,如果不设置,将会刷新失败)
        converter.setVerifier(new RsaVerifier(RsaUtils.getRSAPublicKey("D:\\auth_key\\rsa_key.pub")));
        return converter;
    }

    //JWT令牌存储策略
    @Bean
    public TokenStore tokenStore() {
    
    
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    //授权信息保存在数据库中
    @Bean
    public ApprovalStore approvalStore() {
    
    
        return new JdbcApprovalStore(dataSource);
    }

    @Autowired
    private SysUserDetailsService userDetailsService;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    
    
        endpoints
                .authenticationManager(authenticationManager)//使用认证管理器
                .accessTokenConverter(jwtAccessTokenConverter())//令牌转换器
                .tokenStore(tokenStore())//token存储在哪里
                .approvalStore(approvalStore())//授权信息存储在哪里
                .userDetailsService(userDetailsService)//使用用户详情服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);//只允许post请求访问token端点
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    
    
        security
                .tokenKeyAccess("permitAll()")//oauth/token_key公开
                .checkTokenAccess("permitAll()")//oauth/check_token公开
                .allowFormAuthenticationForClients();//允许表单认证
    }
}

修改完毕以后,我们需要重新启动oauth2-server-auth1001

6.4、修改资源服务配置

com.caochenlei.config.ResourceServerConfig

@Configuration//说明这是一个配置类
@EnableResourceServer//开启资源服务器
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法权限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
    
    
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setVerifier(new RsaVerifier(RsaUtils.getRSAPublicKey("D:\\auth_key\\rsa_key.pub")));
        return converter;
    }

    @Bean
    public TokenStore tokenStore() throws Exception {
    
    
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    
    
        resources.resourceId("RESOURCE-ORDER")//当前资源的标识
                .tokenStore(tokenStore())//当前令牌的存储
                .stateless(true);//禁用当前session会话
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
    
    
        http.authorizeRequests()
                //指定不同请求方式访问资源所需要的权限,一般查询是read,其余是write。
                .antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
                .antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
                //允许cors
                .and().cors()
                //禁用csrf
                .and().csrf().disable()
                //禁用session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

修改完毕以后,我们需要重新启动oauth2-resource-order1002

6.5、测试四种模式使用

6.5.1、测试授权码模式

获取授权码:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=code

获取token:http://localhost:1001/oauth/token

查看token:https://jwt.io/

6.5.2、测试简化模式

访问地址:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=token&scope=read%20write&redirect_uri=http://www.baidu.com

6.5.3、测试密码模式

6.5.4、测试客户端模式

6.5.5、测试刷新token

6.6、测试资源权限控制

首先使用李四这个账户,myclient1拥有订单资源和商品资源服务的权限,而李四账户本身有订单资源的所有方法权限,登录后获取access_token,如下:

在访问具体方法的时候,需要把token也传递过去,参数头为Authorization,参数值为Bearer+空格+access_token,访问地址是你正常业务的地址,如下:

其次使用王五这个账户,myclient1拥有订单资源和商品资源服务的权限,而王五账户本身没有订单资源的所有方法权限,登录后获取access_token,如下:

在访问具体方法的时候,需要把token也传递过去,参数头为Authorization,参数值为Bearer+空格+access_token,访问地址是你正常业务的地址,如下:

第七章 OAuth2.0实现单点登录

在这里,我使用一种取巧的方式开进行单点登录,既然OAuth2.0可以为第三方应用授权访问系统资源,我给当前的系统申请一个拥有所有资源权限的客户端id不就行了,这里为了省事,我还是用myclient1,在登录的时候,我们使用密码模式进行登录,但是,你不能每次让用户都自己输入客户端id和客户端密钥,我们需要写一个控制器,接收用户传递过来的账户和密码,我们内部自己发送请求,以此实现登录效果,登录成功以后返回用户数据。

7.1、注入Rest模板

com.caochenlei.OAuth2Server1001Application

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@EnableHystrix
public class OAuth2Server1001Application {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(OAuth2Server1001Application.class, args);
    }

    @Bean
    public RestTemplate restTemplate() {
    
    
        return new RestTemplate();
    }
}

7.2、创建控制器类

com.caochenlei.controller.AuthController

@RestController
public class AuthController {
    
    
    //当前认证服务器的IP地址
    @Value("${spring.cloud.client.ip-address}")
    private String authIp;

    //当前认证服务器的Port端口
    @Value("${server.port}")
    private String authPort;

    @Autowired
    private RestTemplate restTemplate;

    //声明客户端的id和secret,这里应该用配置文件方式注入进来,为了方便,我就写死了
    private String clientId = "myclient1";
    private String clientSecret = "123456";

    @RequestMapping("/user/login")
    public Map login(String username, String password) {
    
    
        //1.定义申请token的认证服务地址
        String url = "http://" + authIp + ":" + authPort + "/oauth/token";

        //2.定义头信息 (有client id 和client secr)
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        String base64 = Base64.getEncoder().encodeToString(new String(clientId + ":" + clientSecret).getBytes());
        headers.add("Authorization", "Basic " + base64);

        //3.定义请求体、有授权模式、用户的名称和密码
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        formData.add("grant_type", "password");
        formData.add("username", username);
        formData.add("password", password);

        //4.模拟浏览器发送POST请求,携带请求头和请求体到认证服务器
        /**
         * 参数1 指定要发送的请求的url
         * 参数2 指定要发送的请求的方法 PSOT
         * 参数3 指定请求实体(包含请求头和请求体数据)
         */
        HttpEntity<MultiValueMap> requestentity = new HttpEntity<>(formData, headers);
        ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestentity, Map.class);

        //5.接收到返回的响应,就是令牌的信息
        Map body = responseEntity.getBody();

        //6.自己封装数据对象,不能随便改变字段,否则刷新token会发生问题,丢失部分字段
        Map<String, Object> response = new LinkedHashMap();
        response.put("access_token", (String) body.get("access_token"));
        response.put("token_type", (String) body.get("token_type"));
        response.put("refresh_token", (String) body.get("refresh_token"));
        response.put("expires_in", (Integer) body.get("expires_in"));
        response.put("scope", (String) body.get("scope"));
        response.put("jti", (String) body.get("jti"));

        //7.返回数据
        return response;
    }
}

7.3、开放登录端口

com.caochenlei.config.WebSecurityConfig

@Configuration//说明这是一个配置类
@EnableWebSecurity//开启Web安全保护
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法权限控制
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    ...
    ...
    @Override
    public void configure(HttpSecurity http) throws Exception {
    
    
        http.authorizeRequests()
                .antMatchers("/user/login").permitAll()//放行登录请求
                .anyRequest().authenticated()//所有请求都需要验证
                .and().formLogin().permitAll()//表单登录我们要放行
                .and().csrf().disable();//禁用csrf跨站保护
    }
    ...
    ...
}

7.4、测试登录效果

重新启动:oauth2-server-auth1001

访问地址:http://localhost:1001/user/login

7.5、测试权限控制

首先使用李四这个账户,myclient1拥有订单资源和商品资源服务的权限,而李四账户本身有订单资源的所有方法权限,登录后获取access_token,如下:

在访问具体方法的时候,需要把token也传递过去,参数头为Authorization,参数值为Bearer+空格+access_token,访问地址是你正常业务的地址,如下:

其次使用王五这个账户,myclient1拥有订单资源和商品资源服务的权限,而王五账户本身没有订单资源的所有方法权限,登录后获取access_token,如下:

在访问具体方法的时候,需要把token也传递过去,参数头为Authorization,参数值为Bearer+空格+access_token,访问地址是你正常业务的地址,如下:

第八章 OAuth2.0服务调用问题

8.1、商品资源服务准备

(1)oauth2-resource-goods1003添加依赖

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

(2)拷贝oauth2-resource-order1002com.caochenlei.utils.RsaUtilsoauth2-resource-goods1003

(3)拷贝oauth2-resource-order1002com.caochenlei.config.ResourceServerConfigoauth2-resource-goods1003

修改资源名称

@Configuration//说明这是一个配置类
@EnableResourceServer//开启资源服务器
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法权限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    
    ...
    ...
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    
    
        resources.resourceId("RESOURCE-GOODS")//当前资源的标识
                .tokenStore(tokenStore())//当前令牌的存储
                .stateless(true);//禁用当前session会话
    }
    ...
    ...
}

(4)拷贝oauth2-server-auth1001com.caochenlei.controller.GoodstControlleroauth2-resource-goods1003

(5)启动oauth2-resource-goods1003

8.2、订单资源服务准备

(1)在oauth2-resource-order1002增加com.caochenlei.service.FeginGoodsService

(2)在oauth2-resource-order1002修改com.caochenlei.controller.OrderController

@RestController
@PreAuthorize("hasAuthority('orderMgr')")
@RequestMapping("/order")
public class OrderController {
    
    
    @Autowired
    private FeginGoodsService feginGoodsService;

    //查询所有
    @PreAuthorize("hasAuthority('orderMgr:find')")
    @RequestMapping("/findAll")
    public String findAll() {
    
    
        return "order findAll ...";
    }

    //分页查询
    @PreAuthorize("hasAuthority('orderMgr:find')")
    @RequestMapping("/findPage")
    public String findPage() {
    
    
        return "order findPage ...";
    }

    //主键查询
    @PreAuthorize("hasAuthority('orderMgr:find')")
    @RequestMapping("/findById")
    public String findById() {
    
    
        String feginGoodsServiceFindById = feginGoodsService.findById();
        return "order findById ... " + feginGoodsServiceFindById;
    }

    //新增订单
    @PreAuthorize("hasAuthority('orderMgr:add')")
    @RequestMapping("/add")
    public String add() {
    
    
        return "order add ...";
    }

    //删除订单
    @PreAuthorize("hasAuthority('orderMgr:delete')")
    @RequestMapping("/delete")
    public String delete() {
    
    
        return "order delete ...";
    }

    //更新订单
    @PreAuthorize("hasAuthority('orderMgr:update')")
    @RequestMapping("/update")
    public String update() {
    
    
        return "order update ...";
    }
}

(3)重启oauth2-resource-order1002

8.3、订单调用商品问题

我用自定义单点登录的方式进行登录,这里使用zhangsan来访问查找订单服务,订单服务里边会有很多商品,所以,查找订单服务又调用了根据主键查找商品服务,而且,张三拥有对订单所有方法和商品所有方法访问的权限,但是在实际调用过程中,就会报出错误,如下图:

造成上述问题的根本原因就是,资源服务器在请求的时候,需要携带token,来验证这次请求是不是合法,很显然,我们直接调用肯定会报错,我们并没有携带token,要解决也很简单,在每一次请求之前都携带上token即可。

8.4、解决服务调用问题

在Feign所有请求之前,使用拦截器进行拦截,将token头字段追加到头上就行了。

com.caochenlei.interceptor.FeignInterceptor

@Component
public class FeignInterceptor implements RequestInterceptor {
    
    
    @Override
    public void apply(RequestTemplate requestTemplate) {
    
    
        try {
    
    
            //使用RequestContextHolder工具获取request相关变量
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes != null) {
    
    
                //取出request
                HttpServletRequest request = attributes.getRequest();
                //获取所有头文件信息的key
                Enumeration<String> headerNames = request.getHeaderNames();
                if (headerNames != null) {
    
    
                    while (headerNames.hasMoreElements()) {
    
    
                        //头文件的key
                        String name = headerNames.nextElement();
                        //头文件的value
                        String values = request.getHeader(name);
                        //将令牌数据添加到头文件中
                        requestTemplate.header(name, values);
                    }
                }
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

重新启动oauth2-resource-order1002

8.5、演示最终解决效果

猜你喜欢

转载自blog.csdn.net/qq_38490457/article/details/113801009