上一节中介绍了项目的搭建,并实现了授权码模式的访问。在上一节的基础上,再来实现客户端模式。【图文详解】搭建 Spring Authorization Server + Resource + Client 完整Demo
- 用户通过客户端访问资源是 授权码模式
- 微服务(资源)间的访问是 客户端模式;客户端模式下,只需要提供注册客户端的ID和密钥,就可以向授权服务器申请令牌,授权服务器核实ID和密钥后,会直接发放令牌,无须再认证/授权,特别适合项目内部模块间的调用。
为了让请求资源的主体更加清晰,再注册一个客户端
micro_service
,专门供资源服务器之间的相互调用。也可以用原来客户端my_client
,不过要在授权模式GrantType中添加CLIENT_CREDENTIALS
- 客户端模式直接返回token;不需要回调地址
AuthorizationServerConfiguration.java
中添加 /**
* 定义客户端(令牌申请方式:客户端模式)
*
* @param clientId 客户端ID
* @return
*/
private RegisteredClient createRegisteredClient(final String clientId) {
// JWT(Json Web Token)的配置项:TTL、是否复用refrechToken等等
TokenSettings tokenSettings = TokenSettings.builder()
// 令牌存活时间:1年
.accessTokenTimeToLive(Duration.ofDays(365))
// 令牌不可以刷新
//.reuseRefreshTokens(false)
.build();
// 客户端相关配置
ClientSettings clientSettings = ClientSettings.builder()
// 是否需要用户授权确认
.requireAuthorizationConsent(false)
.build();
return RegisteredClient
// 客户端ID和密码
.withId(UUID.randomUUID().toString())
//.withId(id)
.clientId(clientId)
//.clientSecret("{noop}123456")
.clientSecret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"))
// 客户端名称:可省略
.clientName("micro_service")
// 授权方法
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 授权模式
// ---- 【客户端模式】
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 客户端模式直接返回token;不需要回调地址
//.redirectUri("...")
// 授权范围(当前客户端的角色)
.scope("all")
// JWT(Json Web Token)配置项
.tokenSettings(tokenSettings)
// 客户端配置项
.clientSettings(clientSettings)
.build();
}
/**
* 注册客户端
*
* @param jdbcTemplate 操作数据库
* @return 客户端仓库
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
// ---------- 1、检查当前客户端是否已注册
// 操作数据库对象
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
/*
客户端在数据库中记录的区别
------------------------------------------
id:仅表示客户端在数据库中的这个记录
client_id:唯一标示客户端;请求token时,以此作为客户端的账号
client_name:客户端的名称,可以省略
client_secret:密码
*/
String clientId_1 = "my_client";
String clientId_2 = "micro_service";
// 查询客户端是否存在
RegisteredClient registeredClient_1 = registeredClientRepository.findByClientId(clientId_1);
RegisteredClient registeredClient_2 = registeredClientRepository.findByClientId(clientId_2);
// ---------- 2、添加客户端
// 数据库中没有
if (registeredClient_1 == null) {
registeredClient_1 = this.createRegisteredClientAuthorizationCode(clientId_1);
registeredClientRepository.save(registeredClient_1);
}
// 数据库中没有
if (registeredClient_2 == null) {
registeredClient_2 = this.createRegisteredClient(clientId_2);
registeredClientRepository.save(registeredClient_2);
}
// ---------- 3、返回客户端仓库
return registeredClientRepository;
}
用 资源服务器B
调用 资源服务器A
中的资源;
具体:服务B/res1
--> 服务A/res2
;
服务A/res2
接口在前面用 my_client
是无法访问的;
当前 资源服务器B
无安全策略,可以直接访问
@Configuration(proxyBeanMethods = false)
public class RestTemplateConfiguration {
@Bean
public RestTemplate oauth2ClientRestTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.build();
}
}
@RestController
public class ResourceController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/res1")
public String getRes1(HttpServletRequest request) {
// 调用资源服务器A中的资源res2
return getServer("http://127.0.0.1:8001/res2", request);
//return JSON.toJSONString(new Result(200, "服务B -> 资源1"));
}
@GetMapping("/res2")
public String getRes2() {
return JSON.toJSONString(new Result(200, "服务B -> 资源2"));
}
/**
* 请求资源
*
* @param url
* @param request
* @return
*/
private String getServer(String url,
HttpServletRequest request) {
// ======== 1、从session中取token ========
HttpSession session = request.getSession();
String token = (String) session.getAttribute("micro-token");
// ======== 2、请求token ========
// 先查session中是否有token;session中没有
if (StringUtils.isEmpty(token)) {
// ===== 去认证中心申请 =====
// 对id及密钥加密
byte[] userpass = Base64.encodeBase64(("micro_service:123456").getBytes());
String str = "";
try {
str = new String(userpass, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 请求头
HttpHeaders headers1 = new HttpHeaders();
// 组装请求头
headers1.add("Authorization", "Basic " + str);
// 请求体
HttpEntity<Object> httpEntity1 = new HttpEntity<>(headers1);
// 响应体
ResponseEntity<String> responseEntity1 = null;
try {
// 发起申请令牌请求
responseEntity1 = restTemplate.exchange("http://os.com:9000/oauth2/token?grant_type=client_credentials", HttpMethod.POST, httpEntity1, String.class);
} catch (RestClientException e) {
//
System.out.println("令牌申请失败");
}
// 令牌申请成功
if (responseEntity1 != null) {
// 解析令牌
// String t = JSON.parseObject(responseEntity1.getBody(), MyAuth.class).getAccess_token();
Map<String, String> resMap = JSON.parseObject(responseEntity1.getBody(), HashMap.class);
String t = resMap.get("access_token");
// 存入session
session.setAttribute("micro-token", t);
// 赋于token变量
token = t;
}
}
// ======== 3、请求资源 ========
// 请求头
HttpHeaders headers2 = new HttpHeaders();
// 组装请求头
headers2.add("Authorization", "Bearer " + token);
// 请求体
HttpEntity<Object> httpEntity2 = new HttpEntity<>(headers2);
// 响应体
ResponseEntity<String> responseEntity2;
try {
// 发起访问资源请求
responseEntity2 = restTemplate.exchange(url, HttpMethod.GET, httpEntity2, String.class);
} catch (RestClientException e) {
// 令牌失效(认证失效401) --> 清除session
// e.getMessage() 信息格式:
// 401 : "{"msg":"认证失败","uri":"/res2"}"
String str = e.getMessage();
// 判断是否含有 401
if(StringUtils.contains(str, "401")){
// 如果有401,把session中 micro-token 的值设为空
session.setAttribute("micro-token","");
}
// 取两个括号中间的部分(包含两个括号)
return str.substring(str.indexOf("{"), str.indexOf("}") + 1);
}
// 返回
return responseEntity2.getBody();
}
}
// 用于解析申请到的令牌数据
/*@Data
class MyAuth {
private String access_token;
private String scope;
private String token_type;
private long expires_in;
}*/
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-oauth2-resource-serverartifactId>
dependency>
复制 资源服务器A 中配置策略到 资源服务器B 中来
# 自定义 jwt 配置(校验jwt)
jwt:
cert-info:
# 公钥证书存放位置
public-key-location: myjks.cer
claims:
# 令牌的鉴发方:即授权服务器的地址
issuer: http://os.com:9000
clean
清理项目如果需要 资源服务器A 调用 B 中资源;可以把 B 中的实现逻辑复制过去就行。
后期会把资源服务器中的公共部分抽离出来,制成starter…