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
,其中定义了咱们十分相熟的登录认证接口,咱们只有本人重写登录认证接口,间接调用默认的实现逻辑,而后把默认返回的后果解决下即可,上面是默认的实现逻辑;
@FrameworkEndpointpublic 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)@Builderpublic 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. */@ControllerAdvicepublic 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@EnableWebFluxSecuritypublic 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. */@Componentpublic 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@EnableWebFluxSecuritypublic 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!