OAuth 2

1. OAuth 2简介

OAuth是一个开放标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如头像、照片、视频等),而在这个过程中无须将用户名和密码提供给第三方应用。实现这一功能是通过提供一个令牌(token) ,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站在特定的时段内访问特定的资源。这样,OAuth 让用户可以授权第三方网站灵活地访问存储在另外一些资源服务器的特定信息,而非所有内容。例如,用户想通过QQ登录知乎,这时知乎就是-一个第三方应用,知乎要访问用户的一些基本信息就需要得到用户的授权,如果用户把自己的QQ用户名和密码告诉知乎,那么知乎就能访问用户的所有数据,并且只有用户修改密码才能收回授权,这种授权方式安全隐患很大,如果使用OAuth,就能很好地解决这一问题。

采用令牌的方式可以让用户灵活地对第三方应用授权或者收回权限。OAuth2是OAuth协议的下一版本,但不向下兼容OAuth 1.0。OAuth 2关注客户端开发者的简易性,同时为Web应用、桌面应用、移动设备、起居室设备提供专门的认证流程。传统的Web开发登录认证一般都是基于Session的,但是在前后端分离的架构中继续使用Session会有许多不便,因为移动端(Android、iOS、微信小程序等)要么不支持Cookie(微信小程序),要么使用非常不便,对于这些问题,使用OAuth 2认证都能解决。

2. OAuth2角色

要了解OAuth2,需要先了解OAuth2中几个基本的角色。

  • 资源所有者: 资源所有者即用户,具有头像、照片、视频等资源。
  • 客户端:客户端即第三方应用,例如上文提到的知乎。
  • 授权服务器:授权服务器用来验证用户提供的信息是否正确,并返回一个令牌给第三方应用。
  • 资源服务器: 资源服务器是提供给用户资源的服务器,例如头像、照片、视频等。

一般来说,授权服务器和资源服务器可以是同一台服务器。

3. OAuth 2授权流程

OAuth 2的授权流程到底是什么样的呢?如图所示。

这是OAuth2一个大致的授权流程图,具体步骤如下:

步骤01:客户端 (第三方应用)向用户请求授权。
步骤02:用户单击客户端所呈现的服务授权页面上的同意授权按钮后,服务端返回一个授权许可凭证给客户端。
步骤03:客户端拿着授权许可凭证去授权服务器申请令牌。
步骤04:授权服务 器验证信息无误后,发放令牌给客户端。
步骤05:客户端拿着令牌去资源服务器访问资源。
步骤06:资源服 务器验证令牌无误后开放资源。

这是一个大致的流程,因为OAuth 2中有4种不同的授权模式,每种授权模式的授权流程又会有差异,基本流程如图所示。

在这里插入图片描述

4.授权模式

OAuth协议的授权模式共分为4种,分别说明如下。

  • 授权码模式:授权码模式( authorization code)是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本都是使用这种模式。

  • 简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器申请令牌,一般若网站是纯静态页面,则可以采用这种方式。

  • 密码模式: 密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器申请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。

  • 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作OAuth协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。

这4种模式各有千秋,分别适用于不同的开发场景,开发者要根据实际情况进行选择。

5. 实践

本案例要介绍的是在前后端分离应用(或者为移动端、微信小程序等)提供的认证服务器中如何搭建OAuth服务,因此主要介绍密码模式。搭建步骤如下。

5.1 创建项目,添加依赖

创建Spring Boot Web项目,添加如下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.5.2.RELEASE</version>
</dependency>

由于Spring Boot中的OAuth协议是在Spring Security的基础上完成的,因此首先要添加Spring Security依赖,要用到OAuth 2,因此添加OAuth 2相关依赖,令牌可以存储在Redis缓存服务器上,同时Redis具有过期等功能,很适合令牌的存储,因此也加入Redis依赖。项目创建成功后,接下来在application.properties中配置一下Redis 服务器的连接信息,代码如下:

spring.redis.database=0
spring.redis.host=120.55.61.170
spring.redis.port=6379
spring.redis.password=123@456
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.max-wait=-1ms
spring.redis.jedis.pool.min-idle=0

Redis配置可以参考我的springboot的整合redis的博客,这里不再赘述。

5.2 配置授权服务器

授权服务器和资源服务器可以是同一台服务器,也可以是不同服务器,本案例中假设是同一台服务器,通过不同的配置分别开启授权服务器和资源服务器,首先是授权服务器:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    // 该对象用来支持 password 模式
    @Autowired
    AuthenticationManager authenticationManager;

    // 该对象用来将令牌信息存储到内存中
    @Autowired(required = false)
    TokenStore inMemoryTokenStore;

    // 该对象将为刷新token提供支持
    @Autowired
    UserDetailsService userDetailsService;

    // 指定密码的加密方式
    @Bean
    PasswordEncoder passwordEncoder() {
        // 使用BCrypt强哈希函数加密方案(密钥迭代次数默认为10)
        return new BCryptPasswordEncoder();
    }

    // 配置 password 授权模式
    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
            throws Exception {
        clients.inMemory()
                .withClient("password")
                .authorizedGrantTypes("password", "refresh_token") //授权模式为password和refresh_token两种
                .accessTokenValiditySeconds(1800) // 配置access_token的过期时间
                .resourceIds("rid") //配置资源id
                .scopes("all")
                .secret("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq"); //123加密后的密码
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.tokenStore(inMemoryTokenStore) //配置令牌的存储(这里存放在内存中)
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        // 表示支持 client_id 和 client_secret 做登录认证
        security.allowFormAuthenticationForClients();
    }
}

代码解释:

  • 自定义类继承自AuthorizationServerConfigurerAdapter, 完成对授权服务器的配置,然后通过@EnableAuthorizationServer注解开启授权服务器。
  • 第6、7行注入了AuthenticationManager,该对象将用来支持password模式。
  • 第10、11行注入了RedisConnectionFactory, 该对象将用来完成Redis缓存,将令牌信息存储到Redis缓存中。
  • 第14、15行注入了UserDetailsService, 该对象将为刷新token提供支持。
  • 第 18~22行提供一个PasswordEncoder,这个和前文中的配置一样,不再赘述。
  • 第25~35行配置password授权模式,authorizedGrantTypes 表示OAuth 2中的授权模式为“password”和“refresh_ token” 两种,在标准的OAuth 2协议中,授权模式并不包括“refresh_ token” ,但是在Spring Security的实现中将其归为一种,因此如果要实现access_token的刷新,就需要添加这样一种授权模式; accessTokenValiditySeconds方法配置了access_ token的过期时间; resourceIds 配置了资源id; secret 方法配置了加密后的密码,明文是123.
  • 第39 行配置了令牌的内存存储,AuthenticationManager 和UserDetailsService 主要用于支持password模式以及令牌的刷新。
  • 第47行的配置表示支持client_ id 和client_ secret 做登录认证。

5.3 配置资源服务器

接下来配置资源服务器,代码如下:

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {

        resources.resourceId("rid") // 配置资源id,这里的资源id和授权服务器中的资源id一致
                .stateless(true); // 设置这些资源仅基于令牌认证
    }

    // 配置 URL 访问权限
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/user/**").hasRole("user")
                .anyRequest().authenticated();
    }
}

代码解释:

  • 自定义类继承 自ResourceServerConfigurerAdapter,并添加@EnableResourceServer注解开启资源服务器配置。
  • 第8行配置资源id,这里的资源id和授权服务器中的资源id一致,然后设置这些资源仅基于令牌认证。
  • 第13~19行配置HttpSecurity,这和前面博客介绍的配置一致, 不再赘述。

5.4 配置Security

接下来配置Spring Security,代码如下:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {

        return super.userDetailsService();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") //123
                .roles("admin")
                .and()
                .withUser("sang")
                .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") //123
                .roles("user");
    }

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

这里Spring Security的配置基本.上和前文一致, 唯一不同的是多 了两个Bean,这里两个Bean将注入授权服务器配置类中使用。另外,这里的HttpSecurity配置主要是配置“/oauth/**” 模式的URL,这一-类的请求直接放行。在Spring Security 配置和资源服务器配置中,一共涉及两个HttpSecurity,其中Spring Security 中的配置优先级高于资源服务器中的配置,即请求地址先经过Spring Security的HttpSecurity, 再经过资源服务器的HttpSecurity。

5.5 测试验证

首先创建三个简单的请求地址,代码如下:

@RestController
public class HelloController {
    @GetMapping("/admin/hello")

    public String admin() {
        return "hello admin!";
    }

    @GetMapping("/user/hello")
    public String user() {
        return "hello user! ";
    }

    @GetMapping("/hello")
    public String hello() {
        return "hello!";
    }
}

根据前文的配置,要请求这三个地址,分别需要admin角色、user 角色以及登录后访问。所有都配置完成后,启动Redis服务器,再启动Spring Boot项目,首先发送一个 POST请求获取token,请求地址如下(注意这里是一个POST请求,为了显示方便,将参数写在地址栏中) :
http://localhost:8080/oauth/token?username=sang&password=123&grant_type=password&client_id=password&scope=all&client_secret=123请求地址中包含的参数有用户名、密码、授权模式、客户端id、scope 以及客户端密码,基本就是授权服务器中所配置的数据或者使用postman进行访问。

在这里插入图片描述

access_token 过期后,可以使用 refresh_token 重新获取新的 access_token(前提是 access_token 未过期),这里也是 POST 请求:

  • 请求地址oauth/token(不变)
  • 请求参数:授权模式(变成了 refresh_token)、refresh_token、客户端 id、以及客户端密码
  • 返回结果:与获取前面登录获取 token 返回的内容项一样。不过每次请求,access_tokenaccess_token有效期都会变化。

在这里插入图片描述

接下来访问所有资源,携带上access_ token 参数即可,例如“/user/hello” 接口:http://localhost:8080/user/hello?access_token=p8L-rTNEf3CqPIkp8WCPvneSd8c访问结果如图所示。

在这里插入图片描述

如果非法访问一个资源,比如 admin 用户访问“user/hello”接口,结果如下:

在这里插入图片描述

5.6 将token令牌保存到Redis服务器上

在上面的样例中,令牌(Access Token)是存储在内存中,这种方式在单服务上可以体现出很好特效(即并发量不大,并且它在失败的时候不会进行备份)

我们也可以将令牌保存到数据库或者 Redis 缓存服务器上。使用这中方式,可以在多个服务之间实现令牌共享。下面我通过样例演示如何将令牌存储Redis 缓存服务器上,同时 Redis 具有过期等功能,很适合令牌的存储

将授权服务器进行修改

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    // 该对象用来支持 password 模式
    @Autowired
    AuthenticationManager authenticationManager;

    // 该对象用来将令牌信息存储到Redis中
    @Autowired
    RedisConnectionFactory redisConnectionFactory;

    // 该对象将为刷新token提供支持
    @Autowired
    UserDetailsService userDetailsService;

    // 指定密码的加密方式
    @Bean
    PasswordEncoder passwordEncoder() {
        // 使用BCrypt强哈希函数加密方案(密钥迭代次数默认为10)
        return new BCryptPasswordEncoder();
    }

    // 配置 password 授权模式
    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
            throws Exception {
        clients.inMemory()
                .withClient("password")
                .authorizedGrantTypes("password", "refresh_token") //授权模式为password和refresh_token两种
                .accessTokenValiditySeconds(1800) // 配置access_token的过期时间
                .resourceIds("rid") //配置资源id
                .scopes("all")
                .secret("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq"); //123加密后的密码
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory)) //配置令牌存放在Redis中
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        // 表示支持 client_id 和 client_secret 做登录认证
        security.allowFormAuthenticationForClients();
    }
}

运行测试

启动项目,再次通过 /oauth/token 接口获取令牌。然后查看下Redis中的数据,可以发现令牌确实以及保存在Redis上了:

在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐