519、Java Spring Cloud Alibaba -【Spring Cloud Alibaba Oauth2 - 下】 2021.11.01

132 篇文章 0 订阅

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! 

3、参考链接

[01] Spring Cloud Alibaba Oauth2

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值