OAuth2快速入门到实战案例

281 阅读16分钟

1. Oauth2 介绍

1.1 什么是 OAuth 2

  • OAuth 是一个开放标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源 (如头像、照片、视频等),而在这个过程中无须将用户名和密码提供给第三方应用。实现这一功 能是通过提供一个令牌(token),而不是用户名和密码来访问他们存放在特定服务提供者的数据。
  • 每一个令牌授权一个特定的网站在特定的时段内访问特定的资源。这样,OAuth 让用户可以授权 第三方网站灵活地访问存储在另外一些资源服务器的特定信息,而非所有内容。目前主流的 qq,微信等第三方授权登录方式都是基于 OAuth2 实现的。
  • OAuth 2 是 OAuth 协议的下一版本,但不向下兼容 OAuth 1.0。OAuth 2 关注客户端开发者的 简易性,同时为 Web 应用、桌面应用、移动设备、起居室设备提供专门的认证流程。
  • 传统的 Web 开发登录认证一般都是基于 Session 的,但是在前后端分离的架构中继续使用 Session 会有许多不便,因为移动端(Android、iOS、微信小程序等)要么不支持Cookie(微 信小程序),要么使用非常不便,对于这些问题,使用 OAuth 2 认证都能解决。

1.2 OAuth 2 授权流程

这是官网的流程图 image.png 翻译一下更好理解 image.png

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

看到一个图解能很好的理解这个过程,作者是chihokyo

1.3 OAuth 2 授权详细图解

  1. 我们这里有一份用户的数据

image.png 2. 用户的数据我们保存在资源服务器 (Resource server) 里 image.png 3. 这时候有个 第三方应用程序(Third-party application)想要请求资源服务器要用户数据 image.png 4. 为了让用户数据和第三方程序程序良好的交互,资源服务器准备了一个 API 接口

image.png

  1. 第三方应用程序向资源服务器请求用户的数据

image.png

  1. 资源服务器表示好的给你了

image.png

  1. 但如果这个第三方应用程序是恶意的第三方呢?那么就会有以下的场景出现

image.png 8. 所以我们需要一个机制来保护 API 接口,不能随随便便毫无安全可言的把用户的数据送出去 image.png 9. 这个最佳实践就事先在第三方程序里保存一个令牌 access_token image.png 10. 第三方应用程序在向资源服务器请求用户数据的时候会出示这个 access_token image.png 11. 然后资源服务器取出授权码并且验证是否有授权 image.png 12. 授权通过,资源服务器才会把用户数据传递给第三方应用程序 image.png 13. 但这种方案需要事先给第三方 access_token image.png 14. 所以我们需要一个东西用来发行这个 access_token,这时候认证服务器 (Authorization server)登场了 image.png 15. 认证服务器负责生成并且发行 access_token 给第三方应用程序

image.png

  1. 接下来我们看一下目前的登场的人物有
  • 第三方应用程序
  • 资源服务器
  • 认证服务器
  • access_token
  • 用户数据

资源服务器和认证服务器有时候是同一台服务器 image.png

  1. 接下来我们来走一下流程 认证服务器生成 access_token image.png

  2. 认证服务器发行 access_token 授权给第三方应用程序 image.png

  3. 第三方应用程序拿着 access_token 去找资源服务器要用户数据 image.png

  4. 资源服务器取出来 access_token 并验证 image.png

  5. 验证通过 用户数据送出

image.png

  1. 问题点来了

到上面为止有个很大的问题就是,认证服务器生成 access_token 竟然没人管!那岂不是随便发行了,这不行,于是我们的用户(Resource Owner:资源所有者)出现了!

image.png

  1. 解决

认证服务器在发行 access_token 之前要先通过用户的同意

  1. 于是接下来就是
  2. 第三方应用程序向认证服务器要 access_token

image.png 2. 认证服务器生成之前先问问用户能不能授权啊

image.png 3. 用户说好的可以给

image.png 认证服务器生成 access_token 并且发行给第三方应用程序

image.png 25. oAuth2.0

第三方应用程序和这个认证服务器之间围绕着access_token进行请求和响应的等等就是 oAuth2.0

image.png

1.4 OAuth 2 角色

  • 资源所有者(Resource Owner):即代表授权客户端访问本身资源信息的用户,客户端访问用户帐户 的权限仅限于用户授权的“范围”。
  • 客户端(Client):即代表意图访问受限资源的第三方应用。在访问实现之前,它必须先经过用户者授 权,并且获得的授权凭证将进一步由授权服务器进行验证。
  • 授权服务器(Authorization Server):授权服务器用来验证用户提供的信息是否正确,并返回一个令牌给第三方应用。
  • 资源服务器(Resource Server):资源服务器是提供给用户资源的服务器,例如头像、照片、视频 等。

1.5 OAuth 2 授权模式

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

  • 授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。它的 特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本都是使用这种模式。
  • 简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器中请令牌,一般若网站是纯静态页面,则可以采用这种方式。
  • 密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器中请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。
  • 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。

2. Spring Security Oauth2.0 入门案例

授权认证服务器和资源服务器可以用一个工程表示,这里分开为了结构更清晰,方便演示微服务架构中权限认证的流程。

2.1 授权服务器

2.1.1 搭建授权工程auth-server

pom.xml

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.1.RELEASE</version>
    <relativePath/>
</parent>

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Hoxton.SR4</spring-cloud.version>
</properties>

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>

application.yml

server:
    port: 8888

2.1.2 配置类

Oauth2配置类 OAuth2Config.java

@Configuration
//开启授权服务
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    private static final String CLIENT_ID = "cms";
    private static final String SECRET_CHAR_SEQUENCE = "{noop}secret";
    private static final String SCOPE_READ = "read";
    private static final String SCOPE_WRITE = "write";
    private static final String TRUST = "trust";
    private static final String USER ="user";
    private static final String ALL = "all";
    private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 30*60;
    private static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 30*60;
    // 密码模式授权模式
    private static final String GRANT_TYPE_PASSWORD = "password";
    //授权码模式
    private static final String AUTHORIZATION_CODE = "authorization_code";
    //refresh token模式
    private static final String REFRESH_TOKEN = "refresh_token";
    //简化授权模式
    private static final String IMPLICIT = "implicit";
    //客户端模式
    private static final String CLIENT_CREDENTIALS="client_credentials";
    //指定哪些资源是需要授权验证的
    private static final String RESOURCE_ID = "resource_id";

    /**
     * 配置客户端信息
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
                // 使用内存存储
                .inMemory()
                //标记客户端id
                .withClient(CLIENT_ID)
                //客户端安全码
                .secret(SECRET_CHAR_SEQUENCE)
                //为true 直接自动授权成功返回code
                .autoApprove(true)
                .redirectUris("http://127.0.0.1:8084/cms/login") //重定向uri
                //允许授权范围
                .scopes(ALL)
                //token 时间秒
                .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
                //刷新token 时间 秒
                .refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS)
                //允许授权类型
                .authorizedGrantTypes(AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT, GRANT_TYPE_PASSWORD, CLIENT_CREDENTIALS);
    }

    /**
     * token存储方式
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 使用内存保存生成的token
        endpoints.authenticationManager(authenticationManager).tokenStore(memoryTokenStore());
    }

    /**
     * 认证服务器的安全配置
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                // 开启/oauth/token_key验证端口认证权限访问
                .tokenKeyAccess("permitAll()")
                //  开启/oauth/check_token验证端口认证权限访问
//                .checkTokenAccess("isAuthenticated()")
                .checkTokenAccess("permitAll()")
                //允许表单认证
                .allowFormAuthenticationForClients();
    }

    @Bean
    public TokenStore memoryTokenStore() {
        // 最基本的InMemoryTokenStore生成token
        return new InMemoryTokenStore();
    }

}

Spring Security配置类

@Configuration
@EnableWebSecurity
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {    //auth.inMemoryAuthentication()
        auth.inMemoryAuthentication()
                .withUser("xxx")
                .password("{noop}123")
                .roles("admin");
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //解决静态资源被拦截的问题
        web.ignoring().antMatchers("/asserts/**");
        web.ignoring().antMatchers("/favicon.ico");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http   // 配置登录页并允许访问
                .formLogin().permitAll()
                // 配置Basic登录
                //.and().httpBasic()
                // 配置登出页面
                .and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
                // 配置允许访问的链接
                .and().authorizeRequests().antMatchers("/oauth/**", "/login/**", "/logout/**", "/api/**").permitAll()
                // 其余所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                // 关闭跨域保护;
                .and().csrf().disable();
    }

}

2.1.3 启动AuthServerApplication测试

image.png

因为现在还没有获取token,能够执行但是token是无效的,设置了.checkTokenAccess("permitAll()")允许匿名访问,测试成功,验证服务器测试完成。

2.2 资源服务器

2.2.1 搭建资源服务器

pom.xml

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>

application.properties

server.port=8084
server.servlet.context-path=/cms

2.2.2 配置类

@Configuration
public class Oauth2ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    //令牌校验端点
    private static final String CHECK_TOKEN_URL = "http://localhost:8888/oauth/check_token";

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
      RemoteTokenServices tokenService = new RemoteTokenServices();
      tokenService.setCheckTokenEndpointUrl(CHECK_TOKEN_URL);
      tokenService.setClientId("cms");
      tokenService.setClientSecret("secret");

      resources.tokenServices(tokenService);
    }

}

这里使用TokenService验证令牌,这种方式,每次验证都要跟认证服务器鉴权,判断合不合法,这样效率较低。
平时项目中使用JWT公钥私钥验证,私钥颁发令牌,公钥给资源服务器,资源服务器保留公钥,这样就能客户端自己完成。

spring security配置类

@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  
    @Override
    protected void configure(HttpSecurity http) throws Exception {
      http.authorizeRequests().antMatchers("/**").authenticated();
      // 禁用CSRF
      http.csrf().disable();
    }
    
}

2.2.3 Controller

@RestController
public class HelloController {

    @GetMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication) {
        return authentication;
    }

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

2.2.4 测试

image.png

不让访问,没有认证。需要携带着授权服务器颁发的令牌访问,才能访问。

而授权服务器颁发令牌通过4中授权模式:授权码模式、简化模式、密码模式、客户端模式。

3. OAuth2授权模式

回顾一下OAuth2的四种授权模式: OAuth 协议的授权模式共分为 4 种,分别说明如下:

  • 授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。它的 特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本都是使用这种模式。
  • 简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器中请令牌,一般若网站是纯静态页面,则可以采用这种方式。
  • 密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器中请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。
  • 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。

3.1 授权码模式

3.1.1 授权码模式流程

授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌 1。授权码模式功能最完整、使用最广泛、流程最严密的授权模式。

image.png 第三方授权一般就是授权码模式,流程如下:

  • (A):客户端携带client_id、redirect_uri,中间通过代理者访问授权服务器,如果已经登录过会 直接返回redirect_uri,没有登录过就跳转到登录页面
  • (B)授权服务器对客户端进行身份验证(通过用户代理,让用户输入用户名和密码)
  • (C)授权通过,会重定向到redirect_uri并携带授权码code作为uri参数
  • (D)客户端携带授权码访问授权服务器
  • (E)验证授权码通过,返回acceptToken。

3.1.2 认证服务授权码配置

image.png

3.1.2.1 申请授权码

访问授权链接,在浏览器访问就可以,授权码模式response_type参数传code:

Get请求: http://localhost:8888/oauth/authorize?client_id=cms&client_secret=secret&response_type=code

  • client_id:客户端id,和授权配置类中设置的客户端id一致。
  • response_type:授权码模式固定为
  • code scop:客户端范围,和授权配置类中设置的scop一致。
  • redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)

因为没登录,所以会返回SpringSecurity的默认登录页面,具体代码是 http .formLogin().permitAll(); ,如果要弹窗登录的,可以配置 http.httpBasic(); ,这种配置是没有 登录页面的,自定义登录页面可以这样配置 http.formLogin().loginPage("/login").permitAll()
参考OAuth2Config代码

image.png

输入zhangsan123

image.png

image.png

登录成功,返回redirect_uri,拿到授权码 image.png

3.1.2.2 申请令牌

拿到授权码后,申请令牌。

  • grant_type:授权类型,填写authorization_code,表示授权码模式
  • code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
  • redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。

http basic认证:

image.png

image.png 返回信息:

  • access_token:访问令牌,携带此令牌访问资源
  • token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer Token
  • refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
  • expires_in:过期时间,单位为秒。
  • scope:范围,与定义的客户端范围一致。
  • jti:当前token的唯一标识

3.1.2.3 校验令牌

Spring Security Oauth2提供校验令牌的端点,如下:

http://localhost:8888/oauth/check_token?token=c6203426-01be-43ae-ab00-af518de8d4cf

image.png

3.1.2.4 使用令牌

使用正确令牌访问/index服务

image.png image.png

3.2 简化模式

3.2.1 简化模式流程

简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此称简化模式。简化模式是相对于授权码模式而言的。

image.png 简化模式,流程如下:

  • (A):客户端携带client_id、redirect_uri,中间通过代理者访问授权服务器,如果已经登录过会 直接返回redirect_uri,没有登录过就跳转到登录页面
  • (B)授权服务器对客户端进行身份验证(通过用户代理,让用户输入用户名和密码)
  • (C)验证通过,授权服务器直接将token作为参数返回给User-agent
  • (D)User-agent携带token访问资源服务器
  • (E)验证token通过,返回资源

授权码模式与简化模式主要的区别就在于简化模式跳过了授权码阶段,并且在授权码模式下user-agent只持有code,但是在简化模式下user-agent持有token,简化模式相对也没有这么安全。

3.2.2 认证服务授权配置

image.png

3.2.3 功能测试

3.2.3.1 申请令牌

访问授权链接,在浏览器访问就可以,授权码模式response_type参数传token

Get请求: http://localhost:8888/oauth/authorize?client_id=cms&redirect_uri=http://127.0.0.1:8084/cms/login&response_type=token&scope=all

输入zhangsan123

image.png

登录成功,返回redirect_uri,直接拿到token令牌 image.png

3.2.3.2 校验令牌

Spring Security Oauth2提供校验令牌的端点,如下:

Get: http://localhost:8888/oauth/check_token?token=883c1dbc-4236-46a0-b5cf-e0d7266f7026

image.png

3.2.3.3 使用令牌

使用正确令牌访问/index服务 image.png

3.2.3.4 简化模式和授权码模式的区别

授权码模式User-agent(浏览器)只是持有授权码(code)使用授权码获得令牌,授权码,只能校验 一次,这样即使授权码泄露,令牌相对安全,而简化模式由user agent(浏览器),直接持有令牌,相对不安全

3.3 密码模式

密码模式(resource owner password credentials):密码模式中,用户向客户端提供自己的用户名 和密码,这通常用在用户对客户端高度信任的情况 image.png

密码授权一般就是授权码模式,流程如下:

  • (A)用户访问客户端,提供URI连接包含用户名和密码信息给授权服务器
  • (B)授权服务器对客户端进行身份验证
  • (C)授权通过,返回acceptToken给客户端

3.3.1 密码模式配置

image.png

3.3.2 功能测试

3.3.2.1 申请令牌

  • grant_type:授权类型,填写password,表示密码模式
  • username:用户名
  • password:密码 此链接需要使用 http Basic认证。 什么是http Basic认证? http协议定义的一种认证方式,将客户端id 和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用base64编码,放在header中请求服务端, 一个例子: Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA= 是用户名:密码的base64编码。 认证失败服务端返回 401 Unauthorized。
    http basic认证:

image.png

3.3.2.2 令牌校验

image.png

3.3.2.3 使用令牌

image.png

3.4 客户端模式

3.4.1 客户端模式流程

客户端模式(client credentials):客户端模式(client credentials)适用于没有前端的命令行应用,即在命令行下请求令牌。

image.png 客户端模式,流程如下:

3.4.2 认证服务授权码配置

image.png

3.4.3 功能测试

3.4.3.1 申请令牌

post请求: http://localhost:8888/oauth/token?client_id=cms&client_secret=secret&grant_type=client_credentials&scope=all

image.png

3.4.3.2 令牌校验

image.png

3.4.3.3 令牌使用

image.png

4. 令牌存储方式

对于token存储有如下方式,分别进行介绍:

  • InMemoryTokenStore,默认存储,保存在内存
  • JdbcTokenStore,access_token存储在数据库
  • JwtTokenStore,JWT这种方式比较特殊,这是一种无状态方式的存储,不进行内存、数据库存 储,只是JWT中携带全面的用户信息,保存在jwt中携带过去校验就可以,系统中采用 JwtTokenStore
  • RedisTokenStore,将 access_token 存到 redis 中。
  • JwkTokenStore,将 access_token 保存到 JSON Web Key。

image.png