Spring Cloud Security:Oauth2使用

1.OAuth2 简介

OAuth 2.0是用于授权的行业标准协议。OAuth 2.0为简化客户端开发提供了特定的授权流,包括Web应用、桌面应用、移动端应用等。

(1)OAuth2 相关名词解释

  • Resource owner(资源拥有者):拥有该资源的最终用户,他有访问资源的账号密码;
  • Resource server(资源服务器):拥有受保护资源的服务器,如果请求包含正确的访问令牌,可以访问资源;
  • Client(客户端):访问资源的客户端,会使用访问令牌去获取资源服务器的资源,可以是浏览器、移动设备或者服务器;
  • Authorization server(认证服务器):用于认证用户的服务器,如果客户端认证通过,发放访问资源服务器的令牌。

(2)四种授权模式

  • Authorization Code(授权码模式):正宗的OAuth2的授权模式,客户端先将用户导向认证服务器,登录后获取授权码,然后进行授权,最后根据授权码获取访问令牌;
  • Implicit(简化模式):和授权码模式相比,取消了获取授权码的过程,直接获取访问令牌;
  • Resource Owner Password Credentials(密码模式):客户端直接向用户获取用户名和密码,之后向认证服务器获取访问令牌;
  • Client Credentials(客户端模式):客户端直接通过客户端认证(比如client_id和client_secret)从认证服务器获取访问令牌。(本文采用的是这种模式)

(3)两种常用的授权模式

1.授权码模式

springcloud eureka defaultZone配置 springcloud oauth2_Oauth2

  • (A)客户端将用户导向认证服务器;
  • (B)用户在认证服务器进行登录并授权;
  • ©认证服务器返回授权码给客户端;
  • (D)客户端通过授权码和跳转地址向认证服务器获取访问令牌;
  • (E)认证服务器发放访问令牌(有需要带上刷新令牌)。
2.密码模式

springcloud eureka defaultZone配置 springcloud oauth2_java_02

  • (A)客户端从用户获取用户名和密码;
  • (B)客户端通过用户的用户名和密码访问认证服务器;
  • ©认证服务器返回访问令牌(有需要带上刷新令牌)。

启动这三个服务 api远程调用服务 auth 认证服务 gateway 网关服务

springcloud eureka defaultZone配置 springcloud oauth2_Oauth2_03

2.开始进行服务调试运行

2.1登录接口

http://localhost:9201/auth/oauth/token

springcloud eureka defaultZone配置 springcloud oauth2_服务器_04

首先源代码

springcloud eureka defaultZone配置 springcloud oauth2_spring_05

我们进行改造。其实差不多的就是重写该方法,目的是我们将输出的数据封装到我们自定义返回类型中的data中去

@RestController
@RequestMapping("/oauth")
public class AuthController {

    @Autowired
    private TokenEndpoint tokenEndpoint;

    private Set<HttpMethod> allowedRequestMethods;

    /**
     * Oauth2登录认证
     */
    @RequestMapping(value = "/token", method = RequestMethod.POST)
    public CommonResult<Oauth2TokenDto> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        if (!this.allowedRequestMethods.contains(HttpMethod.POST)) {
            return CommonResult.failed("请求方法必须是POST");
        }
        OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
        Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder()
                .token(oAuth2AccessToken.getValue())
                .refreshToken(oAuth2AccessToken.getRefreshToken().getValue())
                .expiresIn(oAuth2AccessToken.getExpiresIn())
                .tokenHead("Bearer ").build();

        return CommonResult.success(oauth2TokenDto);
    }
}

调用接口后:

springcloud eureka defaultZone配置 springcloud oauth2_客户端_06

2.2 oauth2调用接口的传参讲解,以下面登录接口为例子

springcloud eureka defaultZone配置 springcloud oauth2_Oauth2_07

先看下代码分析

springcloud eureka defaultZone配置 springcloud oauth2_客户端_08

  • 当我们输错密码,再次调用登录认证接口时,发现认证失败的结果也统一了。

怎么弄的?上代码

springcloud eureka defaultZone配置 springcloud oauth2_服务器_09

看了源码,发现异常处理都是有这个类OAuth2Exception去处理,这时候我们自定义异常处理类去接受即可。

2.3自定义网关鉴权失败结果

  • 当我们使用过期或签名不正确的JWT令牌访问需要权限的接口时,会直接返回状态码401

首先看代码

springcloud eureka defaultZone配置 springcloud oauth2_java_10

springcloud eureka defaultZone配置 springcloud oauth2_Oauth2_11

打断点发现 这个格式都是自己自定义的

{

  "code": 401,

  "message": "暂未登录或token已经过期",

  "data": "Jwt expired at 2022-07-27T08:47:51Z"

}

2.4兼容白名单接口

  • 其实对于白名单接口一直有个问题,当携带过期或签名不正确的JWT令牌访问时,会直接返回token过期的结果,我们可以访问下登录认证接口试试;

springcloud eureka defaultZone配置 springcloud oauth2_Oauth2_12

  • 明明就是个白名单接口,只不过携带的token不对就不让访问了,显然有点不合理。如何解决呢,我们先看看不带token访问怎么样;
  • 其实我们只要在Oauth2默认的认证过滤器前面再加个过滤器,如果是白名单接口,直接移除认证头即可,首先定义好我们的过滤器;
/**
 * 白名单路径访问时需要移除JWT请求头
 * Created by hjt on 2020/7/24.
 */
@Component
public class IgnoreUrlsRemoveJwtFilter implements WebFilter {
    @Autowired
    private IgnoreUrlsConfig ignoreUrlsConfig;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        URI uri = request.getURI();
        PathMatcher pathMatcher = new AntPathMatcher();
        //白名单路径移除JWT请求头
        List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
        for (String ignoreUrl : ignoreUrls) {
            if (pathMatcher.match(ignoreUrl, uri.getPath())) {
                //请求不带Authorization
                request = exchange.getRequest().mutate().header("Authorization", "").build();
                exchange = exchange.mutate().request(request).build();
                return chain.filter(exchange);
            }
        }
        return chain.filter(exchange);
    }
}
  • 然后把这个过滤器配置到默认的认证过滤器之前即可,在ResourceServerConfig中进行配置;
/**
 * 资源服务器配置
 * Created by hjt on 2020/6/19.
 */
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
    private final AuthorizationManager authorizationManager;
    private final IgnoreUrlsConfig ignoreUrlsConfig;
    private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
    private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.oauth2ResourceServer().jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
        //自定义处理JWT请求头过期或签名错误的结果
        http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
        //对白名单路径,直接移除JWT请求头 必须设置在默认的认证过滤器前面
        http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION);
        //默认的认证过滤器
        http.authorizeExchange()
                .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//白名单配置
                .anyExchange().access(authorizationManager)//鉴权管理器配置
                .and().exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)//处理未授权
                .authenticationEntryPoint(restAuthenticationEntryPoint)//处理未认证
                .and().csrf().disable();
        return http.build();
    }

}