OAuth2 企业级实践指南:多个微服务配置的实现

133 阅读6分钟

项目背景

本项目是基于 OAuth2.0 实现的微服务认证与授权系统,旨在为多个微服务模块提供统一的认证、授权功能。项目采用 UAA 服务 作为认证服务器,负责用户登录、令牌的生成与管理;同时利用网关服务(Gateway)作为资源服务器入口,对各微服务的请求进行安全保护。
OAuth2.0 在本项目中主要采用 密码模式(Password Grant Type) 实现,适用于可信任的客户端与用户直接交互的场景。项目的主要目标是通过分布式认证、动态权限验证以及高效的令牌管理,保证服务之间的安全调用和灵活的权限控制。


UAA 服务的配置

1. 核心配置:AuthorizationServerConfiguration

UAA 服务实现了 OAuth2 的授权服务器扩展功能,配置如下:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private DataSource dataSource;
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    @Qualifier(value = "clientDetailsServiceImpl")
    private ClientDetailsService customClientDetailsService;

    @Bean
    public TokenStore tokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }

    @Bean
    public ApprovalStore approvalStore() {
        return new JdbcApprovalStore(dataSource);
    }

    @Bean
    public TokenEnhancer tokenEnhancer() {
        return new OpenTokenEnhancer();
    }

    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(customClientDetailsService);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
                .authenticationManager(authenticationManager)
                .approvalStore(approvalStore())
                .userDetailsService(userDetailsService)
                .tokenServices(createDefaultTokenServices())
                .accessTokenConverter(OpenHelper.buildAccessTokenConverter())
                .authorizationCodeServices(authorizationCodeServices());
    }

    private DefaultTokenServices createDefaultTokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setTokenEnhancer(tokenEnhancer());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setReuseRefreshToken(true);
        tokenServices.setClientDetailsService(customClientDetailsService);
        return tokenServices;
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();
    }
}

2. 关键功能

  • RedisTokenStore:将令牌存储在 Redis 中。
  • JdbcApprovalStore:基于 JDBC 的用户授权存储。
  • 自定义 ClientDetailsService:支持动态管理客户端信息。
  • 异常处理:支持自定义授权和错误页面。

3. 登录接口

UAA 服务中登录接口的作用是处理用户登录,并根据用户的身份信息生成访问令牌(Access Token)。以下为接口的详细说明:

/**
 * 获取用户访问令牌
 * 基于OAuth2密码模式登录
 *
 * @param username 用户名
 * @param password 密码
 * @return 返回包含访问令牌的响应
 */
@ApiOperation(value = "登录获取用户访问令牌", notes = "基于OAuth2密码模式登录,无需签名,返回access_token")
@ApiImplicitParams({
        @ApiImplicitParam(name = "username", required = true, value = "登录名", paramType = "form"),
        @ApiImplicitParam(name = "password", required = true, value = "登录密码", paramType = "form")
})
@PostMapping("/login/token")
public Object getLoginToken(
        @RequestParam String username, 
        @RequestParam String password, 
        @RequestHeader HttpHeaders httpHeaders) throws Exception {
    
    Map<String, Object> result = getToken(username, password, "admin", httpHeaders);
    if (result.containsKey("access_token")) {
        return ResultBody.ok().data(result);  // 返回成功响应
    } else {
        return result;  // 返回错误信息
    }
}
private JSONObject getToken(String username, String password, String type, HttpHeaders headers) {
    OpenOAuth2ClientDetails clientDetails = clientProperties.getOauth2().get("admin");
    String url = WebUtils.getServerUrl(WebUtils.getHttpServletRequest()) + "/oauth/token";

    // 构建请求参数
    MultiValueMap<String, Object> postParameters = new LinkedMultiValueMap<>();
    postParameters.add("username", username);
    postParameters.add("password", password);
    postParameters.add("client_id", clientDetails.getClientId());
    postParameters.add("client_secret", clientDetails.getClientSecret());
    postParameters.add("grant_type", "password");  // 密码模式
    postParameters.add("login_type", type);

    // 设置请求头
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    headers.remove(HttpHeaders.AUTHORIZATION);

    // 发起请求
    HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(postParameters, headers);
    return restTemplate.postForObject(url, request, JSONObject.class);
}

说明

  • getToken() 方法是登录接口的核心逻辑,通过 RestTemplate 发起 HTTP POST 请求,与 OAuth2.0 /oauth/token 端点交互。

  • 该方法使用客户端的 client_idclient_secret,配合用户凭据完成认证。


网关服务的配置

网关服务在整个微服务架构中扮演了重要角色,其主要职责是作为 OAuth2.0 资源服务器,负责对外部请求进行过滤、验证以及路由转发。通过对网关服务的配置,可以实现对所有微服务的统一安全保护和动态权限控制。

本网关服务的职责与核心功能

  1. 请求过滤

    • 对进入网关的请求进行预处理,包括跨域处理、签名验证、访问限制等。
    • 防止非法请求进入微服务内网。
  2. 认证与授权

    • 作为资源服务器,验证客户端提交的 access_token
    • 动态解析权限并对资源访问进行控制,确保只有经过授权的用户能够访问指定的资源。
  3. 统一路由转发

    • 根据配置的路由规则,将请求转发到对应的微服务模块。
    • 支持动态路由更新,便于扩展和维护。
  4. 安全日志记录

    • 记录请求的访问日志、认证日志和异常信息,用于后续分析和审计。

1. 核心配置:ResourceServerConfiguration

@Configuration
public class ResourceServerConfiguration {

    private static final String MAX_AGE = "18000L";

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    @Autowired
    private ResourceLocator apiresourceLocator;
    @Autowired
    private ApiProperties apiProperties;

    // 为跨域请求添加必要的响应头,支持跨域访问
    @Bean
    public WebFilter corsFilter() {
        return (ServerWebExchange ctx, WebFilterChain chain) -> {
            ServerHttpRequest request = ctx.getRequest();
            if (CorsUtils.isCorsRequest(request)) {
                HttpHeaders requestHeaders = request.getHeaders();
                ServerHttpResponse response = ctx.getResponse();
                HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
                HttpHeaders headers = response.getHeaders();
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
                headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
                if (requestMethod != null) {
                    headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
                }
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
                headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
                headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);
                if (request.getMethod() == HttpMethod.OPTIONS) {
                    response.setStatusCode(HttpStatus.OK);
                    return Mono.empty();
                }
            }
            return chain.filter(ctx);
        };
    }

    
    // 为网关服务添加 OAuth2.0 认证功能,验证请求中携带的 `access_token`。
    @Bean
    SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception {
        // 自定义oauth2 认证, 使用redis读取token,而非jwt方式
        JsonAuthenticationEntryPoint entryPoint = new JsonAuthenticationEntryPoint(accessLogService);
        JsonAccessDeniedHandler accessDeniedHandler = new JsonAccessDeniedHandler(accessLogService);
        AccessManager accessManager = new AccessManager(apiresourceLocator, apiProperties);
        // 通过 `RedisTokenStore` 实现令牌的分布式存储与管理。
        RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
        RedisAuthenticationManager redisAuthenticationManager = new RedisAuthenticationManager(tokenStore);
        AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(redisAuthenticationManager);
        oauth2.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());
        oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
        oauth2.setAuthenticationSuccessHandler(new ServerAuthenticationSuccessHandler() {
            @Override
            public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
                ServerWebExchange exchange = webFilterExchange.getExchange();
                SecurityContextServerWebExchange securityContextServerWebExchange = new SecurityContextServerWebExchange(exchange, ReactiveSecurityContextHolder.getContext().subscriberContext(
                        ReactiveSecurityContextHolder.withAuthentication(authentication)
                ));
                return webFilterExchange.getChain().filter(securityContextServerWebExchange);
            }
        });
        // 动态管理权限规则,根据请求路径和用户权限动态决定是否放行。
        http
                .httpBasic().disable()
                .csrf().disable()
                .authorizeExchange()
                .pathMatchers("/").permitAll()
                // 动态权限验证
                .anyExchange().access(accessManager)
                .and().exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(entryPoint).and()
                // 日志前置过滤器
                .addFilterAt(new PreRequestFilter(baseAppServiceClient), SecurityWebFiltersOrder.FIRST)
                // 跨域过滤器
                .addFilterAt(corsFilter(), SecurityWebFiltersOrder.CORS)
                // 签名验证过滤器
                .addFilterAt(new PreSignatureFilter(baseAppServiceClient, apiProperties, new JsonSignatureDeniedHandler(accessLogService)), SecurityWebFiltersOrder.CSRF)
                // 签名验证过滤器2
                .addFilterAt(new SignatureFilter(baseAppServiceClient, new JsonSignatureDeniedHandler(accessLogService)), SecurityWebFiltersOrder.CSRF)
                // 访问验证前置过滤器
                .addFilterAt(new PreCheckFilter(tokenStore, accessManager, accessDeniedHandler, apiProperties), SecurityWebFiltersOrder.CSRF)
                // oauth2认证过滤器
                .addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION)
                .addFilterAt(new RateLimitFilter(baseAppServiceClient, repository, apiProperties, new RateLimiter(redisTemplate), new JsonRateLimitExceptionHandler(accessLogService), new JsonSignatureDeniedHandler(accessLogService)), SecurityWebFiltersOrder.CSRF)
                // 日志过滤器
                .addFilterAt(new AccessLogFilter(accessLogService), SecurityWebFiltersOrder.SECURITY_CONTEXT_SERVER_WEB_EXCHANGE);
        return http.build();
    }

}

2. 关键功能

  • 跨域配置 (CORS) :支持跨域请求。

  • 令牌存储与验证:基于 Redis 实现令牌存储和验证,使用 RedisAuthenticationManager 进行令牌解析。

  • 动态权限验证:通过 AccessManager 实现资源的动态权限管理。

  • AccessManager

    • 从 Redis 或数据库中加载权限信息,与请求路径进行匹配。
    • 如果用户的权限不满足要求,则拒绝访问。

网关服务的 OAuth2.0 认证逻辑大致分为以下几个步骤:

  1. 请求到达网关

    • 对请求进行跨域处理。
    • 执行签名验证、访问限制等预处理逻辑。
  2. 提取并验证令牌

    • 从请求头中提取 Bearer 类型的令牌。
    • 使用 Redis 中存储的令牌信息进行验证。
  3. 权限校验

    • 根据用户的权限信息和请求路径进行动态匹配。
    • 验证通过后,将请求转发至目标微服务。
  4. 异常处理

    • 如果认证或授权失败,返回对应的错误响应。

OAuth2 工作流程

  1. 用户通过 UAA 服务登录,使用 /oauth/token 接口获取访问令牌。
  2. 用户携带访问令牌访问网关服务。
  3. 网关服务通过 RedisAuthenticationManager 验证令牌。
  4. 验证通过后,用户请求被转发到对应的微服务。
  5. 请求被处理后返回响应。

示例: 请求

POST /login/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

username=user1
password=123456

响应

{
  "code": 200,
  "message": "成功",
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "bearer",
    "expires_in": 3600,
    "scope": "read write"
  }
}