乐趣区

关于java:我扒了半天源码终于找到了Oauth2自定义处理结果的最佳方案

SpringBoot 实战电商我的项目 mall(35k+star)地址:https://github.com/macrozheng/mall

摘要

在《微服务权限终极解决方案,Spring Cloud Gateway + Oauth2 实现对立认证和鉴权!》一文中咱们介绍了 Oauth2 在微服务中的应用,然而咱们没有自定义 Oauth2 默认的处理结果。有时候咱们真的很心愿 Oauth2 中的认证受权能返回咱们指定格局的后果,比方登录认证的后果、网关鉴权不通过的后果等等。本文将具体介绍 Oauth2 中自定义处理结果的计划,心愿对大家有所帮忙!

解决什么问题

自定义 Oauth2 处理结果,次要是为了对立接口返回信息的格局,从上面几个方面着手。

  • 自定义 Oauth2 登录认证胜利和失败的返回后果;
  • JWT 令牌过期或者签名不正确,网关认证失败的返回后果;
  • 携带过期或者签名不正确的 JWT 令牌拜访白名单接口,网关间接认证失败。

自定义登录认证后果

认证胜利返回后果

  • 咱们先来看看默认的返回后果,拜访 Oauth2 登录认证接口:http://localhost:9201/auth/oauth/token

  • 咱们之前应用的都是对立的通用返回后果CommonResult,Oauth2 的这个后果显然不合乎,须要对立下,通用返回后果格局如下;
/**
 * 通用返回对象
 * Created by macro on 2019/4/19.
 */
public class CommonResult<T> {
    private long code;
    private String message;
    private T data;
}
  • 其实咱们只有找到一个要害类就能够自定义 Oauth2 的登录认证接口了,它就是org.springframework.security.oauth2.provider.endpoint.TokenEndpoint, 其中定义了咱们十分相熟的登录认证接口,咱们只有本人重写登录认证接口,间接调用默认的实现逻辑,而后把默认返回的后果解决下即可,上面是默认的实现逻辑;
@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
    Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {if (!(principal instanceof Authentication)) {
            throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter.");
        }

        String clientId = getClientId(principal);
        ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

        TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

        if (clientId != null && !clientId.equals("")) {
            // Only validate the client details if a client authenticated during this
            // request.
            if (!clientId.equals(tokenRequest.getClientId())) {
                // double check to make sure that the client ID in the token request is the same as that in the
                // authenticated client
                throw new InvalidClientException("Given client ID does not match authenticated client");
            }
        }
        if (authenticatedClient != null) {oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
        }
        if (!StringUtils.hasText(tokenRequest.getGrantType())) {throw new InvalidRequestException("Missing grant type");
        }
        if (tokenRequest.getGrantType().equals("implicit")) {throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
        }

        if (isAuthCodeRequest(parameters)) {
            // The scope was requested or determined during the authorization step
            if (!tokenRequest.getScope().isEmpty()) {logger.debug("Clearing scope of incoming token request");
                tokenRequest.setScope(Collections.<String> emptySet());
            }
        }

        if (isRefreshTokenRequest(parameters)) {
            // A refresh token has its own default scopes, so we should ignore any added by the factory here.
            tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
        }

        OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
        if (token == null) {throw new UnsupportedGrantTypeException("Unsupported grant type:" + tokenRequest.getGrantType());
        }

        return getResponse(token);

    }
}
  • 咱们将须要的 JWT 信息封装成对象,而后放入到咱们的通用返回后果的 data 属性中去;
/**
 * Oauth2 获取 Token 返回信息封装
 * Created by macro on 2020/7/17.
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Builder
public class Oauth2TokenDto {
    /**
     * 拜访令牌
     */
    private String token;
    /**
     * 刷新令牌
     */
    private String refreshToken;
    /**
     * 拜访令牌头前缀
     */
    private String tokenHead;
    /**
     * 无效工夫(秒)*/
    private int expiresIn;
}
  • 创立一个AuthController,自定义实现 Oauth2 默认的登录认证接口;
/**
 * 自定义 Oauth2 获取令牌接口
 * Created by macro on 2020/7/17.
 */
@RestController
@RequestMapping("/oauth")
public class AuthController {

    @Autowired
    private TokenEndpoint tokenEndpoint;

    /**
     * Oauth2 登录认证
     */
    @RequestMapping(value = "/token", method = RequestMethod.POST)
    public CommonResult<Oauth2TokenDto> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {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);
    }
}
  • 再次调用登录认证接口,咱们能够发现返回后果曾经变成了合乎咱们通用返回后果的格局了!

认证失败返回后果

  • 认证胜利的后果对立了,认证失败的后果咱们也得对立下吧,先来看下原来认证失败的后果;

  • 咱们认真查看下登录认证的默认实现能够发现,很多认证失败的操作都会间接抛出 OAuth2Exception 异样,对于在 Controller 中抛出的异样,咱们能够应用 @ControllerAdvice 注解来进行全局解决;
/**
 * 全局解决 Oauth2 抛出的异样
 * Created by macro on 2020/7/17.
 */
@ControllerAdvice
public class Oauth2ExceptionHandler {
    @ResponseBody
    @ExceptionHandler(value = OAuth2Exception.class)
    public CommonResult handleOauth2(OAuth2Exception e) {return CommonResult.failed(e.getMessage());
    }
}
  • 当咱们输错明码,再次调用登录认证接口时,发现认证失败的后果也对立了。

自定义网关鉴权失败后果

  • 当咱们应用过期或签名不正确的 JWT 令牌拜访须要权限的接口时,会间接返回状态码401

  • 这个返回后果不合乎咱们的通用后果格局,其实咱们想要的是返回状态码为200,而后返回如下格局信息;
{
    "code": 401,
    "data": "Jwt expired at 2020-07-10T08:38:40Z",
    "message": "暂未登录或 token 曾经过期"
}
  • 这里有个非常简单的改法,只需增加一行代码,批改网关的平安配置 ResourceServerConfig,设置好资源服务器的ServerAuthenticationEntryPoint 即可;
/**
 * 资源服务器配置
 * Created by macro 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;

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {http.oauth2ResourceServer().jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
        // 自定义解决 JWT 申请头过期或签名谬误的后果(新增加的)http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
        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();}
}
  • 增加实现后,再次拜访须要权限的接口,就会返回咱们想要的后果了。

兼容白名单接口

  • 其实对于白名单接口始终有个问题,当携带过期或签名不正确的 JWT 令牌拜访时,会间接返回 token 过期的后果,咱们能够拜访下登录认证接口试试;

  • 明明就是个白名单接口,只不过携带的 token 不对就不让拜访了,显然有点不合理。如何解决呢,咱们先看看不带 token 拜访怎么样;

  • 其实咱们只有在 Oauth2 默认的认证过滤器后面再加个过滤器,如果是白名单接口,间接移除认证头即可,首先定义好咱们的过滤器;
/**
 * 白名单门路拜访时须要移除 JWT 申请头
 * Created by macro 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())) {request = exchange.getRequest().mutate().header("Authorization", "").build();
                exchange = exchange.mutate().request(request).build();
                return chain.filter(exchange);
            }
        }
        return chain.filter(exchange);
    }
}
  • 而后把这个过滤器配置到默认的认证过滤器之前即可,在 ResourceServerConfig 中进行配置;
/**
 * 资源服务器配置
 * Created by macro 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();}

}
  • 携带过期申请头再次拜访,发现曾经能够失常拜访了。

总结

至此,微服务中应用 Oauth2 实现对立认证和鉴权计划终于欠缺了!

我的项目源码地址

https://github.com/macrozheng…

本文 GitHub https://github.com/macrozheng/mall-learning 曾经收录,欢送大家 Star!

退出移动版