文章来自 itboyhub.com/
授权码模式
新建 maven 父级工程,然后创建 3 个 springboot 子项目
auth-server 授权服务器
依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.11.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR11</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
启动端口号设置 8080
新建 SecurityConfig
配置类,配置我们的用户信息
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("southyin")
.password(new BCryptPasswordEncoder().encode("123"))
.roles("admin");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().formLogin();
}
}
新建 AccessTokenConfig
类,配置 token
存放位置,这里先采取内存
@Configuration
public class AccessTokenConfig {
/**
* 配置 token 保存在哪
* @return InMemoryTokenStore 表示保存在内存中
*/
@Bean
TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
新建 AuthorizationServer
类,配置客户端、令牌、授权码等信息
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
TokenStore tokenStore;
@Autowired
ClientDetailsService detailsService;
/**
* AuthorizationServerSecurityConfigurer 用来配置令牌端点的安全约束,也就是这个端点谁能访
* 问,谁不能访问。
* checkTokenAccess 是指一个 Token 校验的端点,这个端点我们设置为可以直接访问
* (在后面,当资源服务器收到 Token 之后,需要去校验 Token 的合法性,就会访问这个端点)
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
/**
* ClientDetailsServiceConfigurer 用来配置客户端的详细信息
* 授权服务器要做两方面的检验,一方面是校验客户端,另一方面则是校验用户,校验用户,我们前
* 面已经配置了,这里就是配置校验客户端。客户端的信息我们可以存在数据库中,这其实也是比较
* 容易的,和用户信息存到数据库中类似,但是这里为了简化代码,我还是将客户端信息存在内存中
*
* 这里我们分别配置了客户端的 id,secret、资源 id、授权类型、授权范围以及重定向 uri。授
* 权类型我在上篇文章中和大家一共讲了四种,四种之中不包含 refresh_token 这种类型,但是在实
* 际操作中,refresh_token 也被算作一种
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("southyin")
.secret(new BCryptPasswordEncoder().encode("123"))
.resourceIds("res1")
.authorizedGrantTypes("authorization_code","refresh_token")
.scopes("all")
.redirectUris("http://localhost:8082/index.html");
}
/**
* AuthorizationServerEndpointsConfigurer 这里用来配置令牌的访问端点和令牌服务。
*
* authorizationCodeServices用来配置授权码的存储,这里我们是存在在内存中
*
* tokenServices用来配置令牌的存储,即 access_token 的存储位置,这里我们也先存储在内存中
* 授权码和令牌有什么区别?授权码是用来获取令牌的,使用一次就失效,令牌则是用来获取资源的
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authorizationCodeServices(authorizationCodeServices())
.tokenServices(tokenServices());
}
/**
* authorizationCodeServices用来配置授权码的存储,这里我们是存在在内存中
* @return
*/
@Bean
AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}
/**
* tokenServices 这个 Bean 主要用来配置 Token 的一些基本信息,例如 Token 是否支持刷新、
* Token 的存储位置、Token 的有效期以及刷新 Token 的有效期等等。Token 有效期这个好理解,
* 刷新 Token 的有效期我说一下,当 Token 快要过期的时候,我们需要获取一个新的 Token,在获
* 取新的 Token 时候,需要有一个凭证信息,这个凭证信息不是旧的 Token,而是另外一个
* refresh_token,这个 refresh_token 也是有有效期的
* @return
*/
@Bean
AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(detailsService);
services.setSupportRefreshToken(true);
services.setTokenStore(tokenStore);
services.setAccessTokenValiditySeconds(60 * 60 * 2);
services.setRefreshTokenValiditySeconds(60 * 60 * 24 * 3);
return services;
}
}
启动
user-server 资源服务器
依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.11.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR11</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
启动端口号 8081
创建 ResourceServerConfig
类,配置资源服务器信息
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
/**
* tokenServices 我们配置了一个 RemoteTokenServices 的实例,这是因为资源服务器和授权服务
* 器是分开的,资源服务器和授权服务器是放在一起的,就不需要配置 RemoteTokenServices 了
*
* RemoteTokenServices 中我们配置了 access_token 的校验地址、client_id、client_secret 这三个
* 信息,当用户来资源服务器请求资源时,会携带上一个 access_token,通过这里的配置,就能够
* 校验出 token 是否正确等。
* @return
*/
@Bean
RemoteTokenServices tokenServices() {
RemoteTokenServices services = new RemoteTokenServices();
services.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
services.setClientId("southyin");
services.setClientSecret("123");
return services;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("res1").tokenServices(tokenServices());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.anyRequest().authenticated();
}
}
创建资源接口
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello1() {
return "/hello";
}
@GetMapping("/admin/hello")
public String hello2() {
return "/admin/hello";
}
}
启动
client-app 第三方应用
这个项目不是必须的,也可以利用 postman
来测,依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
启动端口号 8082
,templates
文件夹下创建 index.html
模拟第三方网站页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http//www/thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="http://localhost:8080/oauth/authorize?client_id=southyin&response_type=code&scope=all&redirect_uri=http://localhost:8082/index.html">
第三方登录
</a>
<h1 th:text="${msg}"></h1>
</body>
</html>
启动类
@SpringBootApplication
public class ClientAppApplication {
public static void main(String[] args) {
SpringApplication.run(ClientAppApplication.class, args);
}
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}
访问页面接口
@Controller
public class HelloController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/index.html")
public String hello(String code, Model model) {
if (code != null) {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("client_id", "southyin");
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);
String access_token = resp.get("access_token");
System.out.println(access_token);
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);
model.addAttribute("msg", entity.getBody());
}
return "index";
}
}
参数中没有 code
则直接返回 index页面
,若有 code
(这里 code
是授权码,并不是 token
),则向 授权服务器
申请 token
,然后拿着返回的 access_token
再向 资源服务器
申请资源,然后放到 Model
中,返回给 index页面
,index页面
拿到资源信息,在页面显示,启动测试
- 1.访问
http://localhost:8082/index.html
- 2.点击超链接,登录用户申请授权,用户名和密码是之前在
授权服务器
中配置的southyin
和123
密码模式
密码模式要求高度信任第三方,比如授权方和第三方都是自己的应用,是可以的
auth-server 授权服务器
对 授权服务器
进行改造,使其支持 password
模式
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("southyin")
.secret(new BCryptPasswordEncoder().encode("123"))
.resourceIds("res1")
.authorizedGrantTypes("authorization_code","password","refresh_token")
.scopes("all")
.redirectUris("http://localhost:8082/index.html");
}
这里其他地方都不变,主要是在 authorizedGrantTypes
中增加了 password
模式
由于使用了 password 模式之后,用户要进行登录,所以我们需要配置一个 AuthenticationManager
,
还是在 AuthorizationServer
类中,具体配置如下
@Autowired
AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws
Exception {
endpoints
.authenticationManager(authenticationManager)
.tokenServices(tokenServices());
}
注意,在授权码模式中,我们配置的 AuthorizationCodeServices
现在不需要了,取而代之的是 authenticationManager
在 SecurityConfig
中添加 AuthenticationManager
对象
client-app 第三方应用
修改 index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http//www/thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input name="password"></td>
</tr>
<tr>
<td><input type="submit" value="登录"></td>
</tr>
</table>
</form>
<h1 th:text="${msg}"></h1>
</body>
</html>
登录接口 /login
,Post
请求
@Controller
public class HelloController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/index.html")
public String index() {
return "index";
}
@PostMapping("/login")
public String hello(String username,String password, Model model) {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("username", username);
map.add("password", password);
map.add("client_secret", "123");
map.add("client_id", "southyin");
map.add("grant_type", "password");
Map<String,String> resp = restTemplate.postForObject("http://localhost:8080/oauth/token", map, Map.class);
String access_token = resp.get("access_token");
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);
model.addAttribute("msg", entity.getBody());
return "index";
}
}
在登录接口中,当收到一个用户名密码之后,我们通过 RestTemplate
发送一个 post
请求,注意 post
请求中,grant_type
参数的值为 password
,通过这个请求,我们可以获取 auth-server
返回的 access_token
配置完成后,启动 client-app
,访问 http://localhost:8082/index.html
页面进行测试。授权完成后,我们在项目首页可以看到如下内容
刷新token
以之前的 password
模式为例,使用 postman
测试
localhost:8080/oauth/token?client_id=southyin&client_secret=123&grant_type=password&username=southyin&password=123
得到数据如下
{
"access_token": "3d3b5333-67fa-41b2-934a-0063a0f3eb8c",
"token_type": "bearer",
"refresh_token": "b0fb6c4e-31b2-4dd5-b049-3bbd15e2890b",
"expires_in": 7199,
"scope": "all"
}
/oauth/token
端点除了颁发令牌,还可以用来刷新令牌,这个 refresh_token
就是用来刷新令牌用的
再次 postman
发送请求
localhost:8080/oauth/token?client_id=southyin&client_secret=123&grant_type=refresh_token&refresh_token=b0fb6c4e-31b2-4dd5-b049-3bbd15e2890b
得到新的 token
{
"access_token": "d5e88e07-0c28-4677-9f28-6f0e7c6f7e63",
"token_type": "bearer",
"refresh_token": "b0fb6c4e-31b2-4dd5-b049-3bbd15e2890b",
"expires_in": 7199,
"scope": "all"
}
之前的 token
就会失效
令牌存储
在我们配置授权码模式的时候,有两个东西当时存在了内存中:
- InMemoryAuthorizationCodeServices 这个表授权码存在内存中。
- InMemoryTokenStore 表示生成的令牌存在内存中
授权码用过一次就会失效,存在内存中没什么问题,但是令牌,还有其他的存储方案
InMemoryTokenStore
实现了 TokenStore
接口,我们来看下 TokenStore
接口的实现
类:
-
InMemoryTokenStore
,这是我们之前使用的,也是系统默认的,就是将access_token
存到内存中,单机应用这个没有问题,但是在分布式环境下不推荐 -
JdbcTokenStore
,这种方式令牌会被保存到数据中,这样就可以方便的和其他应用共享令牌信息 -
JwtTokenStore
,这个其实不是存储,因为使用了 jwt 之后,在生成的 jwt 中就有用户的所有信息,服务端不需要保存,这也是无状态登录 -
RedisTokenStore
,这个很明显就是将 access_token 存到 redis 中。 -
JwkTokenStore
,将 access_token 保存到 JSON Web Key
其中 RedisTokenStore
和 JwtTokenStore
是常见的使用方案
RedisTokenStore 方案
首先我们启动一个 Redis 服务,然后给 auth-server 添加 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=12345
配置完成后,我们修改 TokenStore 的实例
@Configuration
public class AccessTokenConfig {
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Bean
TokenStore tokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
}
然后分别启动 auth-server
、client-app
以及 user-server
,走一遍第三方登录流程,然后我们发现,派发的 access_token
在 redis
中也有一份
access_token
这个 key
在 Redis
中的有效期就是授权码的有效期。正是因为 Redis
中的这种过期机制,让它在存储 access_token
时具有天然的优势
Jwt方案
放后面
客户端信息入库
前面客户端信息我们是直接存储在内存中的
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("southyin")
.secret(new BCryptPasswordEncoder().encode("123"))
.resourceIds("res1")
.authorizedGrantTypes("authorization_code","refresh_token")
.scopes("all")
.redirectUris("http://localhost:8082/index.html");
}
在实际项目中,这种方式并不可取,一来客户端信息在代码中写死了,以后不好维护,而来我们的客户端信息可能量非常大,比如微信,有大量的第三方应用都接入了微信登录,微信不可能把所有的客户端信息都写死在代码中,所以我们要将客户端信息存入数据库中
客户端信息入库涉及到的接口主要是 ClientDetailsService,这个接口主要有两个实现类
InMemoryClientDetailsService
是存在内存中的。如果要存入数据库,很明显是 JdbcClientDetailsService
,来大概看下 JdbcClientDetailsService
的源码,就能分析出数据库的结构
public class JdbcClientDetailsService implements ClientDetailsService,
ClientRegistrationService {
private static final String CLIENT_FIELDS_FOR_UPDATE = "resource_ids, scope,
"
+ "authorized_grant_types, web_server_redirect_uri, authorities,
access_token_validity, "
+ "refresh_token_validity, additional_information, autoapprove";
private static final String CLIENT_FIELDS = "client_secret, " +
CLIENT_FIELDS_FOR_UPDATE;
private static final String BASE_FIND_STATEMENT = "select client_id, " +
CLIENT_FIELDS
+ " from oauth_client_details";
private static final String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + "
order by client_id";
private static final String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT +
" where client_id = ?";
private static final String DEFAULT_INSERT_STATEMENT = "insert into
oauth_client_details (" + CLIENT_FIELDS
+ ", client_id) values (?,?,?,?,?,?,?,?,?,?,?)";
private static final String DEFAULT_UPDATE_STATEMENT = "update
oauth_client_details " + "set "
+ CLIENT_FIELDS_FOR_UPDATE.replaceAll(", ", "=?, ") + "=? where
client_id = ?";
private static final String DEFAULT_UPDATE_SECRET_STATEMENT = "update
oauth_client_details "
+ "set client_secret = ? where client_id = ?";
private static final String DEFAULT_DELETE_STATEMENT = "delete from
oauth_client_details where client_id = ?";
分析得到表结构脚本
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>
<version>5.1.47</version>
</dependency>
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql:///userserver?useUnicode=true&characterEncoding=UTF-8&useSSL=true
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());
}
配置完成后,重启 auth-server
,走一遍第三方登录流程,和前面效果一样
也可以将令牌有效期配置在数据库中,这样就不用在代码中配置了
授权码模式下,修改后的 AuthorizationServerTokenServices
实例:
@Bean
AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(clientDetailsService());
services.setSupportRefreshToken(true);
services.setTokenStore(tokenStore);
return services;
}
client-app 第三方应用优化
首先定义一个专门的类 TokenTask
用来解决 Token
的管理问题
@Component
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", "southyin");
map.add("client_secret", "123");
map.add("redirect_uri", "http://localhost:8082/index.html");
map.add("grant_type", "authorization_code,password");
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", "southyin");
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");
}
}
-
- 首先在
getData
方法中,如果access_token
为空字符串,并且code
不为null
,表示这是刚刚拿到授权码的时候,准备去申请令牌了,令牌拿到之后,将access_token
和refresh_token
分别赋值给全局变量,然后调用loadDataFromResServer
方法去资源服务器加载数据
- 首先在
-
- 另外有一个
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";
}
}
这样再去 index.html
刷新就不会报错了
OAuth2 + JWT
JWT 包含三部分数据:
1.Header:头部,通常头部有两部分信息:
- 声明类型,这里是JWT
- 加密算法,自定义
我们会对头部进行 Base64Url 编码(可解码),得到第一部分数据
2.Payload:载荷,就是有效数据,在官方文档中(RFC7519),这里给了 7 个示例信息:
- iss (issuer):表示签发人
- exp (expiration time):表示token过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号 这部分也会采用 Base64Url 编码,得到第二部分数据
3.Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥 secret(密钥保存在服务端,不能泄露给客户端),通过 Header 中配置的加密算法生成。用于验证整 个数据完整和可靠性
改造 AccessTokenConfig
@Configuration
public class AccessTokenConfig {
private String SIGNING_KEY = "southyin";
@Bean
TokenStore tokenStore() {
return new JwtTokenStore(tokenConverter());
}
@Bean
JwtAccessTokenConverter tokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
-
TokenStore
使用JwtTokenStore
这个实例。之前将access_token
无论是存储在内存中,还是存储在Redis
中,都是要存下来的,客户端将access_token
发来之后,还要校验看对不对。但是如果使用了JWT
,access_token
实际上就不用存储了(无状态登录,服务端不需要保存信息),因为用户的所有信息都在jwt
里边,所以这里配置的JwtTokenStore
本质上并不是做存储 -
JwtAccessTokenConverter
可以实现将用户信息和JWT
进行转换(将用户信息转为jwt
字符串,或者从jwt
字符串提取出用户信息) -
在
JWT
字符串生成的时候,我们需要一个签名,这个签名需要自己保存好
JWT 默认生成的用户信息主要是用户角色、用户名等,如果我们希望在生成的 JWT 上面添加额外 的信息,可以按照如下方式添加
@Component
public class CustomAdditionalInformation implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> map = accessToken.getAdditionalInformation();
map.put("author", "southyin");
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
return accessToken;
}
}
自定义类 CustomAdditionalInformation
实现 TokenEnhancer
接口,并实现接口中的 enhance
方法。enhance
方法中的 OAuth2AccessToken
参数就是已经生成的 access_token
信息,我们可以从OAuth2AccessToken
中取出已经生成的额外信息,然后在此基础上追加自己的信息,之前配置的 JwtAccessTokenConverter
也是 TokenEnhancer
的一个实例
还需要在 AuthorizationServer
中修改 AuthorizationServerTokenServices
实例
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
CustomAdditionalInformation customAdditionalInformation;
@Bean
AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(clientDetailsService());
services.setSupportRefreshToken(true);
services.setTokenStore(tokenStore);
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter,
customAdditionalInformation));
services.setTokenEnhancer(tokenEnhancerChain);
return services;
}
这里主要是是在 DefaultTokenServices
中配置 TokenEnhancer
,将之前的 JwtAccessTokenConverter
和 CustomAdditionalInformation
两个实例注入进来即可
资源服务器改造
也就是 user-server
,将 auth-server
中的 AccessTokenConfig
类拷贝到 user-server
中,然后在资源服务器配置中不再配置远程校验地址,而是配置一个 TokenStore
即可
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws
Exception {
resources.resourceId("res1").tokenStore(tokenStore);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.anyRequest().authenticated();
}
}
测试,这里采取了 password
模式测试 localhost:8080/oauth/token?client_id=southyin&client_secret=123&grant_type=password&username=southyin&password=123
拿返回的 access_token
去 localhost:8080/oauth/check_token
解析看一看就可以看到 jwt 中保存的用户详细信息了。
同时也可以拿着 access_token
去资源服务器访问了