使用 Spring Security + WSO2 身份服务器的 OAuth 2.0

我正在开发一个 Web 应用程序来公开许多受 OAuth 2.0 保护的 RESTful 服务.这是计划中的架构:

I'm developing a web application to expose a number of RESTful services secured by OAuth 2.0. Here is the planned architecture:

1- OAuth 授权提供者: WSO2 身份服务器 (IS)

1- OAuth Authorization Provider: WSO2 Identity Server (IS)

2- OAuth 资源服务器: 使用以下技术的 Java Web 应用程序:

2- OAuth Resource Server: Java web application using the following technologies:

  • Jersey(用于实现和公开 Web 服务)
  • Spring Security(实现 OAuth 资源服务器部分)

我见过几个例子(ex1, ex2, ex3, etc...) 关于如何使用 WSO2 IS 作为授权服务器 + WSO2 ESB 作为资源服务器来保护 RESTful 服务.这不是我需要的.

I've seen several examples (ex1, ex2, ex3, etc...) on how to secure RESTful services using WSO2 IS as an authorization server + WSO2 ESB as a resource server. This is NOT what I need in my case.

很遗憾,授权服务器和资源服务器的交互超出了OAuth2 RFC.所以,我找不到太多关于它应该是什么样子的信息.

Unfortunately, the interaction between the authorization server and the resource server is beyond the scope of the OAuth2 RFC. So, I couldn't find much about how should it look like.

这是我的问题:

  • 如何配置 Spring Security 作为资源服务器来验证由外部 OAuth 提供者(例如 WSO2 IS)颁发的访问令牌?
  • 资源服务器应如何识别给定访问令牌的范围?
  • 如何识别来自 WSO2 IS 的访问令牌的资源所有者?

谢谢

推荐答案

经过一番研究,我想出了如何去做.解决方案主要分为 2 个部分:WSO2 IS 配置 &资源服务器配置.

After doing some research, I figured out how to do it. The solution is divided into 2 main parts: WSO2 IS configuration & Resources server configuration.

基本场景如下:

1- 客户端(例如移动应用程序)通过向资源服务器(在我的例子中是 Java Web 应用程序)发送请求来使用受保护的资源(例如 Web 服务).

1- A client (e.g. mobile app) consume a secured resource (e.g. web service) by sending a request to the resources sever (Java web application in my case).

2- 资源服务器验证请求中的授权"标头并提取访问令牌.

2- The resources server validates the "Authorization" header in the request and extracts the access token.

3- 资源服务器通过将访问令牌发送到授权服务器 (WSO2 IS) 来验证访问令牌.

3- The resources server validates the access token by sending it to the authorization server (WSO2 IS).

4- 授权服务器响应验证响应.

4- The authorization server responds with validation response.

5- 资源服务器验证响应并决定是授予还是拒绝访问所请求的资源.

5- The resources server validates the response and decides whether to grant or deny access to the requested resource.

在我的演示中,我使用了 WSO2 IS 5.0.0 和 Spring security 3.1.0.

In my demo, I used WSO2 IS 5.0.0 and Spring security 3.1.0.

WSO2 IS 将充当授权服务器.因此,它应该配置为支持 OAuth 2.0.为此,应按如下方式添加和配置新的服务提供者:

WSO2 IS will act as the authorization server. So, it should be configured to support OAuth 2.0. To do so, a new service provider should be added and configured as follows:

(a) 登录 WSO2 IS 管理控制台.

(a) Login to WSO2 IS management console.

(b) 添加新的服务提供者并为其命名和描述.

(b) Add a new service provider and give it a name and description.

(c) 在Inbound Authentication Configuration >> OAuth/OpenID Connect Configuration >> 点击Configure.

(c) Under Inbound Authentication Configuration >> OAuth/OpenID Connect Configuration >> Click Configure.

(d) 如下图所示配置 OAuth 2.0 提供程序,然后单击添加.我们需要 Password 授权类型,它映射到 Resource Owner Password Credentials 授权类型.它最适合我的情况(保护 Web 服务).

(d) Configure OAuth 2.0 provider as shown in the below screenshot and click Add. We'll need Password grant type which maps to Resource Owner Password Credentials grant type. It is best suited for my case (securing web services).

(e) 在 OAuth/OpenID Connect Configuration 下,您会发现 OAuth Client Key 和 OAuth Client Secret 已生成.它们与用户名、密码和范围一起用于生成访问令牌.

(e) Under OAuth/OpenID Connect Configuration, you'll find OAuth Client Key and OAuth Client Secret generated. They are used along with username, password, and scope to generate access tokens.

如前所述,演示 Java Web 应用程序将同时充当资源服务器和客户端.作为资源服务器,Spring security 需要知道如何验证访问令牌.因此,应该提供令牌服务实现.

As mentioned earlier, the demo Java web application will act as Resources server and client at the same time. To act as resources server, Spring security needs to know how to validate access tokens. So, a token services implementation should be provided.

(a) 配置spring作为资源服务器.这是一个示例配置:

(a) Configure spring to act as resources server. Here is a sample configuration:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:security="http://www.springframework.org/schema/security"
   xmlns:oauth2="http://www.springframework.org/schema/security/oauth2"
   xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-3.1.xsd
    http://www.springframework.org/schema/security/oauth2
    http://www.springframework.org/schema/security/spring-security-oauth2.xsd">

    <bean id="tokenServices" class="com.example.security.oauth2.wso2.TokenServiceWSO2" />

    <bean id="authenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint" />

    <security:authentication-manager alias="authenticationManager" />

    <oauth2:resource-server id="resourcesServerFilter" token-services-ref="tokenServices" />

    <security:http pattern="/services/**" create-session="stateless" entry-point-ref="authenticationEntryPoint" >
        <security:anonymous enabled="false" />
        <security:custom-filter ref="resourcesServerFilter" before="PRE_AUTH_FILTER" />
        <security:intercept-url pattern="/services/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
    </security:http>
</beans>

这里配置了一个使用令牌服务实现TokenServiceWSO2的resource-server.resource-server 标签实际上被转换为一个安全过滤器.拦截模式被添加到/services/**",资源服务器过滤器被添加到链中.

Here, a resource-server that uses a token services implementation TokenServiceWSO2 is configured. The resource-server tag is actually transformed to a security filter. An interception pattern is added to "/services/**" and the resources sever filter is added to the chain.

(b) 实施 OAuth 2.0 令牌服务 ResourceServerTokenServices.该实现将访问令牌作为输入,将其传递给 WSO2 IS 公开的 OAuth2TokenValidationService 服务,验证响应并返回一个处理后的对象,其中包含有关令牌颁发者、有效性、范围、对应的基本数据JWT 令牌,...

(b) Implement OAuth 2.0 token services ResourceServerTokenServices. The implementation will take an access token as an input, pass it to OAuth2TokenValidationService service exposed by WSO2 IS, validate the response and return a processed object containing the basic data about the token's issuer, validity, scope, corresponding JWT token, ...

public class TokenServiceWSO2 implements ResourceServerTokenServices {

    @Autowired
    TokenValidatorWSO2 tokenValidatorWSO2;

    public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {

        try {
            TokenValidationResponse validationResponse = tokenValidatorWSO2.validateAccessToken(accessToken);
            OAuth2Request oAuth2Request = new OAuth2Request(null, null, null, true, validationResponse.getScope(), null, null, null,null);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(validationResponse.getAuthorizedUserIdentifier(), null, null);
            OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
            return oAuth2Authentication;
        } catch (ApplicationException ex) {
            // Handle exception
        }
    }

    public OAuth2AccessToken readAccessToken(String accessToken) {
        // TODO Add implementation
    }

}

TokenValidatorWSO2类实现调用WSO2 IS的web服务OAuth2TokenValidationService

TokenValidatorWSO2 class implements the logic to call WSO2 IS's web service OAuth2TokenValidationService

@Component
public class TokenValidatorWSO2 implements OAuth2TokenValidator{

    private static final Logger logger = Logger.getLogger(TokenValidatorWSO2.class);

    @Value("${server_url}")
    private String serverUrl;

    @Value("${validation_service_name}")
    private String validationServiceName;

    @Value("${comsumer_key}")
    private String consumerKey;

    @Value("${admin_username}")
    private String adminUsername;

    @Value("${admin_password}")
    private String adminPassword;

    private OAuth2TokenValidationServiceStub stub;

    private static final int TIMEOUT_IN_MILLIS = 15 * 60 * 1000;

    public TokenValidationResponse validateAccessToken(String accessToken) throws ApplicationException {
        logger.debug("validateAccessToken(String) - start");

        if(stub == null) {
            initializeValidationService();
        }

        OAuth2TokenValidationRequestDTO  oauthRequest;
        TokenValidationResponse validationResponse;
        OAuth2TokenValidationRequestDTO_OAuth2AccessToken oAuth2AccessToken;

        try {
            oauthRequest = new OAuth2TokenValidationRequestDTO();
            oAuth2AccessToken = new OAuth2TokenValidationRequestDTO_OAuth2AccessToken();
            oAuth2AccessToken.setIdentifier(accessToken);
            oAuth2AccessToken.setTokenType("bearer");
            oauthRequest.setAccessToken(oAuth2AccessToken);
            OAuth2TokenValidationResponseDTO response = stub.validate(oauthRequest);

            if(!response.getValid()) {
                throw new ApplicationException("Invalid access token");
            }

            validationResponse = new TokenValidationResponse();
            validationResponse.setAuthorizedUserIdentifier(response.getAuthorizedUser());
            validationResponse.setJwtToken(response.getAuthorizationContextToken().getTokenString());
            validationResponse.setScope(new LinkedHashSet<String>(Arrays.asList(response.getScope())));
            validationResponse.setValid(response.getValid());

        } catch(Exception ex) {
            logger.error("validateAccessToken() - Error when validating WSO2 token, Exception: {}", ex);
        }

        logger.debug("validateAccessToken(String) - end");
        return validationResponse;
    }

    private void initializeValidationService() throws ApplicationException {
        try {
            String serviceURL = serverUrl + validationServiceName;
            stub = new OAuth2TokenValidationServiceStub(null, serviceURL);
            CarbonUtils.setBasicAccessSecurityHeaders(adminUsername, adminPassword, true, stub._getServiceClient());
            ServiceClient client = stub._getServiceClient();
            Options options = client.getOptions();
            options.setTimeOutInMilliSeconds(TIMEOUT_IN_MILLIS);
            options.setProperty(HTTPConstants.SO_TIMEOUT, TIMEOUT_IN_MILLIS);
            options.setProperty(HTTPConstants.CONNECTION_TIMEOUT, TIMEOUT_IN_MILLIS);
            options.setCallTransportCleanup(true);
            options.setManageSession(true);
        } catch(AxisFault ex) {
            // Handle exception
        }
    }
}

TokenValidationResponse 类保存令牌验证响应中返回的基本数据.

TokenValidationResponse class holds the basic data returned in token validation response.

public class TokenValidationResponse {

    private String jwtToken;
    private boolean valid;
    private Set<String> scope;
    private String authorizedUserIdentifier;

    public String getJwtToken() {
        return jwtToken;
    }

    public void setJwtToken(String jwtToken) {
        this.jwtToken = jwtToken;
    }

    public boolean isValid() {
        return valid;
    }

    public void setValid(boolean valid) {
        this.valid = valid;
    }

    public Set<String> getScope() {
        return scope;
    }

    public void setScope(Set<String> scope) {
        this.scope = scope;
    }

    public String getAuthorizedUserIdentifier() {
        return authorizedUserIdentifier;
    }

    public void setAuthorizedUserIdentifier(String authorizedUserIdentifier) {
        this.authorizedUserIdentifier = authorizedUserIdentifier;
    }
}

<小时>

3- 客户端应用配置

最后一步是配置要受 OAuth 2.0 保护的资源.基本上,将 Web 服务配置为使用根 URL 路径/services/**"进行保护.在我的演示中,我使用了 Jersey.


3- Client Application Configuration

The last step is to configure the resources to be protected by OAuth 2.0. Basically, configure the web services to be secured with a root URL path "/services/**". In my demo, I used Jersey.

最后一步是使用受保护的 Web 服务.这是通过将 Authorization 标头添加到值为 "的请求中来完成的,例如bearer 7fbd71c5b28fdf0bdb922b07915c4d5".

The last step is to consume the secured web services. This is done by adding Authorization header to the request with value " ", for example "bearer 7fbd71c5b28fdf0bdb922b07915c4d5".

附:所描述的示例仅用于说明目的.它可能缺少一些实现,异常处理,......请评论进一步查询.

P.S. The described sample is just for clarification purposes. It may be missing some implementations, exception handling, ... Kindly comment for further inquiries.

相关文章