Spring Boot2.0 Oauth2 服务器和客户端配置及原理

1、应用场景

为了理解OAuth的适用场合,让我举一个假设的例子。java

有一个"云冲印"的网站,能够将用户储存在Google的照片,冲印出来。用户为了使用该服务,必须让"云冲印"读取本身储存在Google上的照片。web

问题是只有获得用户的受权,Google才会赞成"云冲印"读取这些照片。那么,"云冲印"怎样得到用户的受权呢?redis

传统方法是,用户将本身的Google用户名和密码,告诉"云冲印",后者就能够读取用户的照片了。这样的作法有如下几个严重的缺点。spring

  1. "云冲印"为了后续的服务,会保存用户的密码,这样很不安全。
  2. Google不得不部署密码登陆,而咱们知道,单纯的密码登陆并不安全。
  3. "云冲印"拥有了获取用户储存在Google全部资料的权力,用户无法限制"云冲印"得到受权的范围和有效期。
  4. 用户只有修改密码,才能收回赋予"云冲印"的权力。可是这样作,会使得其余全部得到用户受权的第三方应用程序所有失效。
  5. 只要有一个第三方应用程序被破解,就会致使用户密码泄漏,以及全部被密码保护的数据泄漏。

 OAuth就是为了解决上面这些问题而诞生的。数据库

2、名词定义

在详细讲解OAuth 2.0以前,须要了解几个专用名词。它们对读懂后面的讲解,尤为是几张图,相当重要。express

  • Third-party application:第三方应用程序,本文中又称"客户端"(client),即上一节例子中的"云冲印"。
  • HTTP service:HTTP服务提供商,本文中简称"服务提供商",即上一节例子中的Google。
  • Resource Owner:资源全部者,本文中又称"用户"(user)。
  • User Agent:用户代理,本文中就是指浏览器。
  • Authorization server:认证服务器,即服务提供商专门用来处理认证的服务器。
  • Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,能够是同一台服务器,也能够是不一样的服务器。  

知道了上面这些名词,就不难理解,OAuth的做用就是让"客户端"安全可控地获取"用户"的受权,与"服务商提供商"进行互动。api

3、OAuth的思路

OAuth在"客户端"与"服务提供商"之间,设置了一个受权层(authorization layer)。"客户端"不能直接登陆"服务提供商",只能登陆受权层,以此将用户与客户端区分开来。"客户端"登陆受权层所用的令牌(token),与用户的密码不一样。用户能够在登陆的时候,指定受权层令牌的权限范围和有效期。浏览器

"客户端"登陆受权层之后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。缓存

4、客户端的受权模式

客户端必须获得用户的受权(authorization grant),才能得到令牌(access token)。OAuth 2.0定义了四种受权方式。安全

  • 受权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

5、受权码模式

受权码模式(authorization code)是功能最完整、流程最严密的受权模式。它的特色就是经过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。

它的步骤以下:

(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端受权。
(C)假设用户给予受权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个受权码。
(D)客户端收到受权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了受权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

6、简化模式

简化模式(implicit grant type)不经过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"受权码"这个步骤,所以得名。全部步骤在浏览器中完成,令牌对访问者是可见的,且客户端不须要认证。

它的步骤以下:

(A)客户端将用户导向认证服务器。

(B)用户决定是否给于客户端受权。

(C)假设用户给予受权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。

(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。

(E)资源服务器返回一个网页,其中包含的代码能够获取Hash值中的令牌。

(F)浏览器执行上一步得到的脚本,提取出令牌。

(G)浏览器将令牌发给客户端。

7、密码模式

密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供本身的用户名和密码。客户端使用这些信息,向"服务商提供商"索要受权。

在这种模式中,用户必须把本身的密码给客户端,可是客户端不得储存密码。这一般用在用户对客户端高度信任的状况下,好比客户端是操做系统的一部分,或者由一个著名公司出品。而认证服务器只有在其余受权模式没法执行的状况下,才能考虑使用这种模式。

它的步骤以下:

(A)用户向客户端提供用户名和密码。

(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。

(C)认证服务器确认无误后,向客户端提供访问令牌。

8、客户端模式

客户端模式(Client Credentials Grant)指客户端以本身的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以本身的名义要求"服务提供商"提供服务,其实不存在受权问题。

它的步骤以下:

(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。

(B)认证服务器确认无误后,向客户端提供访问令牌。

9、更新令牌

若是用户访问的时候,客户端的"访问令牌"已通过期,则须要使用"更新令牌"申请一个新的访问令牌。

客户端发出更新令牌的HTTP请求,包含如下参数:

  • granttype:表示使用的受权模式,此处的值固定为"refreshtoken",必选项。
  • refresh_token:表示早前收到的更新令牌,必选项。
  • scope:表示申请的受权范围,不能够超出上一次申请的范围,若是省略该参数,则表示与上一次一致。

10、client_credentials代码示范

首先引入主要jar包:

<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>
     <version>2.3.3.RELEASE</version>
</dependency>
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
     <groupId>org.springframework.data</groupId>
     <artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
     <groupId>redis.clients</groupId>
     <artifactId>jedis</artifactId>
     <version>2.9.0</version>
</dependency>

下面配置获取token的配置文件:

package cn.chinotan.config.oauth;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
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.token.store.redis.RedisTokenStore;

/**
 * @program: test
 * @description: OAuth2服务配置
 * @author: xingcheng
 * @create: 2018-12-01 16:27
 **/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager ;

    @Autowired
    private RedisConnectionFactory connectionFactory;

    @Bean
    public RedisTokenStore tokenStore() {
        // redis 存储token,方便集群部署
        return new RedisTokenStore(connectionFactory);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) // 配置认证管理器
                .tokenStore(tokenStore()); // 使用redis进行token存储
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()") 
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients(); // 容许表单认证
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("start_test_two") // 获取token的客户端id
                .secret("start_test_two") // 获取token密钥
                .scopes("start_test_two") // 资源范围
                .authorizedGrantTypes("client_credentials", "password", "refresh_token") // 受权类型
                .resourceIds("oauth2-resource") // 资源id
                .accessTokenValiditySeconds(120); // token 有效时间
    }
}

其中,RedisTokenStore这个是基于Redis的实现,令牌(Access Token)会保存到Redis中,须要配置Redis的链接服务

# Redis数据库索引(默认为0)
spring.redis.database: 0
# Redis服务器地址
spring.redis.host: 127.0.0.1
# Redis服务器链接端口
spring.redis.port: 6379
# Redis服务器链接密码(默认为空)
spring.redis.password: 
# 链接池最大链接数(使用负值表示没有限制)
spring.redis.pool.max-active: 8
# 链接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait: -1
# 链接池中的最大空闲链接
spring.redis.pool.max-idle: 8
# 链接池中的最小空闲链接
spring.redis.pool.min-idle: 0
# 链接超时时间(毫秒)
spring.redis.timeout: 100
package cn.chinotan.config.redis;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @program: test
 * @description: redis
 * @author: xingcheng
 * @create: 2018-12-01 17:09
 **/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.timeout}")
    private int timeout;

    @Autowired
    private JedisConnectionFactory jedisConnectionFactory;

    /**
     * Logger
     */
    private static final Logger lg = LoggerFactory.getLogger(RedisConfig.class);

    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        //  设置自动key的生成规则,配置spring boot的注解,进行方法级别的缓存
        // 使用:进行分割,能够不少显示出层级关系
        // 这里其实就是new了一个KeyGenerator对象
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName());
            sb.append(":");
            sb.append(method.getName());
            for (Object obj : params) {
                sb.append(":" + String.valueOf(obj));
            }
            String rsToUse = String.valueOf(sb);
            return rsToUse;
        };
    }

    //缓存管理器
    @Bean
    public CacheManager cacheManager(RedisTemplate redisTemplate) {
        // 初始化缓存管理器,在这里咱们能够缓存的总体过时时间什么的,我这里默认没有配置
        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(jedisConnectionFactory);
        return builder.build();
    }
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory){
        //设置序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置redisTemplate
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<Object, Object>();
        redisTemplate.setConnectionFactory(jedisConnectionFactory);
        RedisSerializer stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer); // key序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value序列化
        redisTemplate.setHashKeySerializer(stringSerializer); // Hash key序列化
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); // Hash value序列化
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Override
    @Bean
    public CacheErrorHandler errorHandler() {
        // 异常处理,当Redis发生异常时,打印日志,可是程序正常走
        CacheErrorHandler cacheErrorHandler = new CacheErrorHandler() {
            @Override
            public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
                lg.error("Redis occur handleCacheGetError:key -> [{}]", key, e);
            }

            @Override
            public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
                lg.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);
            }

            @Override
            public void handleCacheEvictError(RuntimeException e, Cache cache, Object key)    {
                lg.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);
            }

            @Override
            public void handleCacheClearError(RuntimeException e, Cache cache) {
                lg.error("Redis occur handleCacheClearError:", e);
            }
        };
        return cacheErrorHandler;
    }
}

以后配置资源服务器:

package cn.chinotan.config.oauth;

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 javax.servlet.http.HttpServletResponse;

/**
 * @program: test
 * @description: Resource服务配置
 * @author: xingcheng
 * @create: 2018-12-01 16:30
 **/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {


}

以及Web安全配置:

package cn.chinotan.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
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.method.configuration.GlobalMethodSecurityConfiguration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler;

import javax.servlet.http.HttpServletResponse;

/**
 * @program: test
 * @description: WebSecurityConfig
 * @author: xingcheng
 * @create: 2018-12-01 17:29
 **/
@Configuration
@EnableWebSecurity
@Order(Ordered.HIGHEST_PRECEDENCE)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .exceptionHandling() // 统一异常处理
                .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) // 自定义异常返回
                .and()
                .authorizeRequests()
                .antMatchers("/api/**") 
                .authenticated() // 拦截全部/api开头下的资源路径,包括其/api自己
                .anyRequest()
                .permitAll()// 其余请求无需认证
                .and()
                .httpBasic(); // 启用httpBasic认证
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("start_test_two").password(new BCryptPasswordEncoder().encode("start_test_two")).roles("USER"); // 内存中配置httpBasic认证名和密码,使用BCryptPasswordEncoder加密
    }
}

其中注意WebSecurityConfigurerAdapter和ResourceServerConfigurerAdapter都有对于HttpSecurity的配置:

而在ResourceServerConfigurer中,默认全部接口都须要认证:

且一旦匹配上一个filter后就不会走其余的filter了,所以须要将WebSecurityConfigurerAdapter的调用顺序调到最高级:

@Order(Ordered.HIGHEST_PRECEDENCE)

配置完成后启动:

能够看到暴露了/oauth/token接口

Spring-Security-Oauth2的提供的jar包中内置了与token相关的基础端点。本文认证与受权token与/oauth/token有关,其处理的接口类为TokenEndpoint。下面咱们来看一下对于认证与受权token流程的具体处理过程。

1 @FrameworkEndpoint
 2 public class TokenEndpoint extends AbstractEndpoint {
 3     ... 
 4     @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
 5     public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
 6     Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
 7     //首先对client信息进行校验
 8         if (!(principal instanceof Authentication)) {
 9             throw new InsufficientAuthenticationException(
10                     "There is no client authentication. Try adding an appropriate authentication filter.");
11         }
12         String clientId = getClientId(principal);
13         //根据请求中的clientId,加载client的具体信息
14         ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
15         TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
16         ... 
17         
18         //验证scope域范围
19         if (authenticatedClient != null) {
20             oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
21         }
22         //受权方式不能为空
23         if (!StringUtils.hasText(tokenRequest.getGrantType())) {
24             throw new InvalidRequestException("Missing grant type");
25         }
26         //token endpoint不支持Implicit模式
27         if (tokenRequest.getGrantType().equals("implicit")) {
28             throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
29         }
30         ...
31         
32         //进入CompositeTokenGranter,匹配受权模式,而后进行password模式的身份验证和token的发放
33         OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
34         if (token == null) {
35             throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
36         }
37         return getResponse(token);
38     }
39     ...

口处理的主要流程就是对authentication信息进行检查是否合法,不合法直接抛出异常,而后对请求的GrantType进行处理,根据GrantType,进行password模式的身份验证和token的发放。下面咱们来看下TokenGranter的类图。

能够看出TokenGranter的实现类CompositeTokenGranter中有一个List<TokenGranter>,对应五种GrantType的实际受权实现。这边涉及到的getTokenGranter(),代码也列下:

1 public class CompositeTokenGranter implements TokenGranter {
 2     //GrantType的集合,有五种,以前有讲
 3     private final List<TokenGranter> tokenGranters;
 4     public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
 5         this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
 6     }
 7     
 8     //遍历list,匹配到相应的grantType就进行处理
 9     public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
10         for (TokenGranter granter : tokenGranters) {
11             OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
12             if (grant!=null) {
13                 return grant;
14             }
15         }
16         return null;
17     }
18     ...
19 }

启动后,访问下面的接口:

package cn.chinotan.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @program: test
 * @description: oauth2测试类
 * @author: xingcheng
 * @create: 2018-12-01 17:43
 **/
@RestController
public class WordController {

    @RequestMapping("/")
    public String index(){

        return "index" ;
    }

    @RequestMapping("/api")
    public String api(){
        return "api" ;
    }

    @RequestMapping("/login")
    public String login() {
        return "login";
    }


}

能够看到访问/api接口的时候被拦截了,可是其余接口能够访问

那么如何才能访问/api接口呢,首先得获取到access_token才行

经过暴露出的/oauth/token?grant_type=client_credentials接口就能够获取到access_token,其中expires_in为有效时间,看下咱们的token是存储在哪里:

没错,被存在了redis中,相比存在本地内存和数据库中,redis这样的数据结构有着自然的时间特性,能够方便的来作失效处理

以后即可以经过access_token方便的访问/api接口了

NoSuchMethodError.RedisConnection.set([B[B)V #16错误

版本问题,spring-data-redis 2.0版本中set(String,String)被弃用了。而后我按照网页中的决解方法“spring-date-redis”改成2.3.3.RELEASE版本,下面是源码中的存储token过程:

 

相关文章
相关标签/搜索