1、搭建 Oauth2 资源服务(客户端)
搭建下边几个服务任选一个:
会员服务:herring-member-service,微服务之一,接收到请求后会到认证中心验证。
订单服务:herring-orders-service,微服务之二,接收到请求后会到认证中心验证。
商品服务:herring-product-service,微服务之三,接收到请求后会到认证中心验证。
添加 pom 文件依赖:
<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>
如果在 jwt 中加入了额外信息,而在接收到 jwt 格式的 token 之后,用户客户端要把 jwt 解析出来。
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
1.1 application.yml 配置文件:
client-id、client-secret 要和认证服务中的配置一致,
access-token-uri 是密码模式需要用到的获取 token 的接口,
user-authorization-uri 是授权码认证方式需要的,可以不设置。
server:
port: 10801
servlet:
context-path: /api/member
spring:
application:
name: member-service
security:
oauth2:
client:
client-id: app-client
client-secret: client-secret-8888
user-authorization-uri: http://localhost:10800/oauth/authorize
access-token-uri: http://localhost:10800/oauth/token
resource:
jwt:
key-uri: http://localhost:10800/oauth/token_key
key-value: sign-8888
1.2 ResourceServerConfig 类的配置:
资源服务的注解 @EnableResourceServer,注意 JwtAccessTokenConverter 设置的 signingKey 要和配置文件中的 key-value 相同,不然会导致无法正常解码 jwt ,导致验证不通过。
server:
port: 10801
servlet:
context-path: /api/member
spring:
application:
name: member-service
security:
oauth2:
client:
client-id: app-client
client-secret: client-secret-8888
user-authorization-uri: http://localhost:10800/oauth/authorize
access-token-uri: http://localhost:10800/oauth/token
resource:
jwt:
key-uri: http://localhost:10800/oauth/token_key
key-value: sign-8888
1.3 ResourceServerConfig 类的配置:
资源服务的注解 @EnableResourceServer,注意 JwtAccessTokenConverter 设置的 signingKey 要和配置文件中的 key-value 相同,不然会导致无法正常解码 jwt ,导致验证不通过。
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class JwtResourceServerConfig extends ResourceServerConfigurerAdapter {
@Resource
private TokenStore jwtTokenStore;
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setSigningKey("sign-8888");
accessTokenConverter.setVerifierKey("sign-8888");
return accessTokenConverter;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(jwtTokenStore);
resources.resourceId("member-service");
}
}
1.4 HelloController 创建几个测试用的接口:
@RestController
@RequestMapping
public class HelloController {
@Resource
private MemberService memberService;
@RequestMapping("/service")
public String service() {
return memberService.sayHello();
}
@GetMapping(value = "/info/jwt")
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public Object jwtParser(Authentication authentication) {
authentication.getCredentials();
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
String jwtToken = details.getTokenValue();
Claims claims = Jwts.parser()
.setSigningKey("sign-8888".getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(jwtToken)
.getBody();
return claims;
}
}
@Service
public class MemberService {
public String sayHello() {
return "Hello, Member! ";
}
}
1.5 启动资源服务,测试 Oauth2 的密码模式流程:
一、向 Oauth2 认证中心(服务端)请求 token:
向 Oauth2 认证中心(服务端)请求 token
POST http://localhost:10800/oauth/token?grant_type=password&username=admin&password=123456&client_id=app-client&client_secret=client-secret-8888&scope=all
Accept: */*
Cache-Control: no-cache
得到请求结果:
- access_token : 就是之后请求需要带上的 token,也是本次请求的主要目的
- token_type:为 bearer,这是 access token 最常用的一种形式
- refresh_token:之后可以用这个值来换取新的 token,而不用输入账号密码
- expires_in:token 的过期时间(秒)
二、向 Oauth2 资源服务(客户端)请求数据:
请求不带 token:
#### 向 Oauth2 资源服务(客户端)请求数据
GET http://localhost:10801/api/member/service
Accept: */*
Cache-Control: no-cache
得到请求结果:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
请求带错误的 token:
#### 向 Oauth2 资源服务(客户端)请求数据
GET http://localhost:10801/api/member/service
Accept: */*
Cache-Control: no-cache
Authorization: bearer 123456
得到请求结果:
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
就算能解析成 JSON,token 错误也会报其他错误,是得不到正确的请求数据的。
请求带正确的 token,需要请求头 Authorization,格式为 bearer + 空格 + token:
#### 向 Oauth2 资源服务(客户端)请求数据
GET http://localhost:10801/api/member/service
Accept: */*
Cache-Control: no-cache
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib3JkZXJzLXNlcnZpY2UiLCJnYXRld2F5LXNlcnZpY2UiLCJtZW1iZXItc2VydmljZSIsInByb2R1Y3Qtc2VydmljZSJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTYxMjg1ODQxNywiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJiMGQ5ZTI1Yy1jZGE3LTQ4MDctOWJmZS02ZjcyYjM4NGVhNTMiLCJjbGllbnRfaWQiOiJhcHAtY2xpZW50In0.w4M9zCahAVISQ_wfKdkT6n9Aaw6kFtoh5HmCJ_uy-vU
得到请求结果:
Hello, Member!
三、向 Oauth2 认证中心(服务端)刷新 token:
#### 向 Oauth2 认证中心(服务端)刷新 token
POST http://localhost:10800/oauth/token?grant_type=refresh_token&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib3JkZXJzLXNlcnZpY2UiLCJnYXRld2F5LXNlcnZpY2UiLCJtZW1iZXItc2VydmljZSIsInByb2R1Y3Qtc2VydmljZSJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImIwZDllMjVjLWNkYTctNDgwNy05YmZlLTZmNzJiMzg0ZWE1MyIsImV4cCI6MTYxMjkyMzIxNywiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiIzZmQ2MWM4ZS1kNTcyLTQ0YjYtYjViNC0zMzc3ODQ5NjY4YmQiLCJjbGllbnRfaWQiOiJhcHAtY2xpZW50In0.WxisDVLUlfP45pepc4sQM1M7UCvzsET0O8JvF11tKAI&client_id=app-client&client_secret=client-secret-8888
Accept: */*
Cache-Control: no-cache
得到请求结果:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib3JkZXJzLXNlcnZpY2UiLCJnYXRld2F5LXNlcnZpY2UiLCJtZW1iZXItc2VydmljZSIsInByb2R1Y3Qtc2VydmljZSJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTYxMjg1OTQxMSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJkYWVlNjlkZi02NTFhLTQ1MmItYjA0Yi05N2FhYTc2MjkzYTgiLCJjbGllbnRfaWQiOiJhcHAtY2xpZW50In0.F30LZplYodM7zH0N6gwBA29uCBObZISgOPXf-PKB3aI",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib3JkZXJzLXNlcnZpY2UiLCJnYXRld2F5LXNlcnZpY2UiLCJtZW1iZXItc2VydmljZSIsInByb2R1Y3Qtc2VydmljZSJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImRhZWU2OWRmLTY1MWEtNDUyYi1iMDRiLTk3YWFhNzYyOTNhOCIsImV4cCI6MTYxMjkyMzIxNywiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiIzZmQ2MWM4ZS1kNTcyLTQ0YjYtYjViNC0zMzc3ODQ5NjY4YmQiLCJjbGllbnRfaWQiOiJhcHAtY2xpZW50In0.8Ix-x4VWTsDKGeqGqjTzlVJk-P1OnD-ISn-zsQPQUG8",
"expires_in": 7199,
"scope": "all",
"jwt-ext": "JWT 扩展信息",
"jti": "daee69df-651a-452b-b04b-97aaa76293a8"
}
请求带新的 token,向 Oauth2 资源服务(客户端)请求数据:
#### 向 Oauth2 资源服务(客户端)请求数据
GET http://localhost:10801/api/member/service
Accept: */*
Cache-Control: no-cache
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib3JkZXJzLXNlcnZpY2UiLCJnYXRld2F5LXNlcnZpY2UiLCJtZW1iZXItc2VydmljZSIsInByb2R1Y3Qtc2VydmljZSJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTYxMjg1OTQxMSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJkYWVlNjlkZi02NTFhLTQ1MmItYjA0Yi05N2FhYTc2MjkzYTgiLCJjbGllbnRfaWQiOiJhcHAtY2xpZW50In0.F30LZplYodM7zH0N6gwBA29uCBObZISgOPXf-PKB3aI
得到请求结果:
Hello, Member!
四、向 Oauth2 资源服务(客户端)请求查看具体 jwt 的 token 解码内容:
#### 向 Oauth2 资源服务(客户端)请求查看具体 jwt 的 token 解码内容
GET http://localhost:10801/api/member/info/jwt
Accept: */*
Cache-Control: no-cache
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib3JkZXJzLXNlcnZpY2UiLCJnYXRld2F5LXNlcnZpY2UiLCJtZW1iZXItc2VydmljZSIsInByb2R1Y3Qtc2VydmljZSJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTYxMjg1OTQxMSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJkYWVlNjlkZi02NTFhLTQ1MmItYjA0Yi05N2FhYTc2MjkzYTgiLCJjbGllbnRfaWQiOiJhcHAtY2xpZW50In0.F30LZplYodM7zH0N6gwBA29uCBObZISgOPXf-PKB3aI
得到请求结果:
{
"aud": [
"orders-service",
"member-service",
"product-service"
],
"user_name": "admin",
"jwt-ext": "JWT 扩展信息",
"scope": [
"all"
],
"exp": 1612859411,
"authorities": [
"ROLE_ADMIN"
],
"jti": "daee69df-651a-452b-b04b-97aaa76293a8",
"client_id": "app-client"
}
2、Feign 微服务间调用认证踩坑
假设我们按照以上步骤,已经搭建完成了几乎相同的三个微服务:
- 会员服务:herring-member-service,微服务之一,接收到请求后会到认证中心验证。
- 订单服务:herring-orders-service,微服务之二,接收到请求后会到认证中心验证。
- 商品服务:herring-product-service,微服务之三,接收到请求后会到认证中心验证。
此时我写一个请求指向 member-service,但是 member-service 需要调用远程服务 orders-service 或者
product-service 才能返回正确的结果,如果不做 Oauth2 认证相信大家都没问题,如果我们做了 Oauth2 认证呢?我们现在就来尝试下。
1.添加 pom 文件依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.添加 @EnableFeignClients 注解:
@EnableFeignClients
@SpringBootApplication
public class MemberServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MemberServiceApplication.class, args);
}
}
3.member-service,orders-service,product-service 都添加以下类似接口:
@RestController
@RequestMapping
public class HelloController {
@Resource
private MemberService memberService;
@RequestMapping("/service")
public String service() {
return memberService.sayHello();
// return ordersService.sayHello();
// return productService.sayHello();
}
}
就是有个区分,访问 /api/member/xxx,返回对应的 Hello, xxx!
@Service
public class MemberService {
public String sayHello() {
return "Hello, Member! ";
// return "Hello, Orders! ";
// return "Hello, Product! ";
}
}
4.member-service 添加对 orders-service,product-service 远程访问的客户端:
@FeignClient(name = "orders-service", path = "/api/orders")
public interface OrdersClient {
@RequestMapping("/service")
String service();
}
@FeignClient(name = "product-service", path = "/api/product")
public interface ProductClient {
@RequestMapping("/service")
String service();
}
5.member-service 新增加一个接口 /api/member/hello:
@Resource
private MemberService memberService;
@Resource
private ProductClient productClient;
@Resource
private OrdersClient ordersClient;
@RequestMapping("/hello")
public String hello() {
String product = productClient.service();
String orders = ordersClient.service();
return memberService.sayHello() + product + orders;
}
6.带正确的 token,向 Oauth2 资源服务(客户端)请求数据 /api/member/hello:
#### 向 Oauth2 资源服务(客户端)请求数据
GET http://localhost:10801/api/member/hello
Accept: */*
Cache-Control: no-cache
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib3JkZXJzLXNlcnZpY2UiLCJnYXRld2F5LXNlcnZpY2UiLCJtZW1iZXItc2VydmljZSIsInByb2R1Y3Qtc2VydmljZSJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTYxMjg1OTQxMSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJkYWVlNjlkZi02NTFhLTQ1MmItYjA0Yi05N2FhYTc2MjkzYTgiLCJjbGllbnRfaWQiOiJhcHAtY2xpZW50In0.F30LZplYodM7zH0N6gwBA29uCBObZISgOPXf-PKB3aI
得到请求结果:
{
"timestamp": 1612855021645,
"status": 500,
"error": "Internal Server Error",
"message": "[401] during [GET] to [http://product-service/api/product/service] [ProductClient#service()]: [{\"error\":\"unauthorized\",\"error_description\":\"Full authentication is required to access this resource\"}]",
"path": "/api/member/hello"
}
为什么呢?我明明 http 请求头里带了正确的 token,却报 401 forbidden 的错误信息。
原因是 当我请求数据 /api/member/hello 时,虽然 http 请求头里带了正确的 token,但是在远程调用
orders-service,product-service 服务时,feign 新建的请求并不会带上这个 token,这是两个不同的
http 请求,所以就会导致 401 forbidden 的错误信息。
解决方案就是在 member-service,orders-service,product-service 都添加一个 RequestInterceptor:
public class TokenRelayRequestInterceptor implements RequestInterceptor {
public static final String AUTH_TOKEN = "Authorization";
@Override
public void apply(RequestTemplate template) {
// 获取该次请求得token 将token传递
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader(AUTH_TOKEN);
if (!StringUtils.isEmpty(token)) {
template.header(AUTH_TOKEN, token);
}
}
}
并且在 application.yml 中添加配置:
feign:
client:
config:
default:
requestInterceptors:
- com.herring.feign.interceptor.TokenRelayRequestInterceptor
7.带正确的 token,再次向 Oauth2 资源服务(客户端)请求数据 /api/member/hello:
#### 向 Oauth2 资源服务(客户端)请求数据
GET http://localhost:10801/api/member/hello
Accept: */*
Cache-Control: no-cache
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib3JkZXJzLXNlcnZpY2UiLCJnYXRld2F5LXNlcnZpY2UiLCJtZW1iZXItc2VydmljZSIsInByb2R1Y3Qtc2VydmljZSJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTYxMjg1OTQxMSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJkYWVlNjlkZi02NTFhLTQ1MmItYjA0Yi05N2FhYTc2MjkzYTgiLCJjbGllbnRfaWQiOiJhcHAtY2xpZW50In0.F30LZplYodM7zH0N6gwBA29uCBObZISgOPXf-PKB3aI
得到请求结果:
Hello, Member! Hello, Product! Hello, Orders!