- 1、搭建 Oauth2 资源服务(客户端)
- 2、Feign 微服务间调用认证踩坑
- 3、参考链接
搭建下边几个服务任选一个:
会员服务:herring-member-service,微服务之一,接收到请求后会到认证中心验证。
订单服务:herring-orders-service,微服务之二,接收到请求后会到认证中心验证。
商品服务:herring-product-service,微服务之三,接收到请求后会到认证中心验证。
添加 pom 文件依赖:
org.springframework.boot spring-boot-starter-weborg.springframework.cloud spring-cloud-starter-oauth2
如果在 jwt 中加入了额外信息,而在接收到 jwt 格式的 token 之后,用户客户端要把 jwt 解析出来。
io.jsonwebtoken jjwt0.9.1
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
得到请求结果:
{
"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 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 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
得到请求结果:
{
"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!3、参考链接
[01] Spring Cloud Alibaba Oauth2



