springboot2.0 结合Spring Security oauth2 (一)

     之前学过oauth2,后来好久没用,又忘得差不多了,而且现在用到了springboot ,于是就在网上查些资料,balabala半天好多都是springboot1.0版本的,按着撸了遍代码后发现有问题,只好自己重新走个spingboot2.0的代码。

     Springboot1.0更新至Spring Boot 2.0.5.RELEASE版本时会出现一些小问题。

先复习下oauth2.0,之前印象就剩下

  • 权服务 Authorization Service.
  • 资源服务 Resource Service.

以及

  • authorization_code:授权码类型。
  • implicit:隐式授权类型。
  • password:资源所有者(即用户)密码类型。
  • client_credentials:客户端凭据(客户端ID以及Key)类型。

还有 一堆重写的configure方法。再次重新加深下印象。

OAUTH2.0

官方原文:http://projects.spring.io/spring-security-oauth/docs/oauth2.html

几种授权类型的列表,具体授权机制的含义可以参见RFC6749(中文版本)。

如果刚开始没有印象,读着读着就会晕乎,主要是还需要的想象力,把它与日常应用场景结合起来,最容易让人想到的是QQ或是微信的授权登录,接触过第三方登录的很容易明白,主要是用了authorization_code授权码类型。

还有一个就是小区的单元楼和电梯也是很形象例子,物业有“通卡”可以打开所有的大门和电梯楼层,户主只能打开他所属单元的楼层和单元门。

如果户主用的是物业的通卡,然后把自己的密码,告诉租客,他就拥有了与物业同样的权限,这样好像不太合适。万一我想取消他进入小区的权力,也很麻烦,我自己的密码也得跟着改了,还得通知其他的人。

有没有一种办法,让住客能够自由进入小区,又不必知道小区其他居民的密码,而且他的唯一权限就是这间房子,其他需要密码的场合,他都没有权限?

于是,我就有了这样一套设计授权机制。每次物业费时候激活门禁卡

第一步,新户主申请户主门禁卡,叫做"获取授权"。户主需要首先按这个按钮,去申请授权。

第二步,他按下按钮以后,物业(也就是我)的系统:有人正在要求授权。系统还会显示该户主的姓名、单元号,联系电话。

我确认请求属实,就点击按钮,告诉门禁系统,我同意给予他进入小区的授权。

第三步,同时发一个门禁卡access token,令牌就是类似密码的一串数字。

第四步,户主向门禁系统输入令牌,进入小区。

当户主搬离此处时候门禁就失效。

oauth2实际逻辑:

Spring OAuth2.0提供者实际上分为:

  • 授权服务 Authorization Service.
  • 资源服务 Resource Service.

虽然这两个提供者有时候可能存在同一个应用程序中,但在Spring Security OAuth中你可以把

他它们各自放在不同的应用上,而且你可以有多个资源服务,它们共享同一个中央授权服

务。

下面是配置一个授权服务必须要实现的endpoints:

  • AuthorizationEndpoint:用来作为请求者获得授权的服务,默认的URL是/oauth/authorize.
  • TokenEndpoint:用来作为请求者获得令牌(Token)的服务,默认的URL是/oauth/token.

下面是配置一个资源服务必须要实现的过滤器:

  • configure(HttpSecurity http):用来作为认证令牌(Token)的一个处理流程过滤器。只有当过滤器通过之后,请求者才能获得受保护的资源。

 

配置一个授权服务,你需要考虑几种授权类型(Grant Type),不同的授权类型为客户端(Client)提供了不同的获取令牌(Token)方式,为了实现并确定这几种授权,需要配置使用 ClientDetailsService 和 TokenService 来开启或者禁用这几种授权机制。到这里就请注意了,不管你使用什么样的授权类型(Grant Type),每一个客户端(Client)都能够通过明确的配置以及权限来实现不同的授权访问机制。这也就是说,假如你提供了一个支持"client_credentials"的授权方式,并不意味着客户端就需要使用这种方式来获得授权。下面是几种授权类型的列表,具体授权机制的含义可以参见RFC6749(中文版本):

  • authorization_code:授权码类型。
  • implicit:隐式授权类型。
  • password:资源所有者(即用户)密码类型。
  • client_credentials:客户端凭据(客户端ID以及Key)类型。
  • refresh_token:通过以上授权获得的刷新令牌来获取新的令牌。

 

可以用 @EnableAuthorizationServer 注解来配置OAuth2.0 授权服务机制,通过使用@Bean注解的几个方法一起来配置这个授权服务。下面咱们介绍几个配置类,这几个配置是由Spring创建的独立的配置对象,它们会被Spring传入AuthorizationServerConfigurer中:

  • ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
  • AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束.
  • AuthorizationServerEndpointsConfigurer:用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)。

(译者注:以上的配置可以选择继承AuthorizationServerConfigurerAdapter并且覆写其中的三个configure方法来进行配置。)

 

配置授权服务一个比较重要的方面就是提供一个授权码给一个OAuth客户端(通过 authorization_code 授权类型),一个授权码的获取是OAuth客户端跳转到一个授权页面,然后通过验证授权之后服务器重定向到OAuth客户端,并且在重定向连接中附带返回一个授权码。

配置客户端详情信息(Client Details):

ClientDetailsServiceConfigurer (AuthorizationServerConfigurer 的一个回调配置项,见上的概述) 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService),有几个重要的属性如下列表:

  • clientId:(必须的)用来标识客户的Id。
  • secret:(需要值得信任的客户端)客户端安全码,如果有的话。
  • scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
  • authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
  • authorities:此客户端可以使用的权限(基于Spring Security authorities)。

 

客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户端详情存储在一个关系数据库的表中,就可以使用 JdbcClientDetailsService)或者通过 ClientDetailsManager 接口(同时你也可以实现 ClientDetailsService 接口)来进行管理。

管理令牌(Managing Token):

AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,在使用这些操作的时候请注意以下几点:

  • 当一个令牌被创建了,你必须对其进行保存,这样当一个客户端使用这个令牌对资源服务进行请求的时候才能够引用这个令牌。
  • 当一个令牌是有效的时候,它可以被用来加载身份信息,里面包含了这个令牌的相关权限。

 

当你自己创建 AuthorizationServerTokenServices 这个接口的实现时,你可能需要考虑一下使用 DefaultTokenServices 这个类,里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了所有的事情。并且 TokenStore 这个接口有一个默认的实现,它就是 InMemoryTokenStore ,如其命名,所有的令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都实现了TokenStore接口:

  • InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。
  • JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时,你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的classpath当中。
  • JwtTokenStore:这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。

配置授权类型(Grant Types):

授权是使用 AuthorizationEndpoint 这个端点来进行控制的,你能够使用 AuthorizationServerEndpointsConfigurer 这个对象的实例来进行配置(AuthorizationServerConfigurer 的一个回调配置项,见上的概述) ,如果你不进行设置的话,默认是除了资源所有者密码(password)授权类型以外,支持其余所有标准授权类型的(RFC6749),我们来看一下这个配置对象有哪些属性可以设置吧,如下列表:

  • authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个 AuthenticationManager 对象。
  • userDetailsService:如果啊,你设置了这个属性的话,那说明你有一个自己的 UserDetailsService 接口的实现,或者你可以把这个东西设置到全局域上面去(例如 GlobalAuthenticationManagerConfigurer 这个配置对象),当你设置了这个之后,那么 "refresh_token" 即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。
  • authorizationCodeServices:这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对象),主要用于 "authorization_code" 授权码类型模式。
  • implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。
  • tokenGranter:这个属性就很牛B了,当你设置了这个东西(即 TokenGranter 接口实现),那么授权将会交由你来完全掌控,并且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的需求的时候,才会考虑使用这个。

配置授权端点的URL(Endpoint URLs):

AuthorizationServerEndpointsConfigurer 这个配置对象(AuthorizationServerConfigurer 的一个回调配置项,见上的概述) 有一个叫做 pathMapping() 的方法用来配置端点URL链接,它有两个参数:

  • 第一个参数:String 类型的,这个端点URL的默认链接。
  • 第二个参数:String 类型的,你要进行替代的URL链接。

以上的参数都将以 "/" 字符为开始的字符串,框架的默认URL链接如下列表,可以作为这个 pathMapping() 方法的第一个参数:

  • /oauth/authorize:授权端点。
  • /oauth/token:令牌端点。
  • /oauth/confirm_access:用户确认授权提交端点。
  • /oauth/error:授权服务错误信息端点。
  • /oauth/check_token:用于资源服务访问的令牌解析端点。
  • /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。

 

需要注意的是授权端点这个URL应该被Spring Security保护起来只供授权用户访问,我们来看看在标准的Spring Security中 WebSecurityConfigurer 是怎么用的。

@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
}

 

注意:如果你的应用程序中既包含授权服务又包含资源服务的话,那么这里实际上是另一个的低优先级的过滤器来控制资源接口的,这些接口是被保护在了一个访问令牌(access token)中,所以请挑选一个URL链接来确保你的资源接口中有一个不需要被保护的链接用来取得授权,就如上面示例中的 /login 链接,你需要在 WebSecurityConfigurer 配置对象中进行设置。

 

令牌端点默认也是受保护的,不过这里使用的是基于 HTTP Basic Authentication 标准的验证方式来验证客户端的,这在XML配置中是无法进行设置的(所以它应该被明确的保护)。

 

一个资源服务(可以和授权服务在同一个应用中,当然也可以分离开成为两个不同的应用程序)提供一些受token令牌保护的资源,Spring OAuth提供者是通过Spring Security authentication filter 即验证过滤器来实现的保护,你可以通过 @EnableResourceServer 注解到一个 @Configuration 配置类上,并且必须使用 ResourceServerConfigurer 这个配置对象来进行配置(可以选择继承自 ResourceServerConfigurerAdapter 然后覆写其中的方法,参数就是这个对象的实例),下面是一些可以配置的属性:

  • tokenServices:ResourceServerTokenServices 类的实例,用来实现令牌服务。
  • resourceId:这个资源服务的ID,这个属性是可选的,但是推荐设置并在授权服务中进行验证。
  • 其他的拓展属性例如 tokenExtractor 令牌提取器用来提取请求中的令牌。
  • 请求匹配器,用来设置需要进行保护的资源路径,默认的情况下是受保护资源服务的全部路径。
  • 受保护资源的访问规则,默认的规则是简单的身份验证(plain authenticated)。
  • 其他的自定义权限保护规则通过 HttpSecurity 来进行配置。

 

@EnableResourceServer 注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter 的过滤器链.

ResourceServerTokenServices 是组成授权服务的另一半,如果你的授权服务和资源服务在同一个应用程序上的话,你可以使用 DefaultTokenServices ,这样的话,你就不用考虑关于实现所有必要的接口的一致性问题,这通常是很困难的。如果你的资源服务器是分离开的,那么你就必须要确保能够有匹配授权服务提供的 ResourceServerTokenServices,它知道如何对令牌进行解码。

在授权服务器上,你通常可以使用 DefaultTokenServices 并且选择一些主要的表达式通过 TokenStore(后端存储或者本地编码)。

RemoteTokenServices 可以作为一个替代,它将允许资源服务器通过HTTP请求来解码令牌(也就是授权服务的 /oauth/check_token 端点)。如果你的资源服务没有太大的访问量的话,那么使用RemoteTokenServices 将会很方便(所有受保护的资源请求都将请求一次授权服务用以检验token值),或者你可以通过缓存来保存每一个token验证的结果。

使用授权服务的 /oauth/check_token 端点你需要将这个端点暴露出去,以便资源服务可以进行访问,这在咱们授权服务配置中已经提到了,下面是一个例子:

@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
    oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')")
        .checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
}

在这个例子中,我们配置了 /oauth/check_token 和 /oauth/token_key 这两个端点(受信任的资源服务能够获取到公有密匙,这是为了验证JWT令牌)。这两个端点使用了HTTP Basic Authentication 即HTTP基本身份验证,使用 client_credentials 授权模式可以做到这一点。

 

 

配置OAuth-Aware表达式处理器(OAuth-Aware Expression Handler):

你也许希望使用 Spring Security's expression-based access control 来获得一些优势,一个表达式处理器会被注册到默认的 @EnableResourceServer 配置中,这个表达式包含了 #oauth2.clientHasRole,#oauth2.clientHasAnyRole 以及 #oauth2.denyClient 所提供的方法来帮助你使用权限角色相关的功能(在 OAuth2SecurityExpressionMethods 中有完整的列表)。

目前先写个简单例子,其他的以后再研究。

pom

<!-- 注意是starter,自动配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- 不是starter,手动配置 -->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 将token存储在redis中 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

ResourceServerConfiguration

@Configuration
    @EnableResourceServer

public class ResourceServerConfiguration extends  ResourceServerConfigurerAdapter{

@Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests().antMatchers("/order/**").authenticated();// 配置order访问控制,必须认证过后才可以访问

        }

        @Override
        public void configure(ResourceServerSecurityConfigurer resources)
                throws Exception {
            resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
        }

}

AuthorizationServerConfiguration

@Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfiguration extends
            AuthorizationServerConfigurerAdapter {
        @Autowired
        AuthenticationManager authenticationManager;
        @Autowired
        RedisConnectionFactory redisConnectionFactory;

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints)
                throws Exception {
            endpoints
            .tokenStore(new RedisTokenStore(redisConnectionFactory))
            .authenticationManager(authenticationManager)
            .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
        }

        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer)
                throws Exception {
            //允许表单认证
            oauthServer.allowFormAuthenticationForClients();
        }

        @Override
        public void configure(ClientDetailsServiceConfigurer clients)
                throws Exception {
//            password 方案一:明文存储,用于测试,不能用于生产
//            String finalSecret = "123456";
//            password 方案二:用 BCrypt 对密码编码
//            String finalSecret = new BCryptPasswordEncoder().encode("123456");
                // password 方案三:支持多种编码,通过密码的前缀区分编码方式
                String finalSecret = "{bcrypt}"+new BCryptPasswordEncoder().encode("123456");
                //配置两个客户端,一个用于password认证一个用于client认证
                clients.inMemory().withClient("client_1")
                        .resourceIds(DEMO_RESOURCE_ID)
                        .authorizedGrantTypes("client_credentials", "refresh_token")
                        .scopes("select")
                        .authorities("oauth2")
                        .secret(finalSecret)
                        .and().withClient("client_2")
                        .resourceIds(DEMO_RESOURCE_ID)
                        .authorizedGrantTypes("password", "refresh_token")
                        .scopes("select")
                        .authorities("oauth2")
                        .secret(finalSecret);
        }

    }

 

 

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatchers()
        .anyRequest()
        .and()
        .authorizeRequests()
        .antMatchers("/oauth/**").permitAll();
    }

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        BCryptPasswordEncoder bCryptPasswordEncoder= new BCryptPasswordEncoder();
//      password 方案一:明文存储,用于测试,不能用于生产
//      String finalPassword = "123456";
//      password 方案二:用 BCrypt 对密码编码
//      String finalPassword = bCryptPasswordEncoder.encode("123456");
      // password 方案三:支持多种编码,通过密码的前缀区分编码方式
        String finalPassword = "{bcrypt}"+bCryptPasswordEncoder.encode("123456");
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("user_1").password(finalPassword).authorities("USSER").build());
        manager.createUser(User.withUsername("user_2").password(finalPassword).authorities("USSER").build());
        
        return manager;
    }
    
//  /**
//  * 这一步的配置是必不可少的,否则SpringBoot会自动配置一个AuthenticationManager,覆盖掉内存中的用户
//  */
 @Bean
 @Override
 public AuthenticationManager authenticationManagerBean() throws Exception {
     AuthenticationManager manager = super.authenticationManagerBean();
     return manager;
 }
    // password 方案一:明文存储,用于测试,不能用于生产
//  @Bean
//  PasswordEncoder passwordEncoder(){
//      return NoOpPasswordEncoder.getInstance();
//  }

  // password 方案二:用 BCrypt 对密码编码
//  @Bean
//  PasswordEncoder passwordEncoder(){
//      return new BCryptPasswordEncoder();
//  }
    // password 方案三:支持多种编码,通过密码的前缀区分编码方式,推荐
    @Bean
    PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

}

 

 

Test

@RestController
public class TestEndpoints {
     @GetMapping("/product/{id}")
        public String getProduct(@PathVariable String id) {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            return "product id : " + id;
        }

        @GetMapping("/order/{id}")
        public String getOrder(@PathVariable String id) {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            return "order id : " + id;
        }
}

postman工具

password模式

http://localhost:8080/oauth/token?username=user_1&password=123456&grant_type=password&scope=select&client_id=client_2&client_secret=123456

 

 

带上access_token

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值