您的位置 首页 java

企业级单点登录实现方案OAuth2-密码模式及客户端模式

上一篇文章中介绍了 oauth 的一些背景知识;本文将编写一个简单的示例,演示授权模式中的密码模式及客户端模式如何实现。

本示例中涉及到的几个对象其关系如下图所示:

OAuth对象及其关系

密码模式一般用于用户对客户端信任度最高的情况下,因为客户端需要保存用户在授权服务器中的用户名及密码信息,客户端可以访问所有用户资源,因此一般在公司内部应用之间使用的较多。比如一个公司前端一般有安卓应用、苹果应用、Web端等,这个时候用户通过客户端使用用户名与密码登录的时候,这些信息实际上是告诉了客户端了,客户端可以拿这些信息来做任何用户权限内的事情。 在密码模式中,其处理流程如下:

  • 客户端在授权服务器端的注册;
  • 用户登录客户端,输入用户名及密码(或以某种其它方式保存用户在授权服务器中的用户名与密码);
  • 客户端访问授权服务器验证授权,并获取访问Token;
  • 客户端在后续的每次访问中都带上Token进行访问。
  • 资源服务器接收到请求后,调用授权服务器相关接口校验Token有效性;Token有效时,进行实际的业务逻辑处理;Token有效时,返回相应错误信息给客户端;

另外,资源服务器与授权服务器可以在一个应用中,也可以分开。本文主要讲述分开处理时的实现,关于集中式实现请参考Spring Security OAuth的官方示例。

1. 授权服务器

授权服务器需要完成以下事情:

  • 管理客户端及其授权信息;
  • 管理用户及其授权信息;
  • 管理Token的生成及其存储;
  • 管理Token的校验及校验Key;

通过定义继承自AuthorizationServerConfigurerAdapter的一个配置类,以及Spring Security的配置,可以完成以上处理。

先来看授权服务器的最终配置:

1.1 MVN

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.4. RELEASE </version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>  

1.2 Spring Security配置

通过Spring Security来完成用户及密码加解密等配置。

 @ Configuration 
@EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
    @ Override 
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("test").password("test").roles("USER")
                .and().passwordEncoder(passwordEncoder());
    }

    @Bean
    public static NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                    .anyRequest().authenticated()
                .and().formLogin().permitAll();
    }
}  

其中几个地方需要注意:

  • 必须要配置一个密码加密解密器,示例中使用了NoOpPasswordEncoder,但这种方式是不安全的,因此这个类已经过时。实际项目中注意修改成BCryptPasswordEncoder等实现类;
  • 需要将AuthenticationManager注入到容器中,在进行OAuth2配置时需要使用到。

1.3 Spring Security OAuth2配置

OAuth2的配置通过继承AuthorizationServerConfigurerAdapter的配置类实现。

 @Configuration
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
    @Resource
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("test")
                .resourceIds("testResource")
                .authorizedGrantTypes("password")
                .authorities("ROLE_CLIENT")
                .scopes("read", "write")
                .secret("secret")
                .redirectUris("#34;);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("hasAuthority('ROLE_CLIENT')")
                .allowFormAuthenticationForClients();
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager);
    }
}  

说明如下:

  • 客户端的注册:本文通过inMemory的方式在内存中注册客户端相关信息;实际项目中可以通过一些管理接口及界面动态实现客户端的注册;
  • 获取Token权限控制:客户端需要通过/oauth/token获取Token,此时实际上是未进行登录的,如果不配置将会报未授权错误;因此需要配置成tokenKeyAccess(“permitAll()”)
  • 校验Token权限控制:资源服务器如果需要调用授权服务器的/oauth/check_token接口校验token有效性,那么需要配置checkTokenAccess(“hasAuthority(‘ROLE_CLIENT’)”),注意到角色是 ROLE_CLIENT,可见这种情况下资源服务器也需要当成一个客户端来进行注册。
  • authenticationManager配置:需要通过endpoints.authenticationManager(authenticationManager)将Security中的authenticationManager配置到Endpoints中,否则,在Spring Security中 配置的权限控制将不会在进行OAuth2相关权限控制的校验时生效。

1.4 授权服务器启用

最后注意需要使用EnableAuthorizationServer来启动授权服务器:

 @SpringBootApplication
@EnableAuthorizationServer
public class AuthenticationServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthenticationServerApplication.class, args);
    }

}  

2. 资源服务器配置

2.1 主配置

资源服务器MVN依赖与授权服务器基本一致。而配置则通过继承自ResourceServerConfigurerAdapter的配置类来实现:

 @Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2Configurer extends ResourceServerConfigurerAdapter {
   @Bean
   public ResourceServerTokenServices tokenServices() {
       RemoteTokenServices tokenServices = new RemoteTokenServices();
       tokenServices.setClientId("test");
       tokenServices.setClientSecret("secret");
       tokenServices.setCheckTokenEndpointUrl("#34;);

       return tokenServices;
   }

   @Override
   public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
       resources.resourceId("testResource")
               .tokenServices(tokenServices());
       super.configure(resources);
   }
}  

主要完成以下配置:

  • TokenService配置:在不采用JWT的情况下,需要配置RemoteTokenServices来充当tokenServices,它主要完成Token的校验等工作。因此需要指定校验Token的授权服务器接口地址。
  • 同时,由于在授权服务器中配置了/oauth/check_token需要客户端登录后才能访问,因此也需要配置客户端编号及Secret;在校验之前先进行登录;
  • 最后通过ResourceServerSecurityConfigurer来配置需要访问的资源编号及使用的TokenServices;

2.2 启用

需要使用EnableResourceServer来启用资源服务器

 @SpringBootApplication
@EnableResourceServer
@EnableWebSecurity
public class ResourceServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ResourceServerApplication.class, args);
    }

}
  

2.3 测试接口

 @RestController
@RequestMapping("/test")
@PreAuthorize("hasRole('ROLE_USER')")
public class TestController {
    @GetMapping
    public String test() {
        return "test";
    }
}  

3. 测试

3.1 main函数测试

 ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails();
details.setId("testResource");
details.setClientId("test");
details.setClientSecret("secret");
details.setScope(Arrays.asList("read", "write"));
details.setGrantType("password");
details.setAccessTokenUri("#34;);
details.setUsername("test");
details.setPassword("test");
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(details);

AccessTokenProviderChain provider = new AccessTokenProviderChain(Collections.singletonList(new ResourceOwnerPasswordAccessTokenProvider()));
restTemplate.setAccessTokenProvider(provider);

System.out.println(restTemplate.getAccessToken());

ResponseEntity<String> responseEntity = restTemplate.getForEntity(
        URI.create("#34;), String.class);
System.out.println(responseEntity);  

说明:

  • 通过ResourceOwnerPasswordResourceDetails及OAuth2RestTemplate可以完成密码模式的OAuth2接口调用;
  • 在调用最终需要调用的接口前,通过restTemplate.getAccessToken()可以获取到AccessToken;
  • 最终调用的时候,restTemplate中会自动在报文头中带上authentication信息,不再需要手工处理。

3.2 通过curl测试

3.2.1 获取Token:

 curl -X POST -d grant_type=password -d username=test -d password=test   

或者

 curl -X POST -d id=test -d client_id=test -d client_secret=secret -d scope=read -d grant_type=password -d username=test -d password=test   

返回结果如下:

 {"access_token":"ba3b7fa9-f206-4868-abd7-24ba8e83bc1b","token_type":"bearer","expires_in":43199,"scope":"read write"}  

3.2.2 使用获取的Token访问接口:

 curl -X GET -H "Authorization:Bearerba3b7fa9-f206-4868-abd7-24ba8e83bc1b"   

返回结果如下:

 test  

4. 客户端模式

使用以下代码可以进行客户端模式访问:

 public static void main(String[] args) {
    ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
    details.setClientId("test");
    details.setClientSecret("secret");
    details.setGrantType("client_credentials");
    details.setAccessTokenUri("#34;);
    OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(details);

    AccessTokenProviderChain provider = new AccessTokenProviderChain(Collections.singletonList(new ClientCredentialsAccessTokenProvider()));
    restTemplate.setAccessTokenProvider(provider);

    ResponseEntity<String> responseEntity = restTemplate.getForEntity(
            URI.create("#34;), String.class);
    System.out.println(responseEntity.getBody());
}  

但此时,由于资源服务器test接口需要ROLE_USER角色才能访问,此时会返回以下异常:

 11:08:17.765 [main] DEBUG org.springframework.web.client.RestTemplate - Response 401 UNAUTHORIZED
Exception in thread "main" error="access_denied", error_description="Error requesting access token."  

如果将Test接口修改下:

 @RestController
@RequestMapping("/test")
@PreAuthorize("hasRole('ROLE_CLIENT')")
public class TestController {
    @GetMapping
    public String test() {
        return "test";
    }
}  

则可正常访问。

5. 总结

  • 每一个客户端需要配置以下信息:id: 客户端编号,用于区分不同客户端;secret: 客户端访问密码;resourceIds: 客户端能够访问的资源编号清单; authorizedGrantTypes:客户端支持的授权模式;authorities:客户端所拥有的角色;scopes:客户端能够对所拥有的资源进行的操作;redirectUris: 用户授权成功后跳转到客户端的对应页面;
  • 使用RemoteServices时,资源服务器与授权服务器中存在调用关系,当资源服务器接收到请求时,将会调用授权服务器的/oauth/check_token接口来验证Token有效性。
  • 可以将资源服务器也配置成一个特定的客户端,这样在访问那些需要ROLE_CLIENT的授权服务器的接口时,可以进行调用;并且这种方式也可以实现资源服务器之间的接口调用(不需要特定用户 登录时);
  • 密码模式一般用于可信客户端的环境下,如公司内部的客户端。对于外部不信任环境,使用该模式对于用户信息及数据的泄露风险巨大。

下一文中将分析JWT方式的实现。

文章来源:智云一二三科技

文章标题:企业级单点登录实现方案OAuth2-密码模式及客户端模式

文章地址:https://www.zhihuclub.com/199936.shtml

关于作者: 智云科技

热门文章

网站地图