OAuth2令牌存入Redis
首先我们需要安装redis:
目前有四种方式获取一个 Redis,这里简单介绍一下后期具体说
- 直接编译安装(推荐使用)
- 使用 Docker
- 也可以直接安装
- 还有一个在线体验的方式,通过在线体验,可以直接使用 Redis 的功能http://try.redis.io/
这里我们使用第一种
提前准备好 gcc 环境。
yum install gcc-c++
接下来下载并安装 Redis:
wget http://download.redis.io/releases/redis-5.0.7.tar.gz
tar -zxvf redis-5.0.7.tar.gz
cd redis-5.0.7/
make
make install
安装完成后,启动 Redis
redis-server redis.conf
启动成功后页面如下
至此安装成功,下面说说springboot中加入redis启动报错的问题
添加 Redis 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
依赖添加成功后,在 application.properties 中添加 redis 配置:
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=123
写上自己的ip和密码
可以用jedis来测试一下是否连接上,测试类如下:
public class JedisTest {
public static void main(String[] args) {
//创建客户端
Jedis jedis = new Jedis("自己的IP地址",6379);
//设置密码
jedis.auth("自己设置的密码");
//设置值
jedis.set("name","cphsw");
//获取值
String value = jedis.get("name");
System.out.println(value);
//释放连接资源
jedis.close();
}
}
当然了我们要在pom.xml引入以下配置
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
第一次连接的时候,肯定会有连接超时或者连接不上问题,就不截图,说方法:
我们配置下防火墙 开一个6379端口权限,防火墙是关闭的可以忽略
firewall-cmd --zone=public --add-port=6379/tcp --permanent
firewall-cmd --reload
我们配置下 redis配置文件
vi /cp/redis/redis-5.0.7/redis.conf
这里绑定了本机,我们把这个备注掉;
# bind 127.0.0.1
配置完后要重启下redis服务
[root@instance-cpe31be2 redis-5.0.7]# redis-cli shutdown
[root@instance-cpe31be2 redis-5.0.7]# redis-server /cp/redis/redis-5.0.7/redis.conf
此时因为远程连接redis redis自我保护 拒绝访问
方法一:
编辑redis.conf,修改下面内容
改成 no即可,此时可以连接上了,但不安全,推荐方法二
方法二: 设置redis连接密码
首先我们服务器上连接上去
127.0.0.1:6379> config set requirepass 123456
设置密码 123456
127.0.0.1:6379> quit
然后重启一下
127.0.0.1:6379> auth 123456
OK
此时我们本地的测试也可以了,到此我们本地项目连接上远程redis服务
OAuth2常见的有四种模式
1.授权码模式
2.简易模式
3.账号密码模式
4.客户端模式
具体的在码云上,感兴趣可以联系获取
在我们配置授权码模式的时候,有两个东西当时存在了内存中:
InMemoryAuthorizationCodeServices 这个表授权码存在内存中。
InMemoryTokenStore 表示生成的令牌存在内存中。
授权码用过一次就会失效,存在内存中没什么问题,但是令牌,我们实际上还有其他的存储方案。
我们所使用的 InMemoryTokenStore 实现了 TokenStore 接口,看下 TokenStore 接口的实现类
可以看到,我们有多种方式来存储 access_token。
1.InMemoryTokenStore,这是我们之前使用的,也是系统默认的,就是将 access_token 存到内存中,单机应用这个没有问题,但是在分布式环境下不推荐。
2.JdbcTokenStore,看名字就知道,这种方式令牌会被保存到数据中,这样就可以方便的和其他应用共享令牌信息。
3.JwtTokenStore,这个其实不是存储,因为使用了 jwt 之后,在生成的 jwt 中就有用户的所有信息,服务端不需要保存,这也是无状态登录
4.RedisTokenStore,这个很明显就是将 access_token 存到 redis 中。
5.JwkTokenStore,将 access_token 保存到 JSON Web Key。
配置完成后,我们修改 TokenStore 的实例
@Configuration
public class AccessTokenConfig {
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Bean
TokenStore tokenStore(){
//return new InMemoryTokenStore();
return new RedisTokenStore(redisConnectionFactory);
}
}
然后分别启动 auth-server、client-app 以及 user-server,走一遍第三方登录流程(http://localhost:8082/index.html),然后我们发现,派发的 access_token 在 redis 中也有一份
可以看到,数据都存到 Redis 中了,access_token 这个 key 在 Redis 中的有效期就是授权码的有效期。正是因为 Redis 中的这种过期机制,让它在存储 access_token 时具有天然的优势
客户端信息之前我们也是存到内存的,后期如果用户量太大,是不合理的
客户端信息入库涉及到的接口主要是 ClientDetailsService,这个接口主要有两个实现类
InMemoryClientDetailsService 就不多说了,这是存在内存中的。如果要存入数据库,很明显是 JdbcClientDetailsService,从JdbcClientDetailsService源码中我们可以看到表的结构
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(48) NOT NULL,
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`authorized_grant_types` varchar(256) DEFAULT NULL,
`web_server_redirect_uri` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
接下来我们将一开始定义的客户端的关键信息存入数据库中
既然用到了数据库,依赖当然也要提供相应的支持,我们给 auth-server 添加如下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
然后在 application.properties 中配置一下数据库的连接信息:
spring.datasource.url=jdbc:mysql:///oauth2?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=Asia/Shanghai
spring.datasource.password=root
spring.datasource.username=root
spring.main.allow-bean-definition-overriding=true
这里的配置多了最后一条。这是因为我们一会要创建自己的 ClientDetailsService,而系统已经创建了 ClientDetailsService,加了最后一条就允许我们自己的实例覆盖系统默认的实例。
接下来,我们来提供自己的实例即可
@Autowired
DataSource dataSource;
@Bean
ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());
}
我们也可以将令牌有效期配置在数据库中,这样就不用在代码中配置了,修改后的数据库如下:
修改后的 AuthorizationServerTokenServices 实例如下:
@Bean
AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(clientDetailsService());
services.setSupportRefreshToken(true);
services.setTokenStore(tokenStore);
return services;
}
第三方应用优化
优化第三方登录中的Controller
首先我们来定义一个专门的类 TokenTask 用来解决 Token 的管理问题
@Component @SessionScope
public class TokenTask {
@Autowired
RestTemplate restTemplate;
public String access_token = "";
public String refresh_token = "";
public String getData(String code) {
if ("".equals(access_token) && code != null) {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("code", code);
map.add("client_id", "javaboy");
map.add("client_secret", "123");
map.add("redirect_uri", "http://localhost:8082/index.html");
map.add("grant_type", "authorization_code");
Map<String, String> resp = restTemplate.postForObject("http://localhost:8080/oauth/token", map, Map.class);
access_token = resp.get("access_token");
refresh_token = resp.get("refresh_token");
return loadDataFromResServer();
} else {
return loadDataFromResServer();
}
}
private String loadDataFromResServer() {
try {
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + access_token);
HttpEntity<Object> httpEntity = new HttpEntity<>(headers);
ResponseEntity<String> entity = restTemplate.exchange("http://localhost:8081/admin/hello", HttpMethod.GET, httpEntity, String.class);
return entity.getBody();
} catch (RestClientException e) {
return "未加载";
}
}
@Scheduled(cron = "0 55 0/1 * * ?")
public void tokenTask() {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("client_id", "javaboy");
map.add("client_secret", "123");
map.add("refresh_token", refresh_token);
map.add("grant_type", "refresh_token");
Map<String, String> resp = restTemplate.postForObject("http://localhost:8080/oauth/token", map, Map.class);
access_token = resp.get("access_token");
refresh_token = resp.get("refresh_token");
}
}
1.首先在 getData 方法中,如果 access_token 为空字符串,并且 code 不为 null,表示这是刚刚拿到授权码的时候,准备去申请令牌了,令牌拿到之后,将 access_token 和 refresh_token 分别赋值给变量,然后调用 loadDataFromResServer 方法去资源服务器加载数据。
2.另外有一个 tokenTask 方法,这是一个定时任务,每隔 115 分钟去刷新一下 access_token(access_token 有效期是 120 分钟)。
HelloController 中略作调整
@Controller
public class HelloController {
@Autowired
TokenTask tokenTask;
@GetMapping("/index.html")
public String hello(String code, Model model) {
model.addAttribute("msg", tokenTask.getData(code));
return "index";
}
}
这样就 OK 了,当我们再去下图这个页面按 F5 刷新就不会出错了。