搜索词>>oauth 耗时0.0030
  • Spring Security OAuth 2开发者指南

    本文主要翻译spring官方的基于spring security框架的oauth2开发指南,spring,oauth2,spring框架,Java编程<h2>介绍</h2> <p>这是用户指南的支持<a href="https://tools.ietf.org/html/draft-ietf-oauth-v2" rel="external nofollow" target="_blank"><code>OAuth 2.0</code></a>。对于OAuth 1.0,一切都是不同的,所以<a href="http://projects.spring.io/spring-security-oauth/docs/oauth1.html" rel="external nofollow" target="_blank">看到它的用户指南</a>。</p> <p>本用户指南分为两部分,第一部分为OAuth 2.0提供者,第二部分为OAuth 2.0客户端。对于提供商和客户端,示例代码的最佳来源是<a href="https://github.com/spring-projects/spring-security-oauth/tree/master/tests" rel="external nofollow" target="_blank">集成测试</a>和<a href="https://github.com/spring-projects/spring-security-oauth/tree/master/samples/oauth2" rel="external nofollow" target="_blank">示例应用程序</a>。</p> <h2>OAuth 2.0提供程序</h2> <p>OAuth 2.0提供者机制负责公开OAuth 2.0受保护的资源。该配置包括建立可独立或代表用户访问其受保护资源的OAuth 2.0客户端。提供者通过管理和验证用于访问受保护资源的OAuth 2.0令牌来实现。在适用的情况下,提供商还必须提供用户界面,以确认客户端可以被授权访问受保护资源(即确认页面)。</p> <h2>OAuth 2.0提供程序实现</h2> <p>OAuth 2.0中的提供者角色实际上是在授权服务和资源服务之间分割的,而有时它们位于同一个应用程序中,使用Spring Security OAuth,您可以选择在两个应用程序之间进行拆分,并且还可以共享多个资源服务授权服务。令牌的请求由Spring MVC控制器端点处理,对受保护资源的访问由标准的Spring Security请求过滤器处理。为了实现OAuth 2.0授权服务器,Spring Security过滤器链中需要以下端点:</p> <ul> <li><a href="http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/provider/endpoint/AuthorizationEndpoint.html" rel="external nofollow" target="_blank" title="授权终点"><code>AuthorizationEndpoint</code></a>用于服务授权请求。默认网址:<code>/oauth/authorize</code>。</li> <li><a href="http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/provider/endpoint/TokenEndpoint.html" rel="external nofollow" target="_blank" title="令牌终点"><code>TokenEndpoint</code></a>用于服务访问令牌的请求。默认网址:<code>/oauth/token</code>。</li> </ul> <p>实施OAuth 2.0资源服务器需要以下过滤器:</p> <ul> <li>将<a href="http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/provider/authentication/OAuth2AuthenticationProcessingFilter.html" rel="external nofollow" target="_blank" title="OAuth2AuthenticationProcessingFilter"><code>OAuth2AuthenticationProcessingFilter</code></a>用于加载给定的认证访问令牌请求的认证。</li> </ul> <p>对于所有OAuth 2.0提供程序功能,使用特殊的Spring OAuth <code>@Configuration</code>适配器简化了配置。还有一个用于OAuth配置的XML命名空间,并且模式位于<a href="http://www.springframework.org/schema/security/spring-security-oauth2.xsd" rel="external nofollow" target="_blank" title="oauth2.xsd">http://www.springframework.org/schema/security/spring-security-oauth2.xsd</a>。命名空间是<code>http://www.springframework.org/schema/security/oauth2</code>。</p> <h2>授权服务器配置</h2> <p>在配置授权服务器时,必须考虑客户端用于从最终用户获取访问令牌(例如授权代码,用户凭据,刷新令牌)的授权类型。服务器的配置用于提供客户端详细信息服务和令牌服务的实现,并且启用或禁用全局机制的某些方面。但是请注意,每个客户端都可以特别配置,以便能够使用某些授权机制和访问授权。也就是因为您的提供商配置为支持“客户端凭据”授权类型,并不意味着特定客户端被授权使用该授权类型。</p> <p>该<code>@EnableAuthorizationServer</code>注释用于配置OAuth 2.0授权服务器机制,以及任何<code>@Beans</code>实现<code>AuthorizationServerConfigurer</code>(有一个方便的适配器实现)。将以下功能委派给由Spring创建并传递到以下内容的单独配置程序<code>AuthorizationServerConfigurer</code>:</p> <ul> <li><code>ClientDetailsServiceConfigurer</code>:一个定义客户端详细信息服务的配置程序。客户端的详细信息可以初始化,也可以参考现有的存储。</li> <li><code>AuthorizationServerSecurityConfigurer</code>:定义令牌端点上的安全约束。</li> <li><code>AuthorizationServerEndpointsConfigurer</code>:定义授权和令牌端点和令牌服务。</li> </ul> <p>提供商配置的一个重要方面是授权代码提供给OAuth客户端(授权代码授权)的方式。授权代码由OAuth客户端通过将最终用户指向用户可以输入其凭据的授权页面获得,导致从提供商授权服务器重定向到具有授权码的OAuth客户端。这在OAuth 2规范中有详细说明。</p> <p>在XML中,有一个<code><authorization-server/></code>元素以类似的方式用于配置OAuth 2.0授权服务器。</p> <h3>配置客户端详细信息</h3> <p>将<code>ClientDetailsServiceConfigurer</code>(从您的回调<code>AuthorizationServerConfigurer</code>)可以用来在内存或JDBC实现客户的细节服务来定义的。客户端的重要属性是</p> <ul> <li><code>clientId</code>:(必填)客户端ID。</li> <li><code>secret</code>:(可信客户端需要)客户机密码(如果有)。</li> <li><code>scope</code>:客户受限的范围。如果范围未定义或为空(默认值),客户端不受范围限制。</li> <li><code>authorizedGrantTypes</code>:授予客户端使用授权的类型。默认值为空。</li> <li><code>authorities</code>授予客户的授权机构(普通的Spring Security权威机构)。</li> </ul> <p>客户端的详细信息可以通过直接访问底层商店(例如,在数据库表中<code>JdbcClientDetailsService</code>)或通过<code>ClientDetailsManager</code>接口(这两种实现<code>ClientDetailsService</code>也实现)来更新运行的应用程序。</p> <p>注意:JDBC服务的架构未与库一起打包(因为在实践中可能需要使用太多变体),而是可以从<a href="https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql" rel="external nofollow" target="_blank">github</a>中的<a href="https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql" rel="external nofollow" target="_blank">测试代码中</a>开始。</p> <h3>管理令牌</h3> <p>该<a href="http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/provider/token/AuthorizationServerTokenServices.html" rel="external nofollow" target="_blank" title="AuthorizationServerTokenServices"><code>AuthorizationServerTokenServices</code></a>接口定义了所必需的管理OAuth 2.0令牌的操作。请注意以下事项:</p> <ul> <li>当创建访问令牌时,必须存储身份验证,以便接受访问令牌的资源可以稍后引用。</li> <li>访问令牌用于加载用于授权其创建的认证。</li> </ul> <p>在创建<code>AuthorizationServerTokenServices</code>实现时,您可能需要考虑使用<a href="http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/provider/token/DefaultTokenServices.html" rel="external nofollow" target="_blank" title="DefaultTokenServices"><code>DefaultTokenServices</code></a>可插入的策略来更改访问令牌的格式和存储。默认情况下,它将通过随机值创建令牌,并处理除代表它的令牌持久化之外的所有内容<code>TokenStore</code>。默认存储是<a href="http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/provider/token/store/InMemoryTokenStore.html" rel="external nofollow" target="_blank" title="InMemoryTokenStore">内存中的实现</a>,但还有一些其他可用的实现。这是一个关于每一个的一些讨论的描述</p> <ul> <li> <p>默认值<code>InMemoryTokenStore</code>对于单个服务器是完全正常的(即,在发生故障的情况下,低流量和热备份备份服务器)。大多数项目可以从这里开始,也可以在开发模式下运行,以便轻松启动没有依赖关系的服务器。</p> </li> <li> <p>这<code>JdbcTokenStore</code>是同一件事的<a href="http://projects.spring.io/spring-security-oauth/docs/JdbcTokenStore" rel="external nofollow" target="_blank">JDBC版本</a>,它将令牌数据存储在关系数据库中。如果您可以在服务器之间共享数据库,则可以使用JDBC版本,如果只有一个,则扩展同一服务器的实例,或者如果有多个组件,则授权和资源服务器。要使用<code>JdbcTokenStore</code>你需要“spring-jdbc”的类路径。</p> </li> <li> <p>商店的<a href="http://projects.spring.io/spring-security-oauth/docs/%60JwtTokenStore%60" rel="external nofollow" target="_blank">JSON Web令牌(JWT)版本</a>将所有关于授权的数据编码到令牌本身(因此,根本没有后端存储是一个显着的优势)。一个缺点是您不能轻易地撤销访问令牌,因此通常被授予短期到期权,撤销在刷新令牌处理。另一个缺点是,如果您在其中存储了大量用户凭据信息,令牌可能会变得非常大。这<code>JwtTokenStore</code>不是一个真正的“商店”,因为它不会保留任何数据,但它在翻译令牌值和验证信息之间起着相同的作用<code>DefaultTokenServices</code>。</p> </li> </ul> <p>注意:JDBC服务的架构未与库一起打包(因为在实践中可能需要使用太多变体),而是可以从<a href="https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql" rel="external nofollow" target="_blank">github</a>中的<a href="https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql" rel="external nofollow" target="_blank">测试代码中</a>开始。确保<code>@EnableTransactionManagement</code>在创建令牌时,防止在竞争相同行的客户端应用程序之间发生冲突。还要注意,示例模式有明确的<code>PRIMARY KEY</code>声明 - 这些在并发环境中也是必需的。</p> <h3>JWT令牌</h3> <p>要使用JWT令牌,您需要<code>JwtTokenStore</code>在授权服务器中。资源服务器还需要能够对令牌进行解码,因此它<code>JwtTokenStore</code>具有依赖性<code>JwtAccessTokenConverter</code>,并且授权服务器和资源服务器都需要相同的实现。默认情况下,令牌被签名,资源服务器还必须能够验证签名,因此它需要与授权服务器(共享密钥或对称密钥)相同的对称(签名)密钥,或者需要公共密钥(验证者密钥),其与授权服务器中的私钥(签名密钥)匹配(公私属或非对称密钥)。公钥(如果可用)由<code>/oauth/token_key</code>端点上的授权服务器公开,默认情况下,访问规则为“denyAll()”。<code>AuthorizationServerSecurityConfigurer</code></p> <p>要使用<code>JwtTokenStore</code>你需要的“spring-security-jwt”你的类路径(你可以在与Spring OAuth相同的github仓库中找到它,但发行周期不同)。</p> <h3>赠款类型</h3> <p><code>AuthorizationEndpoint</code>可以通过以下方式配置支持的授权类型<code>AuthorizationServerEndpointsConfigurer</code>。默认情况下,所有授权类型均受支持,除了密码(有关如何切换它的详细信息,请参见下文)。以下属性会影响授权类型:</p> <ul> <li><code>authenticationManager</code>:通过注入密码授权被打开<code>AuthenticationManager</code>。</li> <li><code>userDetailsService</code>:如果您注入<code>UserDetailsService</code>或者全局配置(例如a <code>GlobalAuthenticationManagerConfigurer</code>),则刷新令牌授权将包含对用户详细信息的检查,以确保该帐户仍然活动</li> <li><code>authorizationCodeServices</code>:定义<code>AuthorizationCodeServices</code>授权代码授权的授权代码服务(实例)。</li> <li><code>implicitGrantService</code>:在批准期间管理状态。</li> <li><code>tokenGranter</code>:(<code>TokenGranter</code>完全控制授予和忽略上述其他属性)</li> </ul> <p>在XML授予类型中包含作为子元素<code>authorization-server</code>。</p> <h3>配置端点URL</h3> <p>该<code>AuthorizationServerEndpointsConfigurer</code>有一个<code>pathMapping()</code>方法。它有两个参数:</p> <ul> <li>端点的默认(框架实现)URL路径</li> <li>需要的自定义路径(以“/”开头)</li> </ul> <p>由框架提供的URL路径<code>/oauth/authorize</code>(授权端点)<code>/oauth/token</code>(令牌端点)<code>/oauth/confirm_access</code>(用户发布批准此处)<code>/oauth/error</code>(用于在授权服务器中呈现错误)<code>/oauth/check_token</code>(由资源服务器用于解码访问令牌) ,并且<code>/oauth/token_key</code>(如果使用JWT令牌,则公开用于令牌验证的公钥)。</p> <p>注意,授权端点<code>/oauth/authorize</code>(或其映射替代方案)应使用Spring Security进行保护,以便只有经过身份验证的用户才能访问。例如使用标准的Spring Security <code>WebSecurityConfigurer</code>:</p> <pre> <code class="language-java"> @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests().antMatchers("/login").permitAll().and() // default protection for all resources (including /oauth/authorize) .authorizeRequests() .anyRequest().hasRole("USER") // ... more configuration, e.g. for form login } </code></pre> <p>注意:如果您的授权服务器也是资源服务器,那么还有另一个优先级较低的安全过滤器链控制API资源。通过访问令牌来保护这些请求,您需要他们的路径<em>不</em>与主用户面临的过滤器链中的路径匹配,因此请务必包含仅在<code>WebSecurityConfigurer</code>上述中选择非API资源的请求匹配器。</p> <p>默认情况下,通过Spring OAuth在<code>@Configuration</code>使用客户机密码的HTTP Basic认证的支持中为您保护令牌端点。在XML中不是这样(因此应该明确保护)。</p> <p>在XML中,<code><authorization-server/></code>元素具有一些可以用于以类似方式更改默认端点URL的属性。该<code>/check_token</code>端点必须(与显式启用<code>check-token-enabled</code>属性)。</p> <h2>自定义UI</h2> <p>大多数授权服务器端点主要由机器使用,但是有一些资源需要一个UI,而这些资源是GET <code>/oauth/confirm_access</code>和HTML响应<code>/oauth/error</code>。它们是在框架中使用白名单实现提供的,因此授权服务器的大多数真实世界实例都希望提供自己的实例,以便他们可以控制样式和内容。所有您需要做的是<code>@RequestMappings</code>为这些端点提供一个Spring MVC控制器,并且框架默认在调度程序中占用较低的优先级。在<code>/oauth/confirm_access</code>端点中,您可以期待<code>AuthorizationRequest</code>绑定到会话中,携带所有需要用户查询的数据(默认的实现是<code>WhitelabelApprovalEndpoint</code>这样查找起始点复制)。<code>/oauth/authorize</code>您可以从该请求中获取所有数据,然后根据需要进行渲染,然后所有用户需要执行的操作都是回复有关批准或拒绝授权的信息。请求参数直接传递给您<code>UserApprovalHandler</code>,<code>AuthorizationEndpoint</code>所以您可以随便解释数据。默认<code>UserApprovalHandler</code>取决于您是否已经提供了一个<code>ApprovalStore</code>在你的<code>AuthorizationServerEndpointsConfigurer</code>(在这种情况下,它是一个<code>ApprovalStoreUserApprovalHandler</code>)或不(在这种情况下,它是一个<code>TokenStoreUserApprovalHandler</code>)。标准审批处理程序接受以下内容:默认取决于您是否已经提供了一个在你的(在这种情况下,它是一个)或不(在这种情况下,它是一个)。标准审批处理程序接受以下内容:默认取决于您是否已经提供了一个在你的(在这种情况下,它是一个)或不(在这种情况下,它是一个)。标准审批处理程序接受以下内容:</p> <ul> <li> <p><code>TokenStoreUserApprovalHandler</code>:简单的是/否决定通过<code>user_oauth_approval</code>等于“真”或“假”。</p> </li> <li> <p><code>ApprovalStoreUserApprovalHandler</code>:一组<code>scope.*</code>参数键与“*”等于所请求的范围。参数的值可以是“true”或“approved”(如果用户批准了授权),则该用户被认为已经拒绝了该范围。如果批准了至少一个范围,则赠款是成功的。</p> </li> </ul> <p>注意:不要忘记在您为用户呈现的表单中包含CSRF保护。默认情况下,Spring Security正期待一个名为“_csrf”的请求参数(它在请求属性中提供值)。有关更多信息,请参阅Spring Security用户指南,或查看whitelabel实现的指导。</p> <h3>执行SSL</h3> <p>普通HTTP对于测试是很好的,但授权服务器只能在生产中使用SSL。您可以在安全容器或代理服务器后面运行应用程序,如果正确设置代理和容器(这与OAuth2无关),则应该可以正常运行。您也可能希望使用Spring Security <code>requiresChannel()</code>限制来保护端点。对于<code>/authorize</code>端点,由您来做,作为您正常应用程序安全性的一部分。对于<code>/token</code>端点<code>AuthorizationServerEndpointsConfigurer</code>,可以使用该<code>sslOnly()</code>方法设置一个标志。在这两种情况下,安全通道设置是可选的,但是如果Spring Security在不安全的通道上检测到请求,则会导致Spring Security重定向到安全通道。</p> <h2>自定义错误处理</h2> <p>授权服务器中的错误处理使用标准Spring MVC功能,即<code>@ExceptionHandler</code>端点本身的方法。用户还可以向<code>WebResponseExceptionTranslator</code>端点自身提供这些改变响应内容的最佳方式,而不是渲染方式。在授权<code>HttpMesssageConverters</code>端点的情况下,在令牌端点和OAuth错误视图(<code>/oauth/error</code>)的情况下,异常呈现(可以添加到MVC配置中)。该白色标签错误的端点提供了HTML的响应,但用户可能需要提供自定义实现(如只需添加一个<code>@Controller</code>带<code>@RequestMapping("/oauth/error")</code>)。</p> <h2>将用户角色映射到范围</h2> <p>限制令牌范围不仅仅是分配给客户端的范围,还可以根据用户自己的权限来进行限制。如果您在其中使用<code>DefaultOAuth2RequestFactory</code>,<code>AuthorizationEndpoint</code>则可以设置一个标志<code>checkUserScopes=true</code>,以将允许的范围限制为仅与那些与用户角色匹配的范围。你也可以注入<code>OAuth2RequestFactory</code>,<code>TokenEndpoint</code>但只有工作(即密码授权),如果你也安装一个<code>TokenEndpointAuthenticationFilter</code>- 你只需要在HTTP之后添加该过滤器<code>BasicAuthenticationFilter</code>。当然,您还可以实现自己的规则,将作用域映射到角色并安装自己的版本<code>OAuth2RequestFactory</code>。将<code>AuthorizationServerEndpointsConfigurer</code>让你注入一个定制的<code>OAuth2RequestFactory</code>,所以你可以使用该功能来建立一个工厂,如果你使用<code>@EnableAuthorizationServer</code>。</p> <h2>资源服务器配置</h2> <p>资源服务器(可以与授权服务器或单独的应用程序相同)提供受OAuth2令牌保护的资源。Spring OAuth提供了实现此保护的Spring Security认证过滤器。您可以<code>@EnableResourceServer</code>在<code>@Configuration</code>类上打开它,并使用a进行配置(必要时)<code>ResourceServerConfigurer</code>。可以配置以下功能:</p> <ul> <li><code>tokenServices</code>:定义令牌服务的bean(实例<code>ResourceServerTokenServices</code>)。</li> <li><code>resourceId</code>:资源的ID(可选,但建议并由验证服务器验证,如果存在)。</li> <li>其他扩展点(例如<code>tokenExtractor</code>从传入请求中提取令牌)</li> <li>请求匹配的受保护资源(默认为全部)</li> <li>受保护资源的访问规则(默认为“已验证”)</li> <li><code>HttpSecurity</code>Spring Security中配置程序允许的受保护资源的其他自定义</li> </ul> <p>该<code>@EnableResourceServer</code>注释添加类型的过滤器<code>OAuth2AuthenticationProcessingFilter</code>自动Spring Security的过滤器链。</p> <p>在XML中有一个<code><resource-server/></code>带有<code>id</code>属性的元素- 这是一个servlet的bean ID,<code>Filter</code>然后可以手动添加到标准的Spring Security链。</p> <p>您<code>ResourceServerTokenServices</code>是与授权服务器的合同的另一半。如果资源服务器和授权服务器在同一个应用程序中,然后使用,<code>DefaultTokenServices</code>那么您不需要太费心思考,因为它实现了所有必要的接口,因此它自动一致。如果您的资源服务器是一个单独的应用程序,那么您必须确保与授权服务器的功能相匹配,并提供一个<code>ResourceServerTokenServices</code>正确的解码令牌。与授权服务器一样,您经常可以使用该<code>DefaultTokenServices</code>选项,并且选择主要通过<code>TokenStore</code>(后端存储或本地编码)来表达。<code>RemoteTokenServices</code>一个替代方案是Spring OAuth功能(不是规范的一部分),允许资源服务器通过授权服务器(<code>/oauth/check_token</code>)上的HTTP资源解码令牌。<code>RemoteTokenServices</code>如果资源服务器中没有大量的流量(每个请求都必须与授权服务器进行验证),或者如果能够缓存结果,那么它们是方便的。要使用<code>/oauth/check_token</code>端点,您需要通过更改其访问规则(默认为“denyAll()”)来公开它<code>AuthorizationServerSecurityConfigurer</code>,例如</p> <pre> <code class="language-java"> @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')").checkTokenAccess( "hasAuthority('ROLE_TRUSTED_CLIENT')"); } </code></pre> <p>在这个例子中,我们配置了<code>/oauth/check_token</code>端点和<code>/oauth/token_key</code>端点(所以信任的资源可以获得JWT验证的公钥)。这两个端点受到使用客户端凭据的HTTP基本身份验证的保护。</p> <h3>配置OAuth感知表达式处理程序</h3> <p>您可能希望利用Spring Security <a href="http://docs.spring.io/spring-security/site/docs/3.2.5.RELEASE/reference/htmlsingle/#el-access" rel="external nofollow" target="_blank" title="表达式访问控制">基于表达式的访问控制</a>。表达式处理程序将默认在<code>@EnableResourceServer</code>安装程序中注册。这些表达式包括<em>#oauth2.clientHasRole</em>,<em>#oauth2.clientHasAnyRole</em>和<em>#oath2.denyClient</em>,可用于根据oauth客户端的角色提供访问(请参阅<code>OAuth2SecurityExpressionMethods</code>全面的列表)。在XML中,您可以<code>expression-handler</code>使用常规<code><http/></code>安全配置的元素注册一个oauth感知表达式处理程序。</p> <h2>OAuth 2.0客户端</h2> <p>OAuth 2.0客户端机制负责访问其他服务器的OAuth 2.0保护资源。该配置包括建立用户可能访问的相关受保护资源。客户端还可能需要提供用于存储用户的授权码和访问令牌的机制。</p> <h3>受保护的资源配置</h3> <p>受保护的资源(或“远程资源”)可以使用类型的bean定义来定义<a href="http://projects.spring.io/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/client/resource/OAuth2ProtectedResourceDetails.java" rel="external nofollow" target="_blank"><code>OAuth2ProtectedResourceDetails</code></a>。受保护的资源具有以下属性:</p> <ul> <li><code>id</code>:资源的id。该id仅由客户端用于查找资源; 它在OAuth协议中从未使用过。它也被用作bean的id。</li> <li><code>clientId</code>:OAuth客户端ID。这是OAuth提供商识别您的客户端的ID。</li> <li><code>clientSecret</code>:与资源相关的秘密。默认情况下,没有密码为空。</li> <li><code>accessTokenUri</code>:提供访问令牌的提供者OAuth端点的URI。</li> <li><code>scope</code>:逗号分隔的字符串列表,指定对资源的访问范围。默认情况下,不指定范围。</li> <li><code>clientAuthenticationScheme</code>:您的客户端用于向访问令牌端点进行身份验证的方案。建议的值:“http_basic”和“form”。默认值为“http_basic”。请参阅OAuth 2规范的第2.1节。</li> </ul> <p>不同的授权类型具有不同的具体实现<code>OAuth2ProtectedResourceDetails</code>(例如<code>ClientCredentialsResource</code>,对于“client_credentials”授权类型)。对于需要用户授权的授权类型,还有一个其他属性:</p> <ul> <li><code>userAuthorizationUri</code>:如果用户需要授权访问资源,则用户将被重定向到的uri。请注意,这并不总是需要,具体取决于支持哪个OAuth 2配置文件。</li> </ul> <p>在XML中有一个<code><resource/></code>可以用来创建类型的bean的元素<code>OAuth2ProtectedResourceDetails</code>。它具有匹配上述所有属性的属性。</p> <h3>客户端配置</h3> <p>对于OAuth 2.0客户端,使用简化配置<code>@EnableOAuth2Client</code>。这有两件事情:</p> <ul> <li> <p>创建一个过滤器bean(带有ID <code>oauth2ClientContextFilter</code>)来存储当前的请求和上下文。在需要在请求期间进行身份验证的情况下,管理重定向到和从OAuth认证uri。</p> </li> <li> <p><code>AccessTokenRequest</code>在请求范围中创建一个类型的bean 。授权代码(或隐式)授权客户端可以使用这种方式来保持与个别用户的状态相关。</p> </li> </ul> <p>过滤器必须连接到应用程序中(例如,使用 同一名称的Servlet初始化程序或<code>web.xml</code>配置<code>DelegatingFilterProxy</code>)。</p> <p>本<code>AccessTokenRequest</code>可以在使用 <code>OAuth2RestTemplate</code>这样的:</p> <pre> <code class="language-java">@Autowired private OAuth2ClientContext oauth2Context; @Bean public OAuth2RestTemplate sparklrRestTemplate() { return new OAuth2RestTemplate(sparklr(), oauth2Context); } </code></pre> <p>在会话范围中放置OAuth2ClientContext(为您),以保持不同用户的状态分开。没有了,您将不得不自己在服务器上管理等效的数据结构,将传入的请求映射到用户,并将每个用户与单独的实例相关联<code>OAuth2ClientContext</code>。</p> <p>在XML中有一个<code><client/></code>带有<code>id</code>属性的元素- 这是一个servlet的bean id,<code>Filter</code>在这种<code>@Configuration</code>情况下必须映射为一个<code>DelegatingFilterProxy</code>(具有相同名称)。</p> <h3>访问受保护的资源</h3> <p>一旦您提供了资源的所有配置,您现在可以访问这些资源。用于访问这些资源的建议的方法是通过使用<a href="http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html" rel="external nofollow" target="_blank" title="RestTemplate">所述<code>RestTemplate</code>在弹簧3引入</a>。Spring Security的OAuth提供<a href="http://projects.spring.io/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/client/OAuth2RestTemplate.java" rel="external nofollow" target="_blank">了</a>只需要提供一个实例的<a href="http://projects.spring.io/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/client/OAuth2RestTemplate.java" rel="external nofollow" target="_blank">RestTemplate的扩展</a><a href="http://projects.spring.io/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/client/resource/OAuth2ProtectedResourceDetails.java" rel="external nofollow" target="_blank"><code>OAuth2ProtectedResourceDetails</code></a>。要使用用户令牌(授权代码授权),您应该考虑使用创建一些请求和会话作用域上下文对象的<code>@EnableOAuth2Client</code>配置(或XML等效项<code><oauth:rest-template/></code>),以便不同用户的请求在运行时不会相冲突。</p> <p>作为一般规则,Web应用程序不应使用密码授权,因此<code>ResourceOwnerPasswordResourceDetails</code>如果可以支持,请避免使用<code>AuthorizationCodeResourceDetails</code>。如果您非常需要从Java客户端工作的密码授权,则使用相同的机制来配置您的凭据,并将凭据<code>OAuth2RestTemplate</code>添加到<code>AccessTokenRequest</code>(这是一个<code>Map</code>短暂的),而不是<code>ResourceOwnerPasswordResourceDetails</code>(在所有访问令牌之间共享)。</p> <h3>在客户端中持久化令牌</h3> <p>客户端并不<em>需要</em>坚持令牌,但它可以很好的为不要求用户每次在客户端应用程序重新启动时批准新的代金券授予。该<a href="http://projects.spring.io/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/client/token/ClientTokenServices.java" rel="external nofollow" target="_blank"><code>ClientTokenServices</code></a>接口定义了所必需的持续的OAuth为特定用户2.0的令牌的动作。提供了一个JDBC实现,但如果您希望实现自己的服务来将持久性数据库中的访问令牌和关联的身份验证实例存储起来,那么您可以使用。如果要使用此功能,您需要提供一个专门配置<code>TokenProvider</code>的<code>OAuth2RestTemplate</code>如</p> <pre> <code class="language-java">@Bean @Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES) public OAuth2RestOperations restTemplate() { OAuth2RestTemplate template = new OAuth2RestTemplate(resource(), new DefaultOAuth2ClientContext(accessTokenRequest)); AccessTokenProviderChain provider = new AccessTokenProviderChain(Arrays.asList(new AuthorizationCodeAccessTokenProvider())); provider.setClientTokenServices(clientTokenServices()); return template; } </code></pre> <h2>外部OAuth2提供商客户端的定制</h2> <p>一些外部OAuth2提供者(例如<a href="https://developers.facebook.com/docs/authentication" rel="external nofollow" target="_blank" title="Facebook">Facebook</a>)不能正确地实现规范,或者他们只是坚持使用旧版本的规范,而不是Spring Security OAuth。要在客户端应用程序中使用这些提供程序,您可能需要调整客户端基础架构的各个部分。</p> <p>以Facebook为例,应用程序中有一个Facebook功能<code>tonr2</code>(您需要更改配置以添加您自己的,有效的客户端ID和密码 - 它们很容易在Facebook网站上生成)。</p> <p>Facebook令牌响应在令牌的到期时间(它们使用<code>expires</code>而不是<code>expires_in</code>)中也包含不符合规定的JSON条目,因此,如果要在应用程序中使用到期时间,则必须使用自定义手动解码<code>OAuth2SerializationService</code>。</p>
  • Spring Boot Security OAuth2 例子(Bcrypt Encoder)

    Spring Boot Security OAuth2 在这篇文章中,我们将讨论如何使用Spring Boot Security OAuth2保护REST API。我们将为不同的crud操作实现AuthorizationServer,ResourceServer和一些REST API,并使用Postman测试这些API。<h2>引言</h2> <blockquote> <p>在这篇文章中,我们将讨论如何使用Spring Boot Security OAuth2保护REST API。我们将为不同的crud操作实现<span style="color:#c7254e"><span style="background-color:#f9f2f4"><span style="font-family:Menlo,Monaco,Consolas,"Courier New",monospace">AuthorizationServer</span></span></span>,<span style="color:#c7254e"><span style="background-color:#f9f2f4"><span style="font-family:Menlo,Monaco,Consolas,"Courier New",monospace">ResourceServer</span></span></span>和一些REST API,并使用Postman测试这些API。在这里我们将使用mysql数据库读取用户证书而不是内存认证。另外,为了简化我们的ORM解决方案,我们将使用spring-data和<span style="color:#c7254e"><span style="background-color:#f9f2f4"><span style="font-family:Menlo,Monaco,Consolas,"Courier New",monospace">BCryptPasswordEncoder</span></span></span>进行密码编码。</p> </blockquote> <h2 style="margin-left:0px; margin-right:0px; text-align:start">什么是OAuth</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">      OAuth只是一个安全的授权协议,它处理第三方应用程序授权访问用户数据而不暴露其密码。例如。(在许多网站上用fb,gPlus,twitter进行登录..)都在这个协议下工作。当你知道涉及的各方时,协议变得更容易。基本上有三方参与:oAuth提供者,oAuth客户端和所有者。这里,oAuth提供者提供了诸如Facebook,Twitter之类的身份验证令牌。同样,oAuth客户端是希望代表所有者访问凭证的应用程序,所有者是拥有oAuth提供程序(例如facebook和twitter)的帐户的用户。</span></span></span><br /> <br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start">什么是OAuth2</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><a href="https://oauth.net/2/" rel="external nofollow" target="_blank" >OAuth 2</a>是一种授权框架,它使应用程序能够获得有限访问HTTP服务(如Facebook,GitHub和DigitalOcean)上的用户帐户的权限。它的工作方式是将用户身份验证委派给承载用户帐户的服务,并授权第三方应用程序访问该用户帐户。OAuth 2为Web和桌面应用程序以及移动设备提供授权流程。</span></span></span><br /> <br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start">OAuth2角色</h2> <h3 style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">OAuth2提供4种不同的角色。 </span></span></span></h3> <ul> <li><strong>Resource Owner</strong>: 用户</li> <li><strong>Client:</strong> 接入应用</li> <li><strong>Resource Server</strong>: API</li> <li><strong>Authorization Server</strong>: API</li> </ul> <h3>OAuth2授权类型</h3> <p><span style="font-family:"Varela Round",sans-serif">以下是OAuth2定义的4种不同的授权类型</span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><strong>授权码(</strong></span></span></span><strong>Authorization Code</strong><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><strong>):</strong>与服务器端应用程序一起使用</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><strong>隐式(</strong></span></span></span><strong>Implicit</strong><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><strong>):</strong>用于移动应用程序或Web应用程序(在用户设备上运行的应用程序)</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><strong>资源所有者密码凭证(</strong></span></span></span><strong>Resource Owner Password Credentials</strong><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><strong>):</strong>与受信任的应用程序(如服务本身拥有的应用程序)一起使用</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><strong>客户端证书(</strong></span></span></span><strong>Client Credentials</strong><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><strong>):</strong>与应用程序API访问一起使用</span></span></span></p> <h2 style="margin-left:0px; margin-right:0px; text-align:start">项目结构</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">以下是Spring Boot Security OAuth2实现的项目结构。</span></span></span><br /> <img srcset="" width="" size="" class="img-thumbnail" alt="以下是Spring Boot Security OAuth2实现的项目结构。 " src="/resources/assist/images/blog/1b41226dd0f4488393ac9ce4393d011b.png" /><br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start">Maven的依赖</h2> <strong>pom.xml</strong> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">以下是所需的依赖关系。</span></span></span></p> <pre> <code class="language-xml"><parent> <groupId> org.springframework.boot </ groupId> <artifactId> spring-boot-starter-parent </ artifactId> <version> 1.5.8.RELEASE </ version> </ parent> <dependencies> <dependency > <groupId> org.springframework.boot </ groupId> <artifactId> spring-boot-starter-web </ artifactId> </ dependency> <dependency> <groupId> org.springframework.boot </ groupId> <artifactId> spring-boot-starter-data-jpa</ artifactId> </ dependency> <dependency> <groupId> org.springframework.boot </ groupId> <artifactId> spring-boot-starter-security </ artifactId> </ dependency> <dependency> <groupId> org.springframework .boot </ groupId> <artifactId> spring-security-oauth2 </ artifactId> </ dependency> <dependency> <groupId> mysql </ groupId> <artifactId>mysql-connector-java </ artifactId> </ dependency> <dependency> <groupId> commons-dbcp </ groupId> <artifactId> commons-dbcp </ artifactId> </ dependency> </ dependencies></code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start">OAuth2授权服务器配置</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">这个类扩展<code>AuthorizationServerConfigurerAdapter</code>并负责生成特定于客户端的令牌。假设,如果用户想通过Facebook 登录到<span style="color:#008800">devglan.com</span>,那么facebook auth服务器将为Devglan生成令牌。在这种情况下,Devglan成为客户端,它将成为代表用户从脸书 - 授权服务器请求授权代码。以下是Facebook将使用的类似实现。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">在这里,我们使用内存凭证,client_id作为<span style="color:#008800">devglan-client</span>,CLIENT_SECRET作为<span style="color:#008800">devglan-secret</span>。但您也可以自由使用JDBC实现。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><span style="color:#008800">@EnableAuthorizationServer:</span>启用授权服务器<span style="color:#008800">。AuthorizationServerEndpointsConfigurer</span>定义授权和令牌端点以及令牌服务。</span></span></span><br /> <br /> <strong>AuthorizationServerConfig.java:</strong><br />  </p> <pre> <code class="language-java">import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.provider.token.TokenStore; @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { static final String CLIEN_ID = "devglan-client"; static final String CLIENT_SECRET = "devglan-secret"; static final String GRANT_TYPE = "password"; static final String AUTHORIZATION_CODE = "authorization_code"; static final String REFRESH_TOKEN = "refresh_token"; static final String IMPLICIT = "implicit"; static final String SCOPE_READ = "read"; static final String SCOPE_WRITE = "write"; static final String TRUST = "trust"; static final int ACCESS_TOKEN_VALIDITY_SECONDS = 1*60*60; static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 6*60*60; @Autowired private TokenStore tokenStore; @Autowired private AuthenticationManager authenticationManager; @Override public void configure(ClientDetailsServiceConfigurer configurer) throws Exception { configurer .inMemory() .withClient(CLIEN_ID) .secret(CLIENT_SECRET) .authorizedGrantTypes(GRANT_TYPE_PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT ) .scopes(SCOPE_READ, SCOPE_WRITE, TRUST) .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS). refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore) .authenticationManager(authenticationManager); } }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start">OAuth2资源服务器配置</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">我们的上下文中的资源是我们为粗暴操作公开的REST API。要访问这些资源,必须对客户端进行身份验证。在实时场景中,每当用户尝试访问这些资源时,都会要求用户提供他真实性,一旦用户被授权,他将被允许访问这些受保护的资源。 </span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><span style="color:#008800">@EnableResourceServer:</span>启用资源服务器</span></span></span><br /> <strong>ResourceServerConfig.java:</strong><br />  </p> <pre> <code class="language-java">import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler; @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { private static final String RESOURCE_ID = "resource_id"; @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId(RESOURCE_ID).stateless(false); } @Override public void configure(HttpSecurity http) throws Exception { http. anonymous().disable() .authorizeRequests() .antMatchers("/users/**").authenticated() .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler()); } }</code></pre> <h2 style="margin-left:0px; margin-right:0px; text-align:start">安全配置</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">这个类扩展了<span style="color:#008800">WebSecurityConfigurerAdapter</span>并提供了通常的spring安全配置。这里,我们使用bcrypt编码器来编码我们的密码。您可以尝试使用此<a href="http://www.devglan.com/online-tools/bcrypt-hash-generator" rel="external nofollow" style="box-sizing:border-box; color:#337ab7; text-decoration:none" target="_blank" >在线Bcrypt工具</a>对密码进行编码和匹配。以下配置基本引导了授权服务器和资源服务器。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><span style="color:#008800">@EnableWebSecurity</span>:启用弹簧安全Web安全支持。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><span style="color:#008800">@EnableGlobalMethodSecurity:</span>支持方法级别访问控制,如<code>@PreAuthorize </code> <code>@PostAuthorize</code></span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">在这里,我们正在使用inmemorytokenstore,但您可以自由使用JdbcTokenStore或JwtTokenStore.Here</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><strong>SecurityConfig.java:</strong><br />  </p> <pre> <code class="language-java">import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import javax.annotation.Resource; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource(name = "userService") private UserDetailsService userDetailsService; @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Autowired public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(encoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .anonymous().disable() .authorizeRequests() .antMatchers("/api-docs/**").permitAll(); } @Bean public TokenStore tokenStore() { return new InMemoryTokenStore(); } @Bean public BCryptPasswordEncoder encoder(){ return new BCryptPasswordEncoder(); } @Bean public FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); bean.setOrder(0); return bean; } } </code></pre> <h2 style="margin-left:0px; margin-right:0px; text-align:start">Rest API</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">以下是我们为测试目的而暴露的非常基本的REST API。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><strong>UserController.java</strong><br />  </p> <pre> <code class="language-java">@RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @RequestMapping(value="/user", method = RequestMethod.GET) public List listUser(){ return userService.findAll(); } @RequestMapping(value = "/user", method = RequestMethod.POST) public User create(@RequestBody User user){ return userService.save(user); } @RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE) public String delete(@PathVariable(value = "id") Long id){ userService.delete(id); return "success"; } }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start">现在让我们定义负责从数据库中获取用户详细信息的用户服务。接下来是Spring将用来验证用户的实现。<br /> <strong>UserServiceImpl.java:</strong><br />  </p> <pre> <code class="language-java">@Service(value = "userService") public class UserServiceImpl implements UserDetailsService, UserService { @Autowired private UserDao userDao; public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException { User user = userDao.findByUsername(userId); if(user == null){ throw new UsernameNotFoundException("Invalid username or password."); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthority()); } private List getAuthority() { return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")); } public List findAll() { List list = new ArrayList<>(); userDao.findAll().iterator().forEachRemaining(list::add); return list; } } </code></pre> <h2 style="margin-left:0px; margin-right:0px; text-align:start">默认数据库SQL脚本</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">以下是在应用程序启动时插入的插入语句。</span></span></span></p> <pre> <code class="language-sql">INSERT INTO User (id, username, password, salary, age) VALUES (1, 'Alex123', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 3456, 33); INSERT INTO User (id, username, password, salary, age) VALUES (2, 'Tom234', '$2a$04$PCIX2hYrve38M7eOcqAbCO9UqjYg7gfFNpKsinAxh99nms9e.8HwK', 7823, 23); INSERT INTO User (id, username, password, salary, age) VALUES (3, 'Adam', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 4234, 45);</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start">测试应用程序</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">运行<span style="color:#008800">Application.java</span>作为Java应用程序。我们将使用邮递员来测试OAuth2实现。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><strong>生成AuthToken</strong>:在头文件中,我们有用户名和密码分别为Alex123和密码作为Authorization头。按照Oauth2规范,Access token请求应该使用<code>application/x-www-form-urlencoded.</code>以下设置。<br /> <img srcset="" width="" size="" class="img-thumbnail" alt="设置postmain工具的HTTP请求头部、" src="/resources/assist/images/blog/ec8782d9af6e49a6873c677dc018c05a.jpg" /></span></span></span>一旦你提出请求,你会得到如下结果。它有访问令牌和刷新令牌。<br /> <img srcset="" width="" size="" class="img-thumbnail" alt="请求测试结果" src="/resources/assist/images/blog/c465faa4e2004356b9c73baad7a723d4.png" /></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><strong>无令牌</strong> <strong>访问资源使用</strong><strong>令牌</strong><strong>访问资源使用刷新令牌刷新令牌<br /> <br /> <img srcset="" width="" size="" class="img-thumbnail" alt="无令牌访问返回" src="/resources/assist/images/blog/28e5ab51d7334178ae7a91a01d636145.png" /><br /> <br /> <img srcset="" width="" size="" class="img-thumbnail" alt="有令牌返回" src="/resources/assist/images/blog/0e1512b3e2e14c37977bf062042fcd66.png" /></strong><br /> 通常情况下,oAuth2的令牌到期时间非常少,您可以使用以下API在令牌过期时刷新令牌。<br /> <img srcset="" width="" size="" class="img-thumbnail" alt="5" src="/resources/assist/images/blog/62da2888066843539f15fabcc9098987.png" /><br /> <br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start">常见错误</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">我可以在评论部分看到,大多数读者遇到了2个错误。因此,添加这个部分最好能帮助读者。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><strong>访问此资源需要完整身份验证</strong><br />  </p> <pre> <code class="language-json">{ "timestamp": 1513747665246, "status": 401, "error": "Unauthorized", "message": "Full authentication is required to access this resource", "path": "/oauth/token" }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><br />  </p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">如果您错过了在POST的授权部分添加用户名/密码的情况,则会导致此问题。在此部分中,选择类型为基本身份验证,并提供凭证作为devglan-client和devglan-secret,然后向url - http://localhost:8080/oauth/token来获得授权令牌。以下是截图。<br /> <img srcset="" width="" size="" class="img-thumbnail" alt="常见错误举证1" src="/resources/assist/images/blog/65840cf1870349e2b2c2b20e127b415f.png" /></span></span></span><br />  </p> <p style="margin-left:0px; margin-right:0px; text-align:start"><strong>没有客户端身份验证。尝试添加适当的认证过滤器。</strong></p> <pre> <code class="language-json">{ "error": "unauthorized", "error_description": "There is no client authentication. Try adding an appropriate authentication filter." }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">在这种情况下,检查你的auth url.It应该是 - http://localhost:8080/oauth/token而不是http://localhost:8080/oauth/token/</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><strong>缺少授权类型</strong><br /> 在这种情况下,您错过了在请求中添加grant_type。请尝试将其添加为<strong>password</strong></p> <h2 style="margin-left:0px; margin-right:0px; text-align:start">总结</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">在本教程中,我们了解了如何通过实现资源服务器和授权服务器来保护REST API与OAUTH2的安全。</span></span></span><br /> <br /> <a href="http://www.leftso.com/resource/1006.html" target="_blank" ><strong>项目源码下载</strong></a></p>
  • spring boot 入门之security oauth2 jwt完美整合例子-java编程

    spring boot 入门之security oauth2 jwt完美整合例子,Java编程中spring boot框架+spring security框架+spring security oauth2框架整合的例子,并且oauth2整合使用jwt方式存储<h2>一、本文简介</h2> 本文主要讲解Java编程中spring boot框架+spring security框架+spring security oauth2框架整合的例子,并且oauth2整合使用jwt方式存储 <h2>二、学习前提</h2> 首先是讲解oauth2的基本说明:<br /> 推荐查看:<a href="http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html" rel="external nofollow" target="_blank">http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html</a><br /> <br /> 上面的地址可以基本了解下oauth2的原理<br /> <img alt="oauth2授权图" class="img-thumbnail" src="/resources/assist/images/blog/37245915-1b05-45d1-922f-84e258c84b8e.png" /><br /> JWT的认知和基本使用:<br /> 推荐查看:<br /> 1.<a rel="" target="_blank"href="http://www.leftso.com/blog/220.html" rel="" target="_blank">什么是JWT?</a> <br /> 2.<a rel="" target="_blank"href="http://www.leftso.com/blog/221.html" rel="" target="_blank">Java编程中java-jwt框架使用</a> <h2>三、代码编辑</h2> 开始代码:<br /> 认证服务:<br /> 几个核心的配置类:<br /> <strong>AuthorizationServerConfiguration.java</strong> <pre> <code class="language-java">package com.leftso.config.security; import java.util.HashMap; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.User; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; /** * 认证授权服务端 * * @author leftso * */ @Configuration @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { @Value("${resource.id:spring-boot-application}") // 默认值spring-boot-application private String resourceId; @Value("${access_token.validity_period:3600}") // 默认值3600 int accessTokenValiditySeconds = 3600; @Autowired private AuthenticationManager authenticationManager; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(this.authenticationManager); endpoints.accessTokenConverter(accessTokenConverter()); endpoints.tokenStore(tokenStore()); } @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')"); oauthServer.checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')"); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("normal-app").authorizedGrantTypes("authorization_code", "implicit") .authorities("ROLE_CLIENT").scopes("read", "write").resourceIds(resourceId) .accessTokenValiditySeconds(accessTokenValiditySeconds).and().withClient("trusted-app") .authorizedGrantTypes("client_credentials", "password").authorities("ROLE_TRUSTED_CLIENT") .scopes("read", "write").resourceIds(resourceId).accessTokenValiditySeconds(accessTokenValiditySeconds) .secret("secret"); } /** * token converter * * @return */ @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() { /*** * 重写增强token方法,用于自定义一些token返回的信息 */ @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { String userName = authentication.getUserAuthentication().getName(); User user = (User) authentication.getUserAuthentication().getPrincipal();// 与登录时候放进去的UserDetail实现类一直查看link{SecurityConfiguration} /** 自定义一些token属性 ***/ final Map<String, Object> additionalInformation = new HashMap<>(); additionalInformation.put("userName", userName); additionalInformation.put("roles", user.getAuthorities()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation); OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication); return enhancedToken; } }; accessTokenConverter.setSigningKey("123");// 测试用,资源服务使用相同的字符达到一个对称加密的效果,生产时候使用RSA非对称加密方式 return accessTokenConverter; } /** * token store * * @param accessTokenConverter * @return */ @Bean public TokenStore tokenStore() { TokenStore tokenStore = new JwtTokenStore(accessTokenConverter()); return tokenStore; } }</code></pre> SecurityConfiguration.java <pre> <code class="language-java">package com.leftso.config.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import com.leftso.entity.Account; import com.leftso.repository.AccountRepository; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { // 查询用户使用 @Autowired AccountRepository accountRepository; @Autowired public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception { // auth.inMemoryAuthentication() // .withUser("user").password("password").roles("USER") // .and() // .withUser("app_client").password("nopass").roles("USER") // .and() // .withUser("admin").password("password").roles("ADMIN"); //配置用户来源于数据库 auth.userDetailsService(userDetailsService()); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll().anyRequest().authenticated().and() .httpBasic().and().csrf().disable(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public UserDetailsService userDetailsService() { return new UserDetailsService() { @Override public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException { // 通过用户名获取用户信息 Account account = accountRepository.findByName(name); if (account != null) { // 创建spring security安全用户 User user = new User(account.getName(), account.getPassword(), AuthorityUtils.createAuthorityList(account.getRoles())); return user; } else { throw new UsernameNotFoundException("用户[" + name + "]不存在"); } } }; } }</code></pre> <br /> 受保护的资源服务:<br /> 核心配置:<br /> SecurityConfiguration.java <pre> <code class="language-java">package com.leftso.config; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers(HttpMethod.OPTIONS).permitAll() .anyRequest().authenticated() .and().httpBasic() .and().csrf().disable(); } } </code></pre> <br /> ResourceServerConfiguration.java <pre> <code class="language-java">package com.leftso.config; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.DefaultTokenServices; import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import org.springframework.security.web.util.matcher.RequestMatcher; /** * 资源服务端 * * @author leftso * */ @Configuration @EnableResourceServer public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Value("${resource.id:spring-boot-application}") private String resourceId; @Override public void configure(ResourceServerSecurityConfigurer resources) { // @formatter:off resources.resourceId(resourceId); resources.tokenServices(defaultTokenServices()); // @formatter:on } @Override public void configure(HttpSecurity http) throws Exception { // @formatter:off http.requestMatcher(new OAuthRequestedMatcher()).authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll() .anyRequest().authenticated(); // @formatter:on } private static class OAuthRequestedMatcher implements RequestMatcher { public boolean matches(HttpServletRequest request) { String auth = request.getHeader("Authorization"); // Determine if the client request contained an OAuth Authorization boolean haveOauth2Token = (auth != null) && auth.startsWith("Bearer"); boolean haveAccessToken = request.getParameter("access_token") != null; return haveOauth2Token || haveAccessToken; } } // ===================================================以下代码与认证服务器一致========================================= /** * token存储,这里使用jwt方式存储 * * @param accessTokenConverter * @return */ @Bean public TokenStore tokenStore() { TokenStore tokenStore = new JwtTokenStore(accessTokenConverter()); return tokenStore; } /** * Token转换器必须与认证服务一致 * * @return */ @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() { // /*** // * 重写增强token方法,用于自定义一些token返回的信息 // */ // @Override // public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { // String userName = authentication.getUserAuthentication().getName(); // User user = (User) authentication.getUserAuthentication().getPrincipal();// 与登录时候放进去的UserDetail实现类一直查看link{SecurityConfiguration} // /** 自定义一些token属性 ***/ // final Map<String, Object> additionalInformation = new HashMap<>(); // additionalInformation.put("userName", userName); // additionalInformation.put("roles", user.getAuthorities()); // ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation); // OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication); // return enhancedToken; // } }; accessTokenConverter.setSigningKey("123");// 测试用,授权服务使用相同的字符达到一个对称加密的效果,生产时候使用RSA非对称加密方式 return accessTokenConverter; } /** * 创建一个默认的资源服务token * * @return */ @Bean public ResourceServerTokenServices defaultTokenServices() { final DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setTokenEnhancer(accessTokenConverter()); defaultTokenServices.setTokenStore(tokenStore()); return defaultTokenServices; } // ===================================================以上代码与认证服务器一致========================================= } </code></pre> <br /> 项目源码下载:<br /> <a href="https://github.com/leftso/demo-spring-boot-security-oauth2" rel="external nofollow" target="_blank">https://github.com/leftso/demo-spring-boot-security-oauth2</a> <h2>四、测试上面的编码</h2> 测试过程<br /> 步骤一:打开浏览器,输入地址 <pre> <code>http://localhost:8080/oauth/authorize?client_id=normal-app&response_type=code&scope=read&redirect_uri=/resources/user</code></pre> <br /> 会提示输入用户名密码,这时候输入用户名leftso,密码111aaa将会出现以下界面<br /> <img alt="授权页面" class="img-thumbnail" src="/resources/assist/images/blog/50f1dbe9-78f0-4085-8ade-c6840d63d192.png" /><br /> 点击Authorize将获取一个随机的code,如图:<br /> <img alt="授权码" class="img-thumbnail" src="/resources/assist/images/blog/831a7283-9b6c-4d5d-9da3-6d3756eb069d.png" /><br /> <br />  打开工具postmain,输入以下地址获取授权token <pre> <code>localhost:8080/oauth/token?code=r8YBUL&grant_type=authorization_code&client_id=normal-app&redirect_uri=/resources/user</code></pre> <strong>注意:url中的code就是刚才浏览器获取的code值<br /> <br /> 获取的token信息如下图:<br /> <img alt="token" class="img-thumbnail" src="/resources/assist/images/blog/7c03ded7d6524068a137ba060654f094.png" /></strong><br /> <br /> 这时候拿到token就可以访问受保护的资源信息了,如下<br />   <pre> <code>localhost:8081//resources/user</code></pre> <br /> 首先,直接访问资源,会报错401如图:<br /> <img alt="401" class="img-thumbnail" src="/resources/assist/images/blog/c6ee8e5c-fda0-4f3f-a538-c9568136e80d.png" /><br /> <br /> 我们加上前面获取的access token再试: <pre> <code>localhost:8081//resources/user?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3ByaW5nLWJvb3QtYXBwbGljYXRpb24iXSwidXNlcl9uYW1lIjoibGVmdHNvIiwic2NvcGUiOlsicmVhZCJdLCJyb2xlcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn1dLCJleHAiOjE0OTEzNTkyMjksInVzZXJOYW1lIjoibGVmdHNvIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6IjgxNjI5NzQwLTRhZWQtNDM1Yy05MmM3LWZhOWIyODk5NmYzMiIsImNsaWVudF9pZCI6Im5vcm1hbC1hcHAifQ.YhDJkMSlyIN6uPfSFPbfRuufndvylRmuGkrdprUSJIM</code></pre> <br /> 这时候我们就能成功获取受保护的资源信息了:<br /> <img alt="resource" class="img-thumbnail" src="/resources/assist/images/blog/31e0ae6d-f19d-41be-8174-57f5457230de.png" /><br /> <br /> 到这里spring boot整合security oauth2 的基本使用已经讲解完毕. <h2>五、扩展思维</h2> 留下一些扩展<br /> 1.认证服务的客户端信息是存放内存的,实际应用肯定是不会放内存的,考虑数据库,默认有个DataSource的方式,还有一个自己实现clientDetail接口方式<br /> 2.jwt这里测试用的最简单的对称加密,实际应用中使用的一般都是RSA非对称加密方式
  • 使用OAuth2安全的Spring security REST API(含demo代码下载)

    使用OAuth2安全的Spring REST API,Secure Spring REST API using OAuth2(含demo代码下载)<p>    让我们<code>Spring REST API</code>使用<code>OAuth2</code>这个时间来保护我们,这是一个简单的指南,显示使用REST API来保护REST API所需的内容<code>Spring OAuth2</code>。我们的用例符合<code>Resource-owner Password Grant</code>OAUth2规范的流程。我们将使用两个不同的客户端(Postman和<code>Spring RestTemplate</code>基于Java 的应用程序)访问我们的OAuth2受保护的REST资源。</p> <p>    如果您已经熟悉OAuth2概念,您可能希望跳过该理论,并直接跳转到代码中。一如以往,在本文末尾附件中可以找到完整的代码。让我们开始吧。<br />  </p> <h3>    什么是OAuth2</h3> <p>    OAuth2是一个标准化的授权协议/框架。根据官方OAuth2规范:</p> <p>    OAuth 2.0授权框架使得第三方应用程序可以通过协调资源所有者和HTTP服务之间的批准交互来代替资源所有者来<u>获取</u>对HTTP服务的<u>有限访问权限</u>,也可以允许第三方应用程序以自己的身份获得访问权限。</p> <p>    Google,Facebook等大型玩家已经在使用自己的OAuth2实现了很长一段时间。企业也正在朝OAuth2采纳方向发展。</p> <p>我发现OAuth2规范比较简单。然而,如果你想开始更快,可以在这里找到一篇关于OAuth2基础知识的优秀文章,从而深入了解OAUth2理论概念。</p> <p>Spring Security OAuth项目提供了使用Spring开发符合OAuth2标准的实现所需的所有API。官方的Spring安全oauth项目提供了一个实施OAuth2的综合示例。这个帖子的代码示例灵感来自这个例子本身。这篇文章的目的是为了保护我们的REST API,只需使用所需的最低限度的功能。</p> <p>至少你应该知道OAuth2中的四个关键概念:<br />  </p> <h3>OAuth2角色</h3> <p>OAuth2定义了四个角色:</p> <ul> <li><strong><code>resource owner</code>:</strong><br /> 可能是你 能够授予访问受保护资源的实体。当资源所有者是个人时,它被称为最终用户。</li> <li><strong><code>resource server</code>:</strong><br /> 托管受保护资源的服务器,能够使用访问令牌接受和响应受保护的资源请求。</li> <li><strong><code>client</code>:</strong><br /> 代表资源所有者及其授权的应用程序生成受保护的资源请求。它可能是一个移动应用程序,要求您访问您的Facebook订阅源,REST客户端尝试访问REST API,一个网站[Stackoverflow例如]使用Facebook帐户提供备用登录选项。</li> <li><strong><code>authorization server</code>:</strong><br /> 服务器在成功验证资源所有者并获得授权后,向客户端发出访问令牌。</li> </ul> <p>在我们的示例中,我们的REST API只能通过资源服务器进行访问,这将需要存在请求的访问令牌<br />  </p> <h3>2. OAuth2授权授权类型</h3> <p>授权授权是表示资源所有者授权(访问其受保护的资源)的凭据,由客户端使用以获取访问令牌。规范定义了四种授权类型:</p> <ul> <li><code>authorization code</code></li> <li><code>implicit</code></li> <li><code>resource owner password credentials</code></li> <li><code>client credentials</code></li> </ul> <p>我们将使用<code>resource owner password credentials</code>授权类型。原因很简单,我们没有实现将我们重定向到登录页面的视图。仅客户端[Postman或RestTemplate为基础的Java客户端例如]拥有资源所有者的凭据,并且他们将这些凭证以及客户机凭证提供给授权服务器,以便最终接收访问令牌[和可选刷新令牌],然后使用该令牌实际访问资源。</p> <p>一个常见的例子是<code>GMail app</code>您的智能手机上的[客户端],您需要您的凭据并使用它们进行连接<code>GMail servers</code>。它还显示“密码凭证授予”最适合当客户端和服务器与信任在同一个公司时,您不想向第三方提供凭据。</p> <h3>OAuth2令牌</h3> <p>令牌是实现特定的随机字符串,由授权服务器生成,并在客户端请求时发出。</p> <ul> <li><code>Access Token</code> :发送每个请求,通常有效期很短的一段时间[一小时例如]</li> <li><code>Refresh Token</code> :主要用于获取新的访问令牌,不发送每个请求,通常比访问令牌寿命更长。</li> </ul> <strong>HTTPS上的一个词</strong>:对于任何类型的安全实现,从基本身份验证到完整的OAuth2实现<strong><code>HTTPS</code></strong>都是必须的。没有HTTPS,无论您的实现是什么,安全性都将受到威胁。 <h3>OAuth2访问令牌范围</h3> <p>客户端可以使用范围[想要访问此用户的Facebook帐户的Feed和照片]来查询具有特定访问权限的资源,授权服务器又返回显示实际授予客户端访问权限的范围[资源所有者只允许Feed访问,没有照片例如]。</p> <hr />我们进入代码 <p>我们来实现使用Spring Security实现OAuth的必要构建块,以便访问我们的REST资源。</p> <h3>资源服务器</h3> <p>资源服务器托管客户端感兴趣的资源[我们的REST API]。资源位于<code>/user/</code>。<code>@EnableResourceServer</code>注释,应用于OAuth2资源服务器,启用使用传入OAuth2令牌对请求进行身份验证的Spring Security过滤器。类<code>ResourceServerConfigurerAdapter</code>实现<code>ResourceServerConfigurer</code>提供了调整由OAuth2安全保护的访问规则和路径的方法。</p> <pre> <code class="language-java">package com.websystique.springmvc.security;   import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler;   @Configuration @EnableResourceServer public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {       private static final String RESOURCE_ID = "my_rest_api";           @Override     public void configure(ResourceServerSecurityConfigurer resources) {         resources.resourceId(RESOURCE_ID).stateless(false);     }       @Override     public void configure(HttpSecurity http) throws Exception {         http.         anonymous().disable()         .requestMatchers().antMatchers("/user/**")         .and().authorizeRequests()         .antMatchers("/user/**").access("hasRole('ADMIN')")         .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());     }   }</code></pre> <h3><br /> 授权服务器是一个负责验证凭据的人员,如果凭据确定,提供令牌[刷新令牌以及访问令牌]。它还包含有关注册的客户端和可能的访问范围和授权类型的信息。令牌存储用于存储令牌。我们将使用内存中的令牌存储。<code>@EnableAuthorizationServer</code>在当前应用程序上下文中启用授权服务器(即AuthorizationEndpoint和TokenEndpoint)。类<code>AuthorizationServerConfigurerAdapter</code>实现<code>AuthorizationServerConfigurer</code>,它提供了配置授权服务器的所有必要方法。2.授权服务器</h3> <pre> <code class="language-java"> package com.websystique.springmvc.security;   import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.approval.UserApprovalHandler; import org.springframework.security.oauth2.provider.token.TokenStore;   @Configuration @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {       private static String REALM="MY_OAUTH_REALM";           @Autowired     private TokenStore tokenStore;       @Autowired     private UserApprovalHandler userApprovalHandler;       @Autowired     @Qualifier("authenticationManagerBean")     private AuthenticationManager authenticationManager;       @Override     public void configure(ClientDetailsServiceConfigurer clients) throws Exception {           clients.inMemory()             .withClient("my-trusted-client")             .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")             .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")             .scopes("read", "write", "trust")             .secret("secret")             .accessTokenValiditySeconds(120).//Access token is only valid for 2 minutes.             refreshTokenValiditySeconds(600);//Refresh token is only valid for 10 minutes.     }       @Override     public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {         endpoints.tokenStore(tokenStore).userApprovalHandler(userApprovalHandler)                 .authenticationManager(authenticationManager);     }       @Override     public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {         oauthServer.realm(REALM+"/client");     }   }</code></pre> <p><br /> <br /> 向客户端注册客户端“我的信任客户端”和密码“秘密”以及允许的角色和范围。以上配置</p> <ul> <li>指定任何生成的访问令牌只有120秒有效</li> <li>指定任何生成的刷新令牌只有600秒有效</li> </ul> <h3>3.安全配置</h3> <p>粘在一起 端点<code>/oauth/token</code>用于请求令牌[访问或刷新]。资源所有者[bill,bob]在这里配置。</p> <pre> <code class="language-java">package com.websystique.springmvc.security;   import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.approval.ApprovalStore; import org.springframework.security.oauth2.provider.approval.TokenApprovalStore; import org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler; import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;   @Configuration @EnableWebSecurity public class OAuth2SecurityConfiguration extends WebSecurityConfigurerAdapter {       @Autowired     private ClientDetailsService clientDetailsService;           @Autowired     public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {         auth.inMemoryAuthentication()         .withUser("bill").password("abc123").roles("ADMIN").and()         .withUser("bob").password("abc123").roles("USER");     }       @Override     protected void configure(HttpSecurity http) throws Exception {         http         .csrf().disable()         .anonymous().disable()         .authorizeRequests()         .antMatchers("/oauth/token").permitAll();     }       @Override     @Bean     public AuthenticationManager authenticationManagerBean() throws Exception {         return super.authenticationManagerBean();     }         @Bean     public TokenStore tokenStore() {         return new InMemoryTokenStore();     }       @Bean     @Autowired     public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore){         TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();         handler.setTokenStore(tokenStore);         handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));         handler.setClientDetailsService(clientDetailsService);         return handler;     }           @Bean     @Autowired     public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {         TokenApprovalStore store = new TokenApprovalStore();         store.setTokenStore(tokenStore);         return store;     }       }</code></pre> <p>另外,启用全局方法安全性,如果我们想要使用它,它将激活@PreFilter,@PostFilter,@PreAuthorize @PostAuthorize注释。</p> <pre> <code class="language-java"> package com.websystique.springmvc.security;   import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler;   @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true) public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {     @Autowired     private OAuth2SecurityConfiguration securityConfig;       @Override     protected MethodSecurityExpressionHandler createExpressionHandler() {         return new OAuth2MethodSecurityExpressionHandler();     } }</code></pre> <h3>4.终点及其目的</h3> <ul> <li>尝试访问资源[REST API],无需任何授权[将当然失败]。<br /> <code>GET http://localhost:8080/SpringSecurityOAuth2Example/user/</code></li> <li>问令牌[接入+刷新]使用<strong>HTTP POST</strong>上<code>/oauth/token</code>,与grant_type =密码和资源所有者凭证REQ-PARAMS。另外,在授权头中发送客户端凭据。 <p> </p> <p><code>POST http://localhost:8080/SpringSecurityOAuth2Example/oauth/token?grant_type=password&username=bill&password=abc123</code></p> </li> <li>要求通过有效的刷新令牌新的访问令牌,使用<strong>HTTP POST</strong>上<code>/oauth/token</code>,与grant_type = refresh_token,并发送实际的刷新令牌。另外,在授权头中发送客户端凭据。 <p> </p> <p><code>POST http://localhost:8080/SpringSecurityOAuth2Example/oauth/token?grant_type=refresh_token&refresh_token=094b7d23-973f-4cc1-83ad-8ffd43de1845</code></p> </li> <li>通过使用<code>access_token</code>具有请求的查询参数提供访问令牌来访问资源。<br /> <code>GET http://localhost:8080/SpringSecurityOAuth2Example/user/?access_token=3525d0e4-d881-49e7-9f91-bcfd18259109</code></li> </ul> <h3>5.休息API</h3> <p>我在大部分帖子中使用的简单的Spring REST API。</p> <pre> <code class="language-java"> package com.websystique.springmvc.controller;    import java.util.List;    import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder;    import com.websystique.springmvc.model.User; import com.websystique.springmvc.service.UserService;    @RestController public class HelloWorldRestController {        @Autowired     UserService userService;  //Service which will do all data retrieval/manipulation work               //-------------------Retrieve All Users--------------------------------------------------------            @RequestMapping(value = "/user/", method = RequestMethod.GET)     public ResponseEntity<List<User>> listAllUsers() {         List<User> users = userService.findAllUsers();         if(users.isEmpty()){             return new ResponseEntity<List<User>>(HttpStatus.NO_CONTENT);//You many decide to return HttpStatus.NOT_FOUND         }         return new ResponseEntity<List<User>>(users, HttpStatus.OK);     }           //-------------------Retrieve Single User--------------------------------------------------------            @RequestMapping(value = "/user/{id}", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE,MediaType.APPLICATION_XML_VALUE})     public ResponseEntity<User> getUser(@PathVariable("id") long id) {         System.out.println("Fetching User with id " + id);         User user = userService.findById(id);         if (user == null) {             System.out.println("User with id " + id + " not found");             return new ResponseEntity<User>(HttpStatus.NOT_FOUND);         }         return new ResponseEntity<User>(user, HttpStatus.OK);     }                      //-------------------Create a User--------------------------------------------------------            @RequestMapping(value = "/user/", method = RequestMethod.POST)     public ResponseEntity<Void> createUser(@RequestBody User user, UriComponentsBuilder ucBuilder) {         System.out.println("Creating User " + user.getName());            if (userService.isUserExist(user)) {             System.out.println("A User with name " + user.getName() + " already exist");             return new ResponseEntity<Void>(HttpStatus.CONFLICT);         }            userService.saveUser(user);            HttpHeaders headers = new HttpHeaders();         headers.setLocation(ucBuilder.path("/user/{id}").buildAndExpand(user.getId()).toUri());         return new ResponseEntity<Void>(headers, HttpStatus.CREATED);     }               //------------------- Update a User --------------------------------------------------------            @RequestMapping(value = "/user/{id}", method = RequestMethod.PUT)     public ResponseEntity<User> updateUser(@PathVariable("id") long id, @RequestBody User user) {         System.out.println("Updating User " + id);                    User currentUser = userService.findById(id);                    if (currentUser==null) {             System.out.println("User with id " + id + " not found");             return new ResponseEntity<User>(HttpStatus.NOT_FOUND);         }            currentUser.setName(user.getName());         currentUser.setAge(user.getAge());         currentUser.setSalary(user.getSalary());                    userService.updateUser(currentUser);         return new ResponseEntity<User>(currentUser, HttpStatus.OK);     }        //------------------- Delete a User --------------------------------------------------------            @RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)     public ResponseEntity<User> deleteUser(@PathVariable("id") long id) {         System.out.println("Fetching & Deleting User with id " + id);            User user = userService.findById(id);         if (user == null) {             System.out.println("Unable to delete. User with id " + id + " not found");             return new ResponseEntity<User>(HttpStatus.NOT_FOUND);         }            userService.deleteUserById(id);         return new ResponseEntity<User>(HttpStatus.NO_CONTENT);     }               //------------------- Delete All Users --------------------------------------------------------            @RequestMapping(value = "/user/", method = RequestMethod.DELETE)     public ResponseEntity<User> deleteAllUsers() {         System.out.println("Deleting All Users");            userService.deleteAllUsers();         return new ResponseEntity<User>(HttpStatus.NO_CONTENT);     }    }</code></pre> <h3><br /> 运行应用程序</h3> <p>运行它并使用两个不同的客户端进行测试。<br /> <br />  </p> <h4>客户端1:Postman</h4> <p>尝试访问一个没有任何信息的资源,将获得一个401。</p> <p><img alt="SpringOAuth2_img1" class="img-thumbnail" src="/resources/assist/images/blog/bbd70c98-9e02-46e9-b4b7-c4bdfadb1d93.png" /></p> <p>让我们得到令牌。首先添加一个<strong>客户端凭证</strong> [my-trusted-client / secret] 的授权头文件。</p> <p><img alt="SpringOAuth2_img2" class="img-thumbnail" src="/resources/assist/images/blog/9c19299c-5b53-4dfb-97f7-30b81963ac1f.png" /></p> <p>单击更新请求,验证标题标签中的标题。</p> <p><img alt="SpringOAuth2_img3" class="img-thumbnail" src="/resources/assist/images/blog/e404605e-f273-476d-a582-0147146e96d9.png" /></p> <p>发送POST请求时,您将收到包含响应<code>access-token</code>以及<code>refresh-token</code>。</p> <p><img alt="SpringOAuth2_img4" class="img-thumbnail" src="/resources/assist/images/blog/a0e2e85e-0a1b-40f8-86e4-3c9f1c062276.png" /></p> <p>将这些令牌保存在某个地方,您将需要它们。现在,您可以使用此访问令牌(有效期为2分钟)来访问资源。</p> <p><img alt="SpringOAuth2_img5" class="img-thumbnail" src="/resources/assist/images/blog/77e1a43f-c866-4b33-b1f0-f1c7d5232f89.png" /></p> <p>2分钟后,access-token将过期,您的进一步资源请求将失败。</p> <p><img alt="SpringOAuth2_img6" class="img-thumbnail" src="/resources/assist/images/blog/dc75aaec-9ce3-4fe0-8bb7-c98e3e8c6054.png" /></p> <p>我们需要一个新的访问令牌。通过刷新令牌来触发一个帖子,以获得全新的访问令牌。</p> <p><img alt="SpringOAuth2_img7" class="img-thumbnail" src="/resources/assist/images/blog/34a5980f-0d49-416a-adf4-fe4182056648.png" /></p> <p>使用这个新的访问令牌访问资源。</p> <p><img alt="SpringOAuth2_img8" class="img-thumbnail" src="/resources/assist/images/blog/4e0b3771-92ed-438a-bc0f-15de7bc6fcf1.png" /></p> <p>刷新令牌也过期[10分钟]。之后,您将看到刷新请求失败。</p> <p><img alt="SpringOAuth2_img9" class="img-thumbnail" src="/resources/assist/images/blog/c8760e84-cdfa-4760-ad3e-0ff07768c117.png" /></p> <p>这意味着您需要请求新的刷新+访问令牌,如步骤2中所示。</p> <h4>客户端2:基于RestTemplate的java应用程序</h4> <p>方法<strong>sendTokenRequest</strong>用于实际获取令牌。然后,我们收到的访问令牌将被用于每个请求。如果需要,您可以在下面的示例中轻松实现刷新令牌流。</p> <pre> <code class="language-java">package com.websystique.springmvc; import java.net.URI; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import org.apache.commons.codec.binary.Base64; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.web.client.RestTemplate; import com.websystique.springmvc.model.AuthTokenInfo; import com.websystique.springmvc.model.User; public class SpringRestClient { public static final String REST_SERVICE_URI = "http://localhost:8080/SpringSecurityOAuth2Example"; public static final String AUTH_SERVER_URI = "http://localhost:8080/SpringSecurityOAuth2Example/oauth/token"; public static final String QPM_PASSWORD_GRANT = "?grant_type=password&username=bill&password=abc123"; public static final String QPM_ACCESS_TOKEN = "?access_token="; /* * Prepare HTTP Headers. */ private static HttpHeaders getHeaders(){ HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); return headers; } /* * Add HTTP Authorization header, using Basic-Authentication to send client-credentials. */ private static HttpHeaders getHeadersWithClientCredentials(){ String plainClientCredentials="my-trusted-client:secret"; String base64ClientCredentials = new String(Base64.encodeBase64(plainClientCredentials.getBytes())); HttpHeaders headers = getHeaders(); headers.add("Authorization", "Basic " + base64ClientCredentials); return headers; } /* * Send a POST request [on /oauth/token] to get an access-token, which will then be send with each request. */ @SuppressWarnings({ "unchecked"}) private static AuthTokenInfo sendTokenRequest(){ RestTemplate restTemplate = new RestTemplate(); HttpEntity<String> request = new HttpEntity<String>(getHeadersWithClientCredentials()); ResponseEntity<Object> response = restTemplate.exchange(AUTH_SERVER_URI+QPM_PASSWORD_GRANT, HttpMethod.POST, request, Object.class); LinkedHashMap<String, Object> map = (LinkedHashMap<String, Object>)response.getBody(); AuthTokenInfo tokenInfo = null; if(map!=null){ tokenInfo = new AuthTokenInfo(); tokenInfo.setAccess_token((String)map.get("access_token")); tokenInfo.setToken_type((String)map.get("token_type")); tokenInfo.setRefresh_token((String)map.get("refresh_token")); tokenInfo.setExpires_in((int)map.get("expires_in")); tokenInfo.setScope((String)map.get("scope")); System.out.println(tokenInfo); //System.out.println("access_token ="+map.get("access_token")+", token_type="+map.get("token_type")+", refresh_token="+map.get("refresh_token") //+", expires_in="+map.get("expires_in")+", scope="+map.get("scope"));; }else{ System.out.println("No user exist----------"); } return tokenInfo; } /* * Send a GET request to get list of all users. */ @SuppressWarnings({ "unchecked", "rawtypes" }) private static void listAllUsers(AuthTokenInfo tokenInfo){ Assert.notNull(tokenInfo, "Authenticate first please......"); System.out.println("\nTesting listAllUsers API-----------"); RestTemplate restTemplate = new RestTemplate(); HttpEntity<String> request = new HttpEntity<String>(getHeaders()); ResponseEntity<List> response = restTemplate.exchange(REST_SERVICE_URI+"/user/"+QPM_ACCESS_TOKEN+tokenInfo.getAccess_token(), HttpMethod.GET, request, List.class); List<LinkedHashMap<String, Object>> usersMap = (List<LinkedHashMap<String, Object>>)response.getBody(); if(usersMap!=null){ for(LinkedHashMap<String, Object> map : usersMap){ System.out.println("User : id="+map.get("id")+", Name="+map.get("name")+", Age="+map.get("age")+", Salary="+map.get("salary"));; } }else{ System.out.println("No user exist----------"); } } /* * Send a GET request to get a specific user. */ private static void getUser(AuthTokenInfo tokenInfo){ Assert.notNull(tokenInfo, "Authenticate first please......"); System.out.println("\nTesting getUser API----------"); RestTemplate restTemplate = new RestTemplate(); HttpEntity<String> request = new HttpEntity<String>(getHeaders()); ResponseEntity<User> response = restTemplate.exchange(REST_SERVICE_URI+"/user/1"+QPM_ACCESS_TOKEN+tokenInfo.getAccess_token(), HttpMethod.GET, request, User.class); User user = response.getBody(); System.out.println(user); } /* * Send a POST request to create a new user. */ private static void createUser(AuthTokenInfo tokenInfo) { Assert.notNull(tokenInfo, "Authenticate first please......"); System.out.println("\nTesting create User API----------"); RestTemplate restTemplate = new RestTemplate(); User user = new User(0,"Sarah",51,134); HttpEntity<Object> request = new HttpEntity<Object>(user, getHeaders()); URI uri = restTemplate.postForLocation(REST_SERVICE_URI+"/user/"+QPM_ACCESS_TOKEN+tokenInfo.getAccess_token(), request, User.class); System.out.println("Location : "+uri.toASCIIString()); } /* * Send a PUT request to update an existing user. */ private static void updateUser(AuthTokenInfo tokenInfo) { Assert.notNull(tokenInfo, "Authenticate first please......"); System.out.println("\nTesting update User API----------"); RestTemplate restTemplate = new RestTemplate(); User user = new User(1,"Tomy",33, 70000); HttpEntity<Object> request = new HttpEntity<Object>(user, getHeaders()); ResponseEntity<User> response = restTemplate.exchange(REST_SERVICE_URI+"/user/1"+QPM_ACCESS_TOKEN+tokenInfo.getAccess_token(), HttpMethod.PUT, request, User.class); System.out.println(response.getBody()); } /* * Send a DELETE request to delete a specific user. */ private static void deleteUser(AuthTokenInfo tokenInfo) { Assert.notNull(tokenInfo, "Authenticate first please......"); System.out.println("\nTesting delete User API----------"); RestTemplate restTemplate = new RestTemplate(); HttpEntity<String> request = new HttpEntity<String>(getHeaders()); restTemplate.exchange(REST_SERVICE_URI+"/user/3"+QPM_ACCESS_TOKEN+tokenInfo.getAccess_token(), HttpMethod.DELETE, request, User.class); } /* * Send a DELETE request to delete all users. */ private static void deleteAllUsers(AuthTokenInfo tokenInfo) { Assert.notNull(tokenInfo, "Authenticate first please......"); System.out.println("\nTesting all delete Users API----------"); RestTemplate restTemplate = new RestTemplate(); HttpEntity<String> request = new HttpEntity<String>(getHeaders()); restTemplate.exchange(REST_SERVICE_URI+"/user/"+QPM_ACCESS_TOKEN+tokenInfo.getAccess_token(), HttpMethod.DELETE, request, User.class); } public static void main(String args[]){ AuthTokenInfo tokenInfo = sendTokenRequest(); listAllUsers(tokenInfo); getUser(tokenInfo); createUser(tokenInfo); listAllUsers(tokenInfo); updateUser(tokenInfo); listAllUsers(tokenInfo); deleteUser(tokenInfo); listAllUsers(tokenInfo); deleteAllUsers(tokenInfo); listAllUsers(tokenInfo); } }</code></pre> <br /> 以上代码将产生以下输出: <pre> <code>AuthTokenInfo [access_token=fceed386-5923-4bf8-b193-1d76f95da4c4, token_type=bearer, refresh_token=29d28ee2-9d09-483f-a2d6-7f93e7a31667, expires_in=71, scope=read write trust] Testing listAllUsers API----------- User : id=1, Name=Sam, Age=30, Salary=70000.0 User : id=2, Name=Tom, Age=40, Salary=50000.0 User : id=3, Name=Jerome, Age=45, Salary=30000.0 User : id=4, Name=Silvia, Age=50, Salary=40000.0 Testing getUser API---------- User [id=1, name=Sam, age=30, salary=70000.0] Testing create User API---------- Location : http://localhost:8080/SpringSecurityOAuth2Example/user/5 Testing listAllUsers API----------- User : id=1, Name=Sam, Age=30, Salary=70000.0 User : id=2, Name=Tom, Age=40, Salary=50000.0 User : id=3, Name=Jerome, Age=45, Salary=30000.0 User : id=4, Name=Silvia, Age=50, Salary=40000.0 User : id=5, Name=Sarah, Age=51, Salary=134.0 Testing update User API---------- User [id=1, name=Tomy, age=33, salary=70000.0] Testing listAllUsers API----------- User : id=1, Name=Tomy, Age=33, Salary=70000.0 User : id=2, Name=Tom, Age=40, Salary=50000.0 User : id=3, Name=Jerome, Age=45, Salary=30000.0 User : id=4, Name=Silvia, Age=50, Salary=40000.0 User : id=5, Name=Sarah, Age=51, Salary=134.0 Testing delete User API---------- Testing listAllUsers API----------- User : id=1, Name=Tomy, Age=33, Salary=70000.0 User : id=2, Name=Tom, Age=40, Salary=50000.0 User : id=4, Name=Silvia, Age=50, Salary=40000.0 User : id=5, Name=Sarah, Age=51, Salary=134.0 Testing all delete Users API---------- Testing listAllUsers API----------- No user exist----------</code></pre>   <h3>项目结构</h3> <img alt="10" class="img-thumbnail" src="/resources/assist/images/blog/aab904e5-276f-4fac-8bd4-3762adb1a28c.png" /> <h3>pom.xml</h3> <pre> <code class="language-xml"><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.websystique.springmvc</groupId> <artifactId>SpringSecurityOAuth2Example</artifactId> <version>1.0.0</version> <packaging>war</packaging> <name>SpringSecurityOAuth2Example</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <springframework.version>4.3.1.RELEASE</springframework.version> <springsecurity.version>4.1.1.RELEASE</springsecurity.version> <springsecurityoauth2.version>2.0.10.RELEASE</springsecurityoauth2.version> <jackson.library>2.7.5</jackson.library> </properties> <dependencies> <!-- Spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${springframework.version}</version> </dependency> <!-- Spring Security --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>${springsecurity.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>${springsecurity.version}</version> </dependency> <!-- Spring Security OAuth2--> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>${springsecurityoauth2.version}</version> </dependency> <!-- Jackson libraries --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.library}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>${jackson.library}</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.2</version> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.4</version> <configuration> <warSourceDirectory>src/main/webapp</warSourceDirectory> <warName>SpringSecurityOAuth2Example</warName> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> <finalName>SpringSecurityOAuth2Example</finalName> </build> </project></code></pre> <h4><strong><a href="http://websystique.com/?smd_process_download=1&download_id=2926" rel="external nofollow" target="_blank"><em><u>下载源代码</u></em></a></strong></h4> <br />  
  • Spring Boot Security Oauth2 Jwt Auth 详细实战例子

    Spring Boot Security Oauth2 Jwt Auth 详细实战例子,在本文中,我们将讨论有关Spring启动安全性和JWT令牌的OAUTH2实现以及保护REST API。<h2>引言</h2> <blockquote> <p>在本文中,我们将讨论有关Spring启动安全性和JWT令牌的OAUTH2实现以及保护REST API。在我上一篇<a href="http://www.leftso.com/blog/382.html" rel="" style="box-sizing:border-box; background-color:#ffffff; color:#337ab7; text-decoration:none; font-family:"Varela Round", sans-serif; font-size:17px; font-style:normal; font-variant-ligatures:normal; font-variant-caps:normal; font-weight:normal; letter-spacing:normal; orphans:2; text-align:start; text-transform:none; white-space:pre-wrap; widows:2; word-spacing:0px; -webkit-text-stroke-width:0px" target="_blank" >Spring Boot Security OAUTH2示例中</a>,我们使用OAUTH2创建了一个示例应用程序,用于使用默认令牌存储进行身份验证和授权,但Spring安全性OAUTH2实现还提供了定义自定义令牌存储的功能。在这里,我们将使用<span style="color:#025969"><span style="font-family:"Varela Round",sans-serif"><span style="background-color:#ffffff">JwtTokenStore</span></span></span>创建一个示例spring security OAUTH2应用程序。使用JwtTokenStore作为令牌提供程序允许我们自定义使用TokenEnhancer生成的令牌以添加其他声明。</p> </blockquote>     这个应用程序中的大部分配置与我以前的spring security OAUTH2实现文章非常相似,因此我们可能会避免在最后一个应用程序中构建的一些通用代码和配置。让我们从简单介绍JWT开始,然后我们将深入创建我们的授权服务器,资源服务器,稍后我们将讨论在令牌中添加自定义声明。<br />   <h2>Json Web Token</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><a href="https://jwt.io/" rel="external nofollow" style="box-sizing:border-box; color:#337ab7; text-decoration:none" target="_blank" >      JSON Web Token(JWT)</a>是一个开放标准(RFC 7519),它定义了一个紧凑且独立的方式,用于在各方之间以JSON对象安全地传输信息。一种无状态身份验证机制,因为用户状态永远不会保存在服务器内存中。 JWT令牌由3个部分组成,用点(。)即Header.payload.signature分隔</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">头部包含2部分类型的令牌和散列算法。包含这两个键的JSON结构是Base64Encoded。</span></span></span><br />  </p> <pre> <code class="language-json">{ "alg": "HS256", "typ": "JWT" }</code></pre> <br /> 有效载荷包含claims。主要有三种类型的claims:保留,reserved, public, private。保留的claims是iss(发行人),exp(到期时间),sub(主题),aud(受众)等预定义claims。在private claims中,我们可以创建一些自定义claims,如主题,角色和其他。 <pre> <code class="language-json">{ "sub": "Alex123", "scopes": [ { "authority": "ROLE_ADMIN" } ], "iss": "http://devglan.com", "iat": 1508607322, "exp": 1508625322 }</code></pre> 签名确保令牌不会在途中发生变化。例如,如果您想使用HMAC SHA256算法,签名将按以下方式创建: <pre> <code class="language-html">HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)</code></pre> 以下是示例JWT令牌。这是一个带有jwt认证应用程序的完整堆栈spring boot程序,用于使用jwt令牌机制保护REST API。 <pre> <code class="language-html">eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBbGV4MTIzIiwic2N.v9A80eU1VDo2Mm9UqN2FyEpyT79IUmhg</code></pre>   <h2 style="margin-left:0px; margin-right:0px; text-align:start">项目结构</h2> <img srcset="" width="" size="" class="img-thumbnail" alt="spring boot项目结构图" src="/resources/assist/images/blog/f250254aeb2c464a89424258b6c23fce.png" /> <h2 style="margin-left:0px; margin-right:0px; text-align:start">Maven的依赖</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">在这里,spring-security-jwt提供了对JwtTokenStore的支持。</span></span></span></p> <pre> <code class="language-xml"><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> </dependency></code></pre> <h2 style="margin-left:0px; margin-right:0px; text-align:start">授权服务器配置</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">我希望你熟悉OAUTH2架构和授权服务器。我在我上一篇文章OAUTH2中解释了它。下面的配置与我们上一次配置的<a href="http://www.leftso.com/blog/382.html" rel="" style="box-sizing:border-box; color:#337ab7; text-decoration:none" target="_blank" >Spring Boot Security OAUTH2实例</a>非常相似,除了JwtAccessTokenConverter和TokenStore.Here,JwtAccessTokenConverter是在JWT编码的令牌值和OAuth之间转换的帮助器认证信息。我们添加了自定义签名以使JWT令牌更健壮。除了JwtTokenStore,spring安全性还提供了InMemoryTokenStore和JdbcTokenStore。 </span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><code>ClientDetailsServiceConfigurer</code>:定义客户端详细信息服务的配置器。客户详细信息可以初始化,或者您可以参考现有的商店。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><code>AuthorizationServerSecurityConfigurer</code> :定义了令牌端点上的安全约束。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><code>AuthorizationServerEndpointsConfigurer</code> :定义授权和令牌端点以及令牌服务。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">ClientDetailsS​​erviceConfigurer可用于定义客户端详细信息服务的内存中或JDBC实现。在此示例中,我们使用的是内存中的实现。</span></span></span><br /> <br /> <strong>AuthorizationServerConfig.java</strong><br />  </p> <pre> <code class="language-java">@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { static final String CLIEN_ID = "devglan-client"; static final String CLIENT_SECRET = "devglan-secret"; static final String GRANT_TYPE_PASSWORD = "password"; static final String AUTHORIZATION_CODE = "authorization_code"; static final String REFRESH_TOKEN = "refresh_token"; static final String IMPLICIT = "implicit"; static final String SCOPE_READ = "read"; static final String SCOPE_WRITE = "write"; static final String TRUST = "trust"; static final int ACCESS_TOKEN_VALIDITY_SECONDS = 1*60*60; static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 6*60*60; @Autowired private AuthenticationManager authenticationManager; @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("as466gf"); return converter; } @Bean public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Override public void configure(ClientDetailsServiceConfigurer configurer) throws Exception { configurer .inMemory() .withClient(CLIEN_ID) .secret(CLIENT_SECRET) .authorizedGrantTypes(GRANT_TYPE_PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT ) .scopes(SCOPE_READ, SCOPE_WRITE, TRUST) .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS). refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore()) .authenticationManager(authenticationManager) .accessTokenConverter(accessTokenConverter()); } }</code></pre> <h2 style="margin-left:0px; margin-right:0px; text-align:start">资源服务器配置</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">我们的上下文中的资源是我们为粗暴操作公开的REST API。要访问这些资源,必须对客户端进行身份验证。在实时场景中,每当用户尝试访问这些资源时,都会要求用户提供他真实性,一旦用户被授权,他将被允许访问这些受保护的资源。 </span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><code>resourceId:</code> 资源的id(可选,但建议并且将由auth服务器验证,如果存在的话)。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">因为我们在同一个项目中有资源服务器和服务器实现,所以我们不需要在资源服务器配置中重新定义我们的JwtAccessTokenConverter,否则我们需要在资源服务器中提供类似的JwtAccessTokenConverter实现。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">在这里,我们已经配置<span style="color:#025969">/ users</span>是一个受保护的资源,它需要一个ADMIN角色进行访问。</span></span></span><br />  </p> <pre> <code class="language-java">@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { private static final String RESOURCE_ID = "resource_id"; @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId(RESOURCE_ID).stateless(false); } @Override public void configure(HttpSecurity http) throws Exception { http. anonymous().disable() .authorizeRequests() .antMatchers("/users/**").access("hasRole('ADMIN')") .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler()); } }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start">REST API</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">现在让我们使用spring控制器公开一些受保护的REST资源。</span></span></span><br />  </p> <pre> <code class="language-java">@RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @RequestMapping(value="/user", method = RequestMethod.GET) public List listUser(){ return userService.findAll(); } @RequestMapping(value = "/user", method = RequestMethod.POST) public User create(@RequestBody User user){ return userService.save(user); } @RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE) public String delete(@PathVariable(value = "id") Long id){ userService.delete(id); return "success"; } }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start">以下是验证用户的用户服务实现。<br />  </p> <pre> <code class="language-java">@Service(value = "userService") public class UserServiceImpl implements UserDetailsService, UserService { @Autowired private UserDao userDao; public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException { User user = userDao.findByUsername(userId); if(user == null){ throw new UsernameNotFoundException("Invalid username or password."); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthority()); } private List getAuthority() { return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")); } public List findAll() { List list = new ArrayList<>(); userDao.findAll().iterator().forEachRemaining(list::add); return list; } } </code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start">以上用户服务在SecurityConfig.java中配置如下。您可以使用此在线Bcrypt计算器来生成Bcrypt密码。</p> <pre> <code class="language-java">@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource(name = "userService") private UserDetailsService userDetailsService; @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Autowired public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(encoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .anonymous().disable() .authorizeRequests() .antMatchers("/api-docs/**").permitAll(); } @Bean public BCryptPasswordEncoder encoder(){ return new BCryptPasswordEncoder(); } @Bean public FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); bean.setOrder(0); return bean; } } </code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start">创建用户SQL脚本</h2> <pre> <code class="language-sql">INSERT INTO User (id, username, password, salary, age) VALUES (1, 'Alex123', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 3456, 33); INSERT INTO User (id, username, password, salary, age) VALUES (2, 'Tom234', '$2a$04$PCIX2hYrve38M7eOcqAbCO9UqjYg7gfFNpKsinAxh99nms9e.8HwK', 7823, 23); INSERT INTO User (id, username, password, salary, age) VALUES (3, 'Adam', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 4234, 45);</code></pre> <h2 style="margin-left:0px; margin-right:0px; text-align:start">测试OAUTH2 JWT应用程序</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">首先,运行Application.java作为java程序并切换到POSTMAIN,并通过http:// localhost:8080 / oauth / token发出POST请求以生成标记。在标头中,我们选择了基本身份验证并提供了用户名和密码as <code>devglan-client</code>和<code>devglan-secret</code>。这将导致access_token,token_type,refresh_token,过期等。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><img srcset="" width="" size="" class="img-thumbnail" alt="postmain请求" src="/resources/assist/images/blog/b3b0c6c4f22946aab3d7369e5f95f0c3.png" /></span></span></span><br /> 现在,我们可以使用相同的标记来访问受保护的资源。<br /> <span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff"><img srcset="" width="" size="" class="img-thumbnail" alt="访问受保护的资源" src="/resources/assist/images/blog/ff5ac84833304e518c91c73b4bdfa6e7.png" /></span></span></span><br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start">总结</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">在这里,我们讨论了如何使用JWT作为Spring启动安全性OAUTH2实现的令牌提供者。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><br /> <br /> <a href="http://www.leftso.com/resource/1007.html" target="_blank" ><span style="font-family:"Varela Round",sans-serif"><span style="color:#333333"><span style="background-color:#ffffff">项目源码下载</span></span></span></a></p>
  • springboot 使用thymeleaf url get请求传多个参数问题解决

    springboot 使用thymeleaf 模板引擎中url中的&引起的org.xml.sax.SAXParseException: 对实体 "uid" 的引用必须以 ';' 分隔符结尾。问题解决<h2>1.问题描述</h2> 在使用thymeleaf模板中,有些时候需要绝对路径带多个参数的地址访问其他页面,这时候由于&字符的问题会报错:<br /> org.xml.sax.SAXParseException: 对实体 "xxx" 的引用必须以 ';' 分隔符结尾。 <h2>2.问题解决</h2> 使用逆转字符代替&,>,<这些符号<br /> 例如: <pre> <code class="language-html"><a class="third-login-qq" href="https://graph.qq.com/oauth2.0/authorize? response_type=code&amp;client_id=101395666&amp;redirect_uri=http://www.leftso.com/user/login.html&amp;scope=get_user_info&amp;state=test" id="qqLoginBtn"></a></code></pre>
  • Spring Boot Security Swagger2整合生成安全的在线REST API文档 SpringMVC也可参考

    创建REST API时,良好的文档是有帮助的。而且,API中的每一个变化都应该在参考文档中同时描述。手动完成这是一个乏味的操作,因此这个过程的自动化是不可避免的。<h2 style="margin-left:0px; margin-right:0px; text-align:start"><strong>1.概述</strong></h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">创建REST API时,良好的文档是有帮助的。</span></span></span><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">而且,API中的每一个变化都应该在参考文档中同时描述。手动完成这是一个乏味的操作,因此这个过程的自动化是不可避免的。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">在本教程中,我们将看看  <strong>Swagger 2的Spring Boot REST Web服务。</strong>对于本文,我们将使用Swagger 2规范的  <strong>Springfox </strong>实现。如果您对Swagger不熟悉,则应 在继续阅读本文之前访问<a href="http://swagger.io/" rel="external nofollow" style="box-sizing:border-box; color:#63b175; text-decoration:none" target="_blank">swagger官网</a>以了解更多信息。</span></span></span><br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start"><strong>2.项目说明</strong></h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">我们在示例中使用的REST服务的创建不在本文的讨论范围之内。如果您已经有合适的项目,请使用它。如果没有,下面的链接是一个很好的开始:</span></span></span></p> <ul style="list-style-type:disc"> <li>使用Spring 4和Java Config文章构建REST API</li> <li>构建一个RESTful Web服务</li> </ul> <p style="margin-left:0px; margin-right:0px; text-align:start"><strong>提示:创建Spring Boot REST项目可以参考之前写的<a rel="" target="_blank"href="http://www.leftso.com/blog/223.html" rel="" target="_blank">Swagger2入门篇</a></strong></p> <h2 style="margin-left:0px; margin-right:0px; text-align:start"><br /> <strong>3.添加Maven依赖项</strong></h2> 这里我们只说需要额外添加的maven依赖。Spring Boot REST项目本身的依赖决定于您自身的项目,暂时不在讨论之内。上一篇我们使用的swagger 版本为2.6.2这里我们将会使用比较新的2.7.0 <pre> <code class="language-xml"><dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.7.0</version> </dependency></code></pre> <blockquote> <p>提示:现在已经有2.8.0版本。目前存在一些无法汉化的问题。暂不推荐正式项目升级。学习研究即可。</p> </blockquote> <h2 style="margin-left:0px; margin-right:0px; text-align:start"><strong>4.将Swagger 2集成到项目中</strong></h2> <h3 style="margin-left:0px; margin-right:0px; text-align:start"><strong>4.1。Java配置</strong></h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">Swagger的配置主要围绕着  <em><strong>Docket</strong></em> bean。</span></span></span></p> <pre> <code class="language-java">@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.any()) .paths(PathSelectors.any()) .build(); } }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">Swagger 2通过<strong>@ EnableSwagger2</strong><em> </em>批注启用 。</span></span></span><br /> <br /> <span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">在定义了<em>Docket</em> bean 之后,其  <strong>select()</strong>  方法返回一个<strong>ApiSelectorBuilder</strong>实例,该实例提供了一种控制Swagger公开的端点的方法。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">可以使用<strong>RequestHandlerSelectors</strong>和<strong>PathSelectors</strong>来配置<em>RequestHandler</em>选择的谓词。使用  <em>任何()</em>  都可以通过Swagger为整个API提供文档。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">这种配置足以将Swagger 2集成到现有的Spring Boot项目中。对于其他Spring项目,需要进行一些额外的调整。</span></span></span></p> <h3 style="margin-left:0px; margin-right:0px; text-align:start"><strong>4.2。项目不是Spring Boot的配置</strong></h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">如果没有Spring Boot,你无法自动配置资源处理程序。Swagger UI添加了一组资源,您必须将其配置为扩展<em>WebMvcConfigurerAdapter</em>的类的一部分  <em>,</em>  并使用<em>@EnableWebMvc进行</em>注释  <em>。</em></span></span></span></p> <pre> <code class="language-java">@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("swagger-ui.html") .addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/"); }</code></pre>   <h3 style="margin-left:0px; margin-right:0px; text-align:start"><strong>4.3。验证Springfox是否正常工作</strong></h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">要验证Springfox是否正常工作,可以在浏览器中访问以下URL:</span></span></span></p> <pre> <code class="language-html">​​​​​​​http://localhost:8080/spring-security-rest/api/v2/api-docs</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">结果是带有大量键值对的JSON响应,这不是非常容易理解的。幸运的是,Swagger 为此提供了<strong>Swagger UI</strong>。</span></span></span></p> <h2 style="margin-left:0px; margin-right:0px; text-align:start"><strong>5. Swagger UI</strong></h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">Swagger UI是一个内置的解决方案,使得用户可以更轻松地与Swagger生成的API文档进行交互。</span></span></span></p> <h3 style="margin-left:0px; margin-right:0px; text-align:start"><strong>5.1。启用S​​pringfox的Swagger UI</strong></h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">要使用Swagger UI,需要另外一个Maven依赖项:</span></span></span></p> <pre> <code class="language-xml"><dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.7.0</version> </dependency></code></pre> 现在,您可以通过访问<em>http://localhost:8080/your-app-root/swagger-ui.html</em><span style="color:#535353; font-family:raleway">在浏览器中对其进行测试</span> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff"><strong>在我们的例子中,顺便说一下,确切的URL是</strong></span></span></span><em>http://localhost:8080/spring-security-rest/api/swagger-ui.html</em></p> 结果应该如下所示:<br /> <img alt="swagger2 UI结果展示" class="img-thumbnail" src="/resources/assist/images/blog/87611d78ac3145fbacc0fbe040cd8b8b.png" /><br />   <h3 style="margin-left:0px; margin-right:0px; text-align:start"><strong>5.2。学习Swagger文档</strong></h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">Swagger的回复中<strong>列出了</strong>您的应用程序中定义<strong>的所有控制器</strong>。点击其中的任何一个将列出有效的HTTP方法(<em>DELETE</em>,<em>GET</em>,<em>HEAD</em>,<em>OPTIONS</em>,<em>PATCH</em>,<em>POST</em>,<em>PUT</em>)。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">展开每种方法都会提供其他有用的数据,例如响应状态,内容类型和参数列表。也可以使用UI来尝试每种方法。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">Swagger与您的代码库同步的能力至关重要。为了演示这一点,你可以添加一个新的控制器到你的应用程序。</span></span></span></p> <pre> <code class="language-java">@RestController public class CustomController { @RequestMapping(value = "/custom", method = RequestMethod.POST) public String custom() { return "custom"; } }</code></pre> 现在,如果刷新Swagger文档,您将在控制器列表中看到<strong>自定义</strong>控制器。如您所知,Swagger的回复中只显示一种方法(<em>POST</em>)。 <h2 style="margin-left:0px; margin-right:0px; text-align:start"><strong>6.高级配置</strong></h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">应用程序的<em>Docket</em> bean可以配置为让您更多地控制API文档生成过程。</span></span></span></p> <h3 style="margin-left:0px; margin-right:0px; text-align:start"><strong>6.1。过滤Swagger响应的API</strong></h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">公开整个API的文档并不总是可取的。您可以通过将参数传递给<em>Docket</em>类的<em><strong>apis()</strong></em>和<em><strong>paths()</strong></em>方法来限制Swagger的响应  。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">如上所示,<em>RequestHandlerSelectors</em>允许使用  <em>any</em>或<em>none</em>谓词,但也可用于根据基本包,类注释和方法注释过滤API。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff"><em><strong>PathSelectors</strong></em>提供了额外的过滤功能,并使用谓词来扫描应用程序的请求路径。你可以使用 <em> any()</em>,<em>none(),  </em><em>regex()</em>或 <em> ant()</em>。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">在下面的示例中,我们将指示Swagger使用<em>ant()</em>  谓词仅包含特定包中的控制器,并使用特定的路径。</span></span></span><br />  </p> <pre> <code class="language-java">@Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("org.baeldung.web.controller")) .paths(PathSelectors.ant("/foos/*")) .build(); }</code></pre> <h3 style="margin-left:0px; margin-right:0px; text-align:start"><strong>6.2。自定义信息</strong></h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">Swagger还在您的自定义响应中提供了一些默认值,例如“Api文档”,“由联系人电子邮件创建”,“Apache 2.0”。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">要更改这些值,可以使用  <strong><em>apiInfo(ApiInfo apiInfo) </em></strong>方法。包含有关API的自定义信息的  <strong><em>ApiInfo</em></strong>类。</span></span></span></p> <pre> <code class="language-java">@Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("com.example.controller")) .paths(PathSelectors.ant("/foos/*")) .build() .apiInfo(apiInfo()); } private ApiInfo apiInfo() { return new ApiInfo( "My REST API", "Some custom description of API.", "API TOS", "Terms of service", new Contact("John Doe", "www.example.com", "myeaddress@company.com"), "License of API", "API license URL", Collections.emptyList()); }</code></pre> <h3 style="margin-left:0px; margin-right:0px; text-align:start"><strong>6.3。自定义方法响应消息</strong></h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">Swagger允许通过  <em>Docket</em>的  <strong><em>globalResponseMessage()</em></strong>方法<strong>全局覆盖HTTP方法的响应消息</strong>。首先,您必须指示Swagger不要使用默认响应消息。<strong> </strong></span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">假设您希望覆盖  所有<em>GET</em>方法的<strong>500</strong>和<strong>403</strong>响应消息。为了达到这个目的,一些代码必须被添加到<em>Docket</em>的初始化块(为了清楚起见,原始冗余代码被排除):</span></span></span></p> <pre> <code class="language-java">.useDefaultResponseMessages(false) .globalResponseMessage(RequestMethod.GET, newArrayList(new ResponseMessageBuilder() .code(500) .message("500 message") .responseModel(new ModelRef("Error")) .build(), new ResponseMessageBuilder() .code(403) .message("Forbidden!") .build()));</code></pre> <img alt="swagger UI 2" class="img-thumbnail" src="/resources/assist/images/blog/62c77efb299f44e3a2c90d0326531f2f.png" /> <h2 style="margin-left:0px; margin-right:0px; text-align:start"><strong>7.具有OAuth安全API的Swagger UI</strong></h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">Swagger UI提供了许多非常有用的功能 - 目前为止我们已经介绍了这些功能。但是如果我们的API是安全的并且不可访问的话,我们不能真正使用其中的大部分。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">让我们看看我们如何允许Swagger访问OAuth安全的API--在本例中使用授权代码授权类型。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">我们将使用<em>SecurityScheme</em>和<em>SecurityContext</em>支持配置Swagger以访问我们的安全API  :</span></span></span></p> <pre> <code class="language-java">@Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2).select() .apis(RequestHandlerSelectors.any()) .paths(PathSelectors.any()) .build() .securitySchemes(Arrays.asList(securityScheme())) .securityContexts(Arrays.asList(securityContext())); }</code></pre> <h3 style="margin-left:0px; margin-right:0px; text-align:start"><strong>7.1。安全配置</strong></h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">我们将在Swagger配置中定义一个  <em>SecurityConfiguration</em> bean - 并设置一些默认值:</span></span></span></p> <pre> <code class="language-java">@Bean public SecurityConfiguration security() { return SecurityConfigurationBuilder.builder() .clientId(CLIENT_ID) .clientSecret(CLIENT_SECRET) .scopeSeparator(" ") .useBasicAuthenticationWithAccessCodeGrant(true) .build(); }</code></pre>   <h3 style="margin-left:0px; margin-right:0px; text-align:start"><strong>7.2。<em>SecurityScheme</em></strong></h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">接下来,我们将定义我们的<em>SecurityScheme</em> ; 这用于描述我们的API如何保护(基本认证,OAuth2,...)。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">在我们的例子中,我们将定义一个用于保护我们的资源服务器的OAuth方案:</span></span></span></p> <pre> <code class="language-java">private SecurityScheme securityScheme() { GrantType grantType = new AuthorizationCodeGrantBuilder() .tokenEndpoint(new TokenEndpoint(AUTH_SERVER + "/token", "oauthtoken")) .tokenRequestEndpoint( new TokenRequestEndpoint(AUTH_SERVER + "/authorize", CLIENT_ID, CLIENT_ID)) .build(); SecurityScheme oauth = new OAuthBuilder().name("spring_oauth") .grantTypes(Arrays.asList(grantType)) .scopes(Arrays.asList(scopes())) .build(); return oauth; }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">请注意,我们使用了授权码授权类型 - 我们需要为其提供令牌端点以及我们的OAuth2授权服务器的授权URL。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">以下是我们需要定义的范围:</span></span></span></p> <pre> <code class="language-java">private AuthorizationScope[] scopes() { AuthorizationScope[] scopes = { new AuthorizationScope("read", "for read operations"), new AuthorizationScope("write", "for write operations"), new AuthorizationScope("foo", "Access foo API") }; return scopes; }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">这些与我们在应用中实际定义的范围同步<em>/ foos</em> API。</span></span></span></p> <h3 style="margin-left:0px; margin-right:0px; text-align:start"><strong>7.3。Security Context</strong></h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">最后,我们需要为我们的示例API定义一个安全上下文:</span></span></span></p> <pre> <code class="language-java">private SecurityContext securityContext() { return SecurityContext.builder() .securityReferences(Arrays.asList(new SecurityReference("spring_oauth", scopes()))) .forPaths(PathSelectors.regex("/foos.*")) .build(); }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">请注意,我们在此处使用的名称(参考 -  <em>spring_oauth)</em>与我们之前使用的名称在<em>SecurityScheme中</em>同步。</span></span></span></p> <h3 style="margin-left:0px; margin-right:0px; text-align:start"><strong>7.4。测试</strong></h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">好吧,现在我们已经准备好了所有的东西,让我们来看看我们的Swagger UI并尝试访问Foo API:</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">我们可以在本地访问Swagger UI:</span></span></span></p> <div style="text-align:start"> <div style="margin-left:0px; margin-right:0px"> <table cellspacing="0" class="table table-bordered table-hover" style="background:0px 0px !important; border-collapse:collapse; border-radius:0px !important; border-spacing:0px; border:0px !important; bottom:auto !important; box-sizing:content-box !important; float:none !important; font-family:"source code pro",consolas,"bitstream vera sans mono","courier new",Courier,monospace !important; font-size:14px !important; font-style:normal !important; font-weight:300 !important; height:auto !important; left:auto !important; line-height:1.43 !important; margin:0px !important; min-height:auto !important; outline:0px !important; overflow:visible !important; padding:0px !important; position:static !important; right:auto !important; text-align:left !important; top:auto !important; vertical-align:baseline !important; width:871px"> <tbody> <tr> <td style="width:auto !important"> <div style="text-align:left !important"><span style="color:#333333"><span style="font-family:raleway"><span style="background-color:#ffffff"><span style="background-color:#ffffff !important"><span style="font-family:"source code pro",consolas,"bitstream vera sans mono","courier new",Courier,monospace !important"><span style="font-family:"source code pro",consolas,"bitstream vera sans mono","courier new",Courier,monospace !important"><span style="color:#afafaf !important">1</span></span></span></span></span></span></span></div> </td> <td style="width:838px"> <div style="text-align:left !important"> <div style="text-align:left !important"><span style="color:#333333"><span style="font-family:raleway"><span style="background-color:#ffffff"><span style="background-color:#ffffff !important"><span style="font-family:"source code pro",consolas,"bitstream vera sans mono","courier new",Courier,monospace !important"><span style="font-family:"source code pro",consolas,"bitstream vera sans mono","courier new",Courier,monospace !important"><span style="font-family:"source code pro",consolas,"bitstream vera sans mono","courier new",Courier,monospace !important"><code>http:</code><code>//localhost</code><code>:8082</code><code>/spring-security-oauth-resource/swagger-ui</code><code>.html</code></span></span></span></span></span></span></span></div> </div> </td> </tr> </tbody> </table> </div> </div> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">由于我们的安全配置,我们可以看到现在存在一个新的“授权”按钮:</span></span></span></p> <img alt="swagger ui security" class="img-thumbnail" src="/resources/assist/images/blog/55027912a7f74e8f9c167b2fced6d4d8.png" /><br /> <br /> 当我们点击“授权”按钮时,我们可以看到以下弹出窗口 - 授权我们的Swagger UI访问安全的API:<br /> <img alt="swagger ui security 2" class="img-thumbnail" src="/resources/assist/images/blog/30d144368b694459bfcb3f386633e1fa.png" /> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">注意:</span></span></span></p> <ul style="list-style-type:disc"> <li>我们已经可以看到CLIENT_ID和CLIENT_SECRET了 - 因为我们已经预先配置了它们(但我们仍然可以更改它们)</li> <li>我们现在可以选择我们需要的范围</li> </ul> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">以下是安全API的标记方式:<br /> <img alt="swagger ui security" class="img-thumbnail" src="/resources/assist/images/blog/b1e5e9e7b818409486ad8b3e78b98583.png" /></span></span></span><br />  </p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">现在,最后,我们可以打我们的API!</span></span></span><br /> <span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">当然,不言而喻,我们需要小心如何在外部公开Swagger UI,因为此安全配置处于活动状态。</span></span></span></p> <h2 style="margin-left:0px; margin-right:0px; text-align:start"><strong>8.总结</strong></h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:#535353"><span style="font-family:raleway"><span style="background-color:#ffffff">在本教程中,我们设置了Swagger 2来为Spring REST API生成文档。我们还探索了可视化和自定义Swagger输出的方法。最后,我们查看了Swagger的一个简单的OAuth配置。</span></span></span></p>
  • RESTful API 设计指南

    RESTful API 设计指南<h2>前言</h2> <p>    网络应用程序,分为前端和后端两个部分。当前的发展趋势,就是前端设备层出不穷(手机、平板、桌面电脑、其他专用设备......)。</p> <p>    因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信。这导致API构架的流行,甚至出现"API First"的设计思想。RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。我以前写过一篇《理解RESTful架构》,探讨如何理解这个概念。</p> <p>    今天,我将介绍RESTful API的设计细节,探讨如何设计一套合理、好用的API。</p> <h2>一、协议</h2> API与用户的通信协议,总是使用HTTP(s)协议。<br />   <h2>二、域名</h2> 应该尽量将API部署在专用域名之下。 <pre> <code class="language-html">https://api.leftso.com</code></pre> 如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。 <pre> <code class="language-html">https://leftso.com/api/</code></pre> <h2>三、版本(Versioning)</h2> 应该将API的版本号放入URL。 <pre> <code class="language-html">https://api.leftso.com/v1/</code></pre> 另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。\ <h2>四、路径(Endpoint)</h2> <p>路径又称"终点"(endpoint),表示API的具体网址。</p> <p>在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。</p> <p>举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。<br />  </p> <pre> <code class="language-html">https://api.example.com/v1/zoos https://api.example.com/v1/animals https://api.example.com/v1/employees</code></pre> <h2>五、HTTP动词</h2> <p>对于资源的具体操作类型,由HTTP动词表示。</p> <p>常用的HTTP动词有下面五个(括号里是对应的SQL命令)。</p> <pre> <code class="language-html">GET(SELECT):从服务器取出资源(一项或多项)。 POST(CREATE):在服务器新建一个资源。 PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。 PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。 DELETE(DELETE):从服务器删除资源。</code></pre> <br /> 还有两个不常用的HTTP动词。 <pre> <code class="language-html">HEAD:获取资源的元数据。 OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。</code></pre> <br /> 下面是一些例子。 <pre> <code class="language-html">GET /zoos:列出所有动物园 POST /zoos:新建一个动物园 GET /zoos/ID:获取某个指定动物园的信息 PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息) PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息) DELETE /zoos/ID:删除某个动物园 GET /zoos/ID/animals:列出某个指定动物园的所有动物 DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物</code></pre> <h2>六、过滤信息(Filtering)</h2> <p>如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。</p> <p>下面是一些常见的参数。<br />  </p> <pre> <code class="language-html">?limit=10:指定返回记录的数量 ?offset=10:指定返回记录的开始位置。 ?page=2&per_page=100:指定第几页,以及每页的记录数。 ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。 ?animal_type_id=1:指定筛选条件</code></pre> 参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。<br />   <h2>七、状态码(Status Codes)</h2> 服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。 <pre> <code class="language-html">200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务) 204 NO CONTENT - [DELETE]:用户删除数据成功。 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。</code></pre> <h2>八、错误处理(Error handling)</h2> 如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。 <pre> <code class="language-json">{ error: "Invalid API key" }</code></pre> <h2>九、返回结果</h2> 针对不同操作,服务器向用户返回的结果应该符合以下规范。 <pre> <code class="language-html">GET /collection:返回资源对象的列表(数组) GET /collection/resource:返回单个资源对象 POST /collection:返回新生成的资源对象 PUT /collection/resource:返回完整的资源对象 PATCH /collection/resource:返回完整的资源对象 DELETE /collection/resource:返回一个空文档</code></pre>   <h2>十、Hypermedia API</h2> <p>RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。</p> <p>比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。<br />  </p> <pre> <code class="language-json">{"link": { "rel": "collection https://www.example.com/zoos", "href": "https://api.example.com/zoos", "title": "List of zoos", "type": "application/vnd.yourformat+json" }}</code></pre> <p>上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。</p> <p>Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。<br />  </p> <pre> <code class="language-json">{ "current_user_url": "https://api.github.com/user", "authorizations_url": "https://api.github.com/authorizations", // ... }</code></pre> 从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果。 <pre> <code class="language-json">{ "message": "Requires authentication", "documentation_url": "https://developer.github.com/v3" }</code></pre> 上面代码表示,服务器给出了提示信息,以及文档的网址。 <h2>十一、其他</h2> <ul> <li>API的身份认证应该使用OAuth 2.0框架。</li> <li>服务器返回的数据格式,应该尽量使用JSON,避免使用XML。</li> </ul>
  • Spring 5 WebClient和WebTestClient使用教程

    Spring 5 WebClient和WebTestClient使用教程,Spring开发人员,您是否曾经觉得需要一个易于使用且高效的流畅功能样式 API 的异步/非阻塞 HTTP客户端? 如果是,那么我欢迎您阅读关于WebClient的文章,WebClient是Spring 5中引入的新的被动HTTP客户端。<h2 style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">1.引言</span></span></span></h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">Spring开发人员,您是否曾经觉得需要一个易于使用且高效的<strong>流畅功能样式</strong> API 的<strong>异步/非阻塞</strong> HTTP客户端?</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">如果是,那么我欢迎您阅读关于WebClient的文章,WebClient是Spring 5中引入的新的被动HTTP客户端。</span></span></span><br /> <br />  </p> <h2 style="margin-left:0px; margin-right:0px; text-align:start">2.如何使用WebClient</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">WebClient是Spring 5的反应性Web框架Spring WebFlux的一部分。要使用WebClient,您需要将<code>spring-webflux</code>模块包含在您的项目中。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff"><strong>在现有的Spring Boot项目中添加依赖项</strong></span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">如果您有一个现有的Spring Boot项目,则可以<code>spring-webflux</code>通过在该<code>pom.xml</code>文件中添加以下依赖项来添加该模块-</span></span></span></p> <pre> <code class="language-xml"><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency></code></pre> <blockquote> <p style="margin-left:0px; margin-right:0px; text-align:start">请注意,您需要Spring Boot 2.xx版本才能使用Spring WebFlux模块。</p> </blockquote> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff"><strong>从Scratch创建一个新项目</strong></span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">如果您从头开始创建项目,那么您可以使用<a href="http://start.spring.io/" rel="external nofollow" style="box-sizing:border-box; color:#419be8; text-decoration:none; word-wrap:break-word" target="_blank" >Spring Initializr</a>网站的<code>spring-webflux</code>模块生成初始项目-</span></span></span></p> <ol> <li>转到<a href="http://start.spring.io/" rel="external nofollow" style="box-sizing:border-box; color:#419be8; text-decoration:none; word-wrap:break-word" target="_blank" >http://start.spring.io</a>。</li> <li>选择弹簧引导版本<strong>2.xx的</strong>。</li> <li>在依赖项部分添加<strong>反应性Web</strong>依赖项。</li> <li>如果需要,请更改<em>组</em>和<em>工件的</em>详细信息,然后单击生成工程下载项目。</li> </ol> <h2 style="margin-left:0px; margin-right:0px; text-align:start">3.使用WebClient消费远程API</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff"><em>让我们做一些有趣的事情,并使用WebClient来使用Real World API。</em></span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">在本文中,我们将使用WebClient来使用<a href="https://developer.github.com/v3/" rel="external nofollow" style="box-sizing:border-box; color:#419be8; text-decoration:none; word-wrap:break-word" target="_blank" >Github的API</a>。我们将使用WebClient在用户的Github存储库上执行CRUD操作。</span></span></span></p> <h3 style="margin-left:0px; margin-right:0px; text-align:start">创建WebClient的一个实例</h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff"><strong>1.使用该<code>create()</code>方法创建WebClient</strong></span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">您可以使用<code>create()</code>工厂方法创建WebClient的实例-</span></span></span></p> <pre> <code class="language-java">WebClient webClient = WebClient.create();</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start">如果您只使用特定服务的API,那么您可以使用该服务的baseUrl来初始化WebClient</p> <pre> <code class="language-java">WebClient webClient = WebClient.create("https://api.github.com");</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff"><strong>2.使用WebClient构建器创建WebClient</strong></span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">WebClient还附带了一个构建器,它为您提供了一些自定义选项,包括过滤器,默认标题,cookie,客户端连接器等 -</span></span></span></p> <pre> <code class="language-java">WebClient webClient = WebClient.builder() .baseUrl("https://api.github.com") .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json") .defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient") .build();</code></pre> <h3 style="margin-left:0px; margin-right:0px; text-align:start">使用WebClient发出请求并检索响应</h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">以下是如何使用WebClient <code>GET</code>向<a href="https://developer.github.com/v3/repos/#list-your-repositories" rel="external nofollow" style="box-sizing:border-box; color:#419be8; text-decoration:none; word-wrap:break-word" target="_blank" >Github的List Repositories API</a>发出请求-</span></span></span></p> <pre> <code class="language-java">public Flux<GithubRepo> listGithubRepositories(String username, String token) { return webClient.get() .uri("/user/repos") .header("Authorization", "Basic " + Base64Utils .encodeToString((username + ":" + token).getBytes(UTF_8))) .retrieve() .bodyToFlux(GithubRepo.class); }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">了解API调用的简单性和简洁性!</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">假设我们有一个名为类<code>GithubRepo</code>,确认到GitHub的API响应,上面的函数会返回一个<code>Flux</code>的<code>GithubRepo</code>对象。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">请注意,我使用<a href="https://developer.github.com/v3/auth/#via-oauth-tokens" rel="external nofollow" style="box-sizing:border-box; color:#419be8; text-decoration:none; word-wrap:break-word" target="_blank" >Github的基本认证</a>机制来调用API。它需要您的github用户名和个人访问令牌,您可以从<a href="https://github.com/settings/tokens" rel="external nofollow" style="box-sizing:border-box; color:#419be8; text-decoration:none; word-wrap:break-word" target="_blank" >https://github.com/settings/tokens中</a>生成该令牌。</span></span></span></p> <h4 style="margin-left:0px; margin-right:0px; text-align:start">使用exchange()方法来检索响应</h4> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">该<code>retrieve()</code>方法是获取响应主体的最简单方法。但是,如果您希望对响应拥有更多的控制权,那么您可以使用可<code>exchange()</code>访问整个<code>ClientResponse</code>标题和正文的方法 -</span></span></span></p> <pre> <code class="language-java">public Flux<GithubRepo> listGithubRepositories(String username, String token) { return webClient.get() .uri("/user/repos") .header("Authorization", "Basic " + Base64Utils .encodeToString((username + ":" + token).getBytes(UTF_8))) .exchange() .flatMapMany(clientResponse -> clientResponse.bodyToFlux(GithubRepo.class)); }</code></pre> <h4 style="margin-left:0px; margin-right:0px; text-align:start">在请求URI中使用参数</h4> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">您可以在请求URI中使用参数,并在<code>uri()</code>函数中分别传递它们的值。所有参数都被花括号包围。在提出请求之前,这些参数将被WebClient自动替换 -</span></span></span></p> <pre> <code class="language-java">public Flux<GithubRepo> listGithubRepositories(String username, String token) { return webClient.get() .uri("/user/repos?sort={sortField}&direction={sortDirection}", "updated", "desc") .header("Authorization", "Basic " + Base64Utils .encodeToString((username + ":" + token).getBytes(UTF_8))) .retrieve() .bodyToFlux(GithubRepo.class); }</code></pre> <h4 style="margin-left:0px; margin-right:0px; text-align:start">使用URIBuilder构造请求URI</h4> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">您也可以使用<code>UriBuilder</code>类似的方法获取对请求URI的完全程序控制,</span></span></span></p> <pre> <code class="language-java">public Flux<GithubRepo> listGithubRepositories(String username, String token) { return webClient.get() .uri(uriBuilder -> uriBuilder.path("/user/repos") .queryParam("sort", "updated") .queryParam("direction", "desc") .build()) .header("Authorization", "Basic " + Base64Utils .encodeToString((username + ":" + token).getBytes(UTF_8))) .retrieve() .bodyToFlux(GithubRepo.class); }</code></pre> <h3 style="margin-left:0px; margin-right:0px; text-align:start">在WebClient请求中传递Request Body</h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">如果你有一个<code>Mono</code>或一个形式的请求体<code>Flux</code>,那么你可以直接将它传递给<code>body()</code>WebClient中的方法,否则你可以从一个对象中创建一个单声道/通量并像这样传递 -</span></span></span></p> <pre> <code class="language-java">public Mono<GithubRepo> createGithubRepository(String username, String token, RepoRequest createRepoRequest) { return webClient.post() .uri("/user/repos") .body(Mono.just(createRepoRequest), RepoRequest.class) .header("Authorization", "Basic " + Base64Utils .encodeToString((username + ":" + token).getBytes(UTF_8))) .retrieve() .bodyToMono(GithubRepo.class); }</code></pre> 如果您具有实际值而不是<code>Publisher</code>(<code>Flux</code>/ <code>Mono</code>),则可以使用<code>syncBody()</code>快捷方式传递请求正文 - <pre> <code class="language-java">public Mono<GithubRepo> createGithubRepository(String username, String token, RepoRequest createRepoRequest) { return webClient.post() .uri("/user/repos") .syncBody(createRepoRequest) .header("Authorization", "Basic " + Base64Utils .encodeToString((username + ":" + token).getBytes(UTF_8))) .retrieve() .bodyToMono(GithubRepo.class); }</code></pre> 最后,你可以使用<code>BodyInserters</code>类提供的各种工厂方法来构造一个<code>BodyInserter</code>对象并将其传递给该<code>body()</code>方法。本<code>BodyInserters</code>类包含的方法来创建一个<code>BodyInserter</code>从<code>Object</code>,<code>Publisher</code>,<code>Resource</code>,<code>FormData</code>,<code>MultipartData</code>等- <pre> <code class="language-java">public Mono<GithubRepo> createGithubRepository(String username, String token, RepoRequest createRepoRequest) { return webClient.post() .uri("/user/repos") .body(BodyInserters.fromObject(createRepoRequest)) .header("Authorization", "Basic " + Base64Utils .encodeToString((username + ":" + token).getBytes(UTF_8))) .retrieve() .bodyToMono(GithubRepo.class); }</code></pre> <h3 style="margin-left:0px; margin-right:0px; text-align:start">添加过滤器功能</h3> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">WebClient支持使用<code>ExchangeFilterFunction</code>。您可以使用过滤器函数以任何方式拦截和修改请求。例如,您可以使用过滤器函数为<code>Authorization</code>每个请求添加一个标头,或记录每个请求的详细信息。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">这<code>ExchangeFilterFunction</code>需要两个参数 -</span></span></span></p> <ol style="margin-left:30px; margin-right:0px"> <li>在<code>ClientRequest</code>与</li> <li><code>ExchangeFilterFunction</code>过滤器链中的下一个。</li> </ol> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">它可以修改<code>ClientRequest</code>并调用<code>ExchangeFilterFucntion</code>过滤器链中的下一个来继续下一个过滤器或<code>ClientRequest</code>直接返回修改以阻止过滤器链。</span></span></span></p> <h4 style="margin-left:0px; margin-right:0px; text-align:start">1.使用过滤器功能添加基本认证</h4> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">在上面的所有示例中,我们都包含一个<code>Authorization</code>用于使用Github API进行基本身份验证的标头。由于这是所有请求共有的内容,因此您可以在创建过滤器函数时将此逻辑添加到过滤器函数中<code>WebClient</code>。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">该<code>ExchaneFilterFunctions</code>API已经为基本认证提供了一个过滤器。你可以像这样使用它 -</span></span></span></p> <pre> <code class="language-java">WebClient webClient = WebClient.builder() .baseUrl(GITHUB_API_BASE_URL) .defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE) .filter(ExchangeFilterFunctions .basicAuthentication(username, token)) .build();</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">现在,您不需要<code>Authorization</code>在每个请求中添加标题。过滤器函数将拦截每个WebClient请求添加此标头。</span></span></span></p> <h4 style="margin-left:0px; margin-right:0px; text-align:start">2.使用过滤器功能记录所有请求</h4> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">我们来看一个习惯的例子<code>ExchangeFilterFunction</code>。我们将编写一个过滤器函数来拦截并记录每个请求 -</span></span></span></p> <pre> <code class="language-java">WebClient webClient = WebClient.builder() .baseUrl(GITHUB_API_BASE_URL) .defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE) .filter(ExchangeFilterFunctions .basicAuthentication(username, token)) .filter(logRequest()) .build();</code></pre> 这里是<code>logRequest()</code>过滤器功能的实现- <pre> <code class="language-java">private ExchangeFilterFunction logRequest() { return (clientRequest, next) -> { logger.info("Request: {} {}", clientRequest.method(), clientRequest.url()); clientRequest.headers() .forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value))); return next.exchange(clientRequest); }; }</code></pre> <h4 style="margin-left:0px; margin-right:0px; text-align:start">3.使用ofRequestProcessor()和ofResponseProcessor()工厂方法来创建过滤器</h4> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">ExchangeFilterFunction API提供两个名为工厂方法<code>ofRequestProcessor()</code>和<code>ofResponseProcessor()</code>用于创建分别截获该请求和响应滤波器的功能。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff"><code>logRequest()</code>我们在前一节中创建的过滤器函数可以使用<code>ofRequestProcessor()</code>这种工厂方法创建-</span></span></span></p> <pre> <code class="language-java">private ExchangeFilterFunction logRequest() { ExchangeFilterFunction.ofRequestProcessor(clientRequest -> { logger.info("Request: {} {}", clientRequest.method(), clientRequest.url()); clientRequest.headers() .forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value))); return Mono.just(clientRequest); }); } </code></pre> 如果您想拦截WebClient响应,则可以使用该<code>ofResponseProcessor()</code>方法创建像这样的过滤器功能 - <pre> <code class="language-java">private ExchangeFilterFunction logResposneStatus() { return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { logger.info("Response Status {}", clientResponse.statusCode()); return Mono.just(clientResponse); }); }</code></pre> <h2 style="margin-left:0px; margin-right:0px; text-align:start">处理WebClient错误</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">只要接收到状态码为4xx或5xx的响应<code>retrieve()</code>,WebClient中的方法<code>WebClientResponseException</code>就会抛出一个。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">您可以使用<code>onStatus()</code>像这样的方法来自定义,</span></span></span><br />  </p> <pre> <code class="language-java">public Flux<GithubRepo> listGithubRepositories() { return webClient.get() .uri("/user/repos?sort={sortField}&direction={sortDirection}", "updated", "desc") .retrieve() .onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.error(new MyCustomClientException()) ) .onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.error(new MyCustomServerException()) ) .bodyToFlux(GithubRepo.class); }</code></pre> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">请注意,与<code>retrieve()</code>方法不同,该<code>exchange()</code>方法在4xx或5xx响应的情况下不会引发异常。您需要自己检查状态代码,并以您想要的方式处理它们。</span></span></span></p> <h4 style="margin-left:0px; margin-right:0px; text-align:start">使用<code>@ExceptionHandler</code>控制器内部的WebClientResponseExceptions处理</h4> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">您可以<code>@ExceptionHandler</code>在控制器内部使用这种方式来处理<code>WebClientResponseException</code>并返回适当的响应给客户端 -</span></span></span></p> <pre> <code class="language-java">@ExceptionHandler(WebClientResponseException.class) public ResponseEntity<String> handleWebClientResponseException(WebClientResponseException ex) { logger.error("Error from WebClient - Status {}, Body {}", ex.getRawStatusCode(), ex.getResponseBodyAsString(), ex); return ResponseEntity.status(ex.getRawStatusCode()).body(ex.getResponseBodyAsString()); }</code></pre> <h2 style="margin-left:0px; margin-right:0px; text-align:start">使用Spring 5 WebTestClient测试Rest API</h2> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">WebTestClient包含类似于WebClient的请求方法。另外,它还包含检查响应状态,标题和正文的方法。您也可以像<code>AssertJ</code>使用WebTestClient 一样使用断言库。</span></span></span></p> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="color:rgba(0, 0, 0, 0.87)"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif"><span style="background-color:#ffffff">查看以下示例以了解如何使用WebTestClient执行其他API测试 -</span></span></span></p> <blockquote> <p style="margin-left:0px; margin-right:0px; text-align:start"><span style="font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif">提示:<a href="http://www.leftso.com/resource/1011.html" target="_blank" >项目源码下载</a></span></p> </blockquote>