配套资料,免费下载
链接:https://pan.baidu.com/s/1la_3-HW-UvliDRJzfBcP_w
提取码:lxfx
复制这段内容后打开百度网盘手机App,操作更方便哦
Base64是一种用64个字符来表示任意二进制数据的编码方式。
@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
JWT,全称JSON Web Tokens,官网地址:https://jwt.io,是一款出色的分布式身份校验方案,可以生成token,也可以解析检验token。
JWT由三部分组成,它们之间用点(.)连接,这三部分分别是:
注意: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个官方字段,可供选用。
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
SIGNATURE
由于Header头部和载荷Payload的数据是使用Base64Url进行的编码,因此,可以这么说,只要你拿到了这两部分,就能知道,这两部分的内容,因此,在Payload载荷中是不可以存放用户密码的,同时,为了防止别人造假这两部分的数据,JWT规定第三部分是一个签名,这个签名是通过使用Header头部定义签名算法的类型,对前两部分进行加密,由于前两部分很容易获得,因此,我们还需要加入一个别人不知道的密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后使用Header里面指定的签名算法(默认是HMAC SHA256),按照下面的公式产生签名。
算出签名以后,把Header、Payload、Signature三个部分拼成一个字符串,每个部分之间用点(.)分隔,就可以返回给用户。
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
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}
下面这幅图是我用官网的可视化工具进行解析的:
从JWT生成的token组成上来看,要想避免token被伪造,主要就得看签名部分了,而签名部分又有三部分组成,其中头部和载荷的Base64Url编码,几乎是透明的,毫无安全性可言,那么最终守护token安全的重担就落在了加入的密钥(盐)上面了! 试想:如果生成token所用的盐与解析token时加入的盐是一样的。岂不是类似于中国人民银行把人民币防伪技术公开了?大家可以用这个盐来解析token,就能用来伪造token。 这时,我们就需要对盐采用非对称加密的方式进行加密,以达到生成token与校验token方所用的盐不一致的安全效果!
1976年,两位美国计算机学家Whitfield Diffie和Martin Hellman,提出了一种崭新构思,可以在不直接传递密钥的情况下,完成解密,这被称为"Diffie-Hellman密钥交换算法"。这个算法启发了其他科学家,人们认识到,加密和解密可以使用不同的规则,只要这两种规则之间存在某种对应关系即可,这样就避免了直接传递密钥,而这种新的加密模式被称为"非对称加密算法"。
RSA是1977年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的,当时他们三人都在麻省理工学院工作,RSA就是他们三人姓氏开头字母拼在一起组成的 。从那时直到现在,RSA算法一直是最广为使用的"非对称加密算法"。毫不夸张地说,只要有计算机网络的地方,就有RSA算法。
这种算法非常可靠,密钥越长,它就越难破解。根据已经披露的文献,目前被破解的最长RSA密钥是768个二进制位。也就是说,长度超过768位的密钥,还无法破解(至少没人公开宣布)。因此可以认为,1024位的RSA密钥基本安全,2048位的密钥极其安全。
在对称加密的时代,加密和解密用的是同一个密钥,这个密钥既用于加密,又用于解密。这样做有一个明显的缺点,如果两个人之间传输文件,两个人都要知道密钥,如果是三个人呢,五个人呢?于是就产生了非对称加密,用一个密钥进行加密(公钥),用另一个密钥进行解密(私钥)。
我们假设,张三有两把钥匙,一把是公钥,另一把是私钥。
张三把公钥送给他的朋友们:李四、王五、赵六,每人一把。
李四要给张三写一封保密的信,她写完后用张三的公钥加密,就可以达到保密的效果。
张三收信后,用私钥解密,就看到了信件内容。这里要强调的是,只要张三的私钥不泄露,这封信就是安全的,即使落在别人手里,也无法解密。
张三给李四回信,决定采用"数字签名"。他写完后先用Hash函数,生成信件的摘要(digest)。然后使用张三自己的私钥进行加密得到签名,张三将这个签名,附在信件上面,一起发给李四,类似JWT。
李四收信后,取下数字签名,用张三的公钥解密,得到信件的摘要。李四再对信件本身使用Hash函数,将得到的结果,与上一步得到的摘要进行对比。如果两者一致,就证明这封信未被修改过。由此证明,这封信确实是张三发出的。
最终总结:既然是加密,那肯定是不希望别人知道我的消息,所以只有我才能解密,所以可得出公钥负责加密,私钥负责解密;同理,既然是签名,那肯定是不希望有人冒充我发消息,只有我才能发布这个签名,所以可得出私钥负责签名,公钥负责验证。
@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
假设用户访问的项目中,至少有3个微服务需要识别用户身份,如果用户访问每个微服务都登录一次就太麻烦了,为了提高用户的体验,我们需要实现让用户在一个系统中登录,其他任意受信任的系统都可以访问,这个功能就叫单点登录。
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。 SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
Java中有很多用户认证的框架都可以实现单点登录:
OAuth是Open Authorization的简写,OAuth协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAuth的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAuth是安全的。
OAuth2.0是OAuth协议的延续版本,但不向前兼容(即完全废止了OAuth1.0)。
那么,我们接下来的学习目标是什么,可以参考如下学习列表:
模式名称
authorization code
使用流程
说明:【A服务客户端】需要用到【B服务资源服务】中的资源。
使用场景
授权码模式是OAuth2.0中最安全最完善的一种模式,应用场景最广泛,可以实现服务之间的调用,常见的微信,QQ等第三方登录也可采用这种方式实现。
模式名称
implicit
使用流程
说明:简化模式中没有【A服务认证服务】这一部分,全部由【A服务客户端】与B服务交互,整个过程不再有授权码,token直接暴露在浏览器。
使用场景
适用于A服务没有服务器的情况。比如:纯手机小程序,JavaScript语言实现的网页插件等。
模式名称
resource owner password credentials
使用流程
使用场景
此种模式虽然简单,但是用户将B服务的用户名和密码暴露给了A服务,需要两个服务信任度非常高才能使用。
模式名称
client credentials
使用流程
说明:这种模式其实已经不太属于OAuth2.0的范畴了。A服务完全脱离用户,以自己的身份去向B服务索取token。换言之,用户无需具备B服务的使用权也可以。完全是A服务与B服务内部的交互,与用户无关了。
使用场景
A服务本身需要B服务资源,与用户无关。
请到配套资料中的01-基础代码
中找到对应的工程代码,并使用idea
打开这个工程,如果一切正常,那么你将看到如下界面:
为了学习的方便,我特意重新建了这个工程,这里边有四个项目,他们基本上都是空的,我已经把要用到的相关依赖、配置文件、包结构都准备好了,如果你打开查看,基本都能看懂 ,并没有什么特别的代码,并且这四个项目,在学习的前期,我们基本上就会用到其中的一到两个,oauth2-server-eureka1000
注册中心我们只要单纯的启动就可以了,本章也不对注册中心进行介绍,重点学习将在oauth2-server-auth1001
和oauth2-resource-order1002
工程中。
我们本章将要学习精确到按钮级别的权限控制管理系统的设计,大部分都是代码,而且涉及到的语句都是最基本的增删改查,相信不难理解。
我们采用的数据库是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-auth1001
、oauth2-resource-order1002
、oauth2-resource-goods1003
这三个项目的数据源的配置。
在初始化的数据中,有三个用户分别是:zhangsan
、lisi
、wangwu
,他们三个用户分别具有以下的角色关系:
在初始化的数据中,有三个角色分别是:系统管理员
、订单管理员
、商品管理员
,他们三个角色分别具有以下的菜单权限关系:
首先我们需要导入spring-cloud-starter-oauth2
的依赖文件,请把以下依赖拷贝到oauth2-server-auth1001
中。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
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;
}
}
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);
}
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;
}
}
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跨站保护
}
}
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 ...";
}
}
(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)我们把之前测试的结果总结一下
我们的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();
}
}
com.caochenlei.config.AuthorizationServerConfig
@Configuration//说明这是一个配置类
@EnableAuthorizationServer//开启认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
}
在当前类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);
}
}
以下是这三个方法的介绍,接下来的所有配置都是围绕这三个方法进行展开的,千万不要死记硬背,要知道为什么用他以及怎么用他。
@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");//返回授权码的回调地址,由于现在没有第三方应用接入,为了方便测试,这里填写百度地址
}
...
...
}
@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端点
}
...
...
}
@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();//允许表单认证
}
}
重新启动项目:oauth2-server-auth1001
首先要获取授权码,正常来说,一旦获取到授权码就应该要申请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
请求参数:
演示效果:
访问地址:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=token&scope=read%20write&redirect_uri=http://www.baidu.com
请求地址:http://localhost:1001/oauth/token
请求类型:POST
请求参数:
演示效果:
请求地址:http://localhost:1001/oauth/token
请求类型:POST
请求参数:
演示效果:
只有授权码模式和密码模式的返回值中含有refresh_token
,因此,只有这两种模式可以进行token
刷新。
请求地址:http://localhost:1001/oauth/token
请求类型:POST
请求参数:
refresh_token
演示效果:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
com.caochenlei.config.ResourceServerConfig
@Configuration//说明这是一个配置类
@EnableResourceServer//开启资源服务器
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法权限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
}
在当前类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);
}
}
以下是这两个方法的介绍,接下来的所有配置都是围绕这两个方法进行展开的,千万不要死记硬背,要知道为什么用他以及怎么用他。
Spring Security
的配置一样。@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会话
}
...
...
}
@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);
}
}
把oauth2-server-auth1001
的OrderController
拷贝到oauth2-resource-order1002
的controller
中。
启动项目:oauth2-resource-order1002
首先来说下,这四种模式生成的token都可以用来访问资源服务,这里以密码模式为例进行演示,访问一个资源的一个控制器方法,首先要看你有没有当前这个资源的权限也就是我们之前配置的scope
,如果你有当前资源的权限,我们再来看看你登录的账户到底有没有该方法的权限,这部分是根据数据库用户与角色与菜单之间的关系得出的。
首先使用李四这个账户,myclient1拥有订单资源和商品资源服务的权限,而李四账户本身有订单资源的所有方法权限,登录后获取access_token
,如下:
在访问具体方法的时候,需要把token也传递过去,参数头为Authorization
,参数值为Bearer
+空格+access_token
,访问地址是你正常业务的地址,如下:
其次使用王五这个账户,myclient1拥有订单资源和商品资源服务的权限,而王五账户本身没有订单资源的所有方法权限,登录后获取access_token
,如下:
在访问具体方法的时候,需要把token也传递过去,参数头为Authorization
,参数值为Bearer
+空格+access_token
,访问地址是你正常业务的地址,如下:
我们现在生成的token、刷新token、授权码、授权的权限都是存放在内存中的,本章节将会介绍如何将这些信息存放到数据库中。
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'
) ;
@Configuration//说明这是一个配置类
@EnableAuthorizationServer//开启认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
}
@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());
}
...
...
}
@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端点
}
...
...
}
@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();//允许表单认证
}
}
重新启动项目:oauth2-server-auth1001
获取授权码:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=code
获取token:http://localhost:1001/oauth/token
访问地址:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=token&scope=read%20write&redirect_uri=http://www.baidu.com
@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);
}
}
重启订单资源oauth2-resource-order1002
首先使用李四这个账户,myclient1拥有订单资源和商品资源服务的权限,而李四账户本身有订单资源的所有方法权限,登录后获取access_token
,如下:
在访问具体方法的时候,需要把token也传递过去,参数头为Authorization
,参数值为Bearer
+空格+access_token
,访问地址是你正常业务的地址,如下:
其次使用王五这个账户,myclient1拥有订单资源和商品资源服务的权限,而王五账户本身没有订单资源的所有方法权限,登录后获取access_token
,如下:
在访问具体方法的时候,需要把token也传递过去,参数头为Authorization
,参数值为Bearer
+空格+access_token
,访问地址是你正常业务的地址,如下:
上边的架构存在一种问题,每一次去访问资源服务的时候,资源服务都需要登录到认证服务,然后对token进行校验,这样无疑降低了系统的性能,能不能我不去认证服务器认证,我也知道你这个token到底合不合法,答案肯定是有的。
我们在这里采用JWT的方式,之前也介绍过JWT,他是可以生成token的,而且,为了安全,我们建议签名算法采用私钥加密,而资源服务只需要使用公钥就可以验证这个token到底是不是合法的,这样,我们就省去了一次访问认证服务去校验token的情况,同时,由于使用了RSA的公钥和私钥,在安全上也得到了保证。
把以下这个类复制到工程oauth2-server-auth1001
和oauth2-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 ...");
}
}
运行RsaUtils
的main
方法,生成公钥和私钥,这里建议你先别改路径,这个secret也就是盐或者说密码,你可以随便改。
注意:如果报错误: 找不到或无法加载主类 com.caochenlei.utils.RsaUtils,请按快捷键
CTRL+F9
重新编译当前项目,然后在运行就没事了。
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
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
获取授权码:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=code
获取token:http://localhost:1001/oauth/token
查看token:https://jwt.io/
访问地址:http://localhost:1001/oauth/authorize?client_id=myclient1&response_type=token&scope=read%20write&redirect_uri=http://www.baidu.com
首先使用李四这个账户,myclient1拥有订单资源和商品资源服务的权限,而李四账户本身有订单资源的所有方法权限,登录后获取access_token
,如下:
在访问具体方法的时候,需要把token也传递过去,参数头为Authorization
,参数值为Bearer
+空格+access_token
,访问地址是你正常业务的地址,如下:
其次使用王五这个账户,myclient1拥有订单资源和商品资源服务的权限,而王五账户本身没有订单资源的所有方法权限,登录后获取access_token
,如下:
在访问具体方法的时候,需要把token也传递过去,参数头为Authorization
,参数值为Bearer
+空格+access_token
,访问地址是你正常业务的地址,如下:
在这里,我使用一种取巧的方式开进行单点登录,既然OAuth2.0可以为第三方应用授权访问系统资源,我给当前的系统申请一个拥有所有资源权限的客户端id不就行了,这里为了省事,我还是用myclient1
,在登录的时候,我们使用密码模式进行登录,但是,你不能每次让用户都自己输入客户端id和客户端密钥,我们需要写一个控制器,接收用户传递过来的账户和密码,我们内部自己发送请求,以此实现登录效果,登录成功以后返回用户数据。
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();
}
}
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;
}
}
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跨站保护
}
...
...
}
重新启动:oauth2-server-auth1001
访问地址:http://localhost:1001/user/login
首先使用李四这个账户,myclient1拥有订单资源和商品资源服务的权限,而李四账户本身有订单资源的所有方法权限,登录后获取access_token
,如下:
在访问具体方法的时候,需要把token也传递过去,参数头为Authorization
,参数值为Bearer
+空格+access_token
,访问地址是你正常业务的地址,如下:
其次使用王五这个账户,myclient1拥有订单资源和商品资源服务的权限,而王五账户本身没有订单资源的所有方法权限,登录后获取access_token
,如下:
在访问具体方法的时候,需要把token也传递过去,参数头为Authorization
,参数值为Bearer
+空格+access_token
,访问地址是你正常业务的地址,如下:
(1)oauth2-resource-goods1003
添加依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
(2)拷贝oauth2-resource-order1002
的com.caochenlei.utils.RsaUtils
到oauth2-resource-goods1003
(3)拷贝oauth2-resource-order1002
的com.caochenlei.config.ResourceServerConfig
到oauth2-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-auth1001
的com.caochenlei.controller.GoodstController
到oauth2-resource-goods1003
(5)启动oauth2-resource-goods1003
(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
我用自定义单点登录的方式进行登录,这里使用zhangsan
来访问查找订单服务,订单服务里边会有很多商品,所以,查找订单服务又调用了根据主键查找商品服务,而且,张三拥有对订单所有方法和商品所有方法访问的权限,但是在实际调用过程中,就会报出错误,如下图:
造成上述问题的根本原因就是,资源服务器在请求的时候,需要携带token,来验证这次请求是不是合法,很显然,我们直接调用肯定会报错,我们并没有携带token,要解决也很简单,在每一次请求之前都携带上token即可。
在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