目录

  • 一、oauth介绍
  • (一)基本介绍
  • (二)什么情况下需要使用OAuth2?
  • 1、第三方授权登录的场景
  • 2、单点登录的场景
  • (三)OAuth2的颁发Token授权方式
  • (四)实现思路
  • 二、搭建认证服务器(Authorization Server)
  • (一)pom
  • (二)配置文件
  • (三)启动类
  • (四)认证服务主配置类
  • (五)辅助配置类:处理用户名和密码等校验信息
  • (六)测试
  • 1、获取token
  • 2、校验token
  • ①校验成功
  • ②校验失败
  • 3、刷新token
  • 三、资源服务器(希望访问被认证的微服务)Resource Server
  • (一)引入pom
  • (二)主配置类
  • (三)测试
  • 1、准备
  • 2、测试
  • 3、注意


一、oauth介绍

(一)基本介绍

OAuth(开放授权)是⼀个开放协议/标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,⽽不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。

比如,登录拉勾网的时候,可以不在拉勾网注册账户,而是使用qq账户直接登录。

用户:我们自己

第三方应用:拉勾网

另外的服务提供者:QQ

OAuth2是OAuth协议的延续版本,但不向后兼容OAuth1即完全废⽌了OAuth1。

微服务 协议 微服务对外开放的协议_ide

  • 资源所有者(Resource Owner):可以理解为用户自己
  • 客户端(Client):我们想登陆的网站或应用,比如拉勾网
  • 认证服务器(Authorization Server):可以理解为微信或者QQ
  • 资源服务器(Resource Server):可以理解为微信或者QQ

使用OAuth2解决问题的本质是,引⼊了⼀个认证授权层,认证授权层连接了资源的拥有者,在授权层⾥⾯,资源的拥有者可以给第三方应用授权去访问我们的某些受保护资源

(二)什么情况下需要使用OAuth2?

1、第三方授权登录的场景

比如,我们经常登录⼀些网站或者应用的时候,可以选择使用第三方授权登录的方式,比如:微信授权登录、QQ授权登录、微博授权登录等,这是典型的 OAuth2 使用场景。

2、单点登录的场景

如果项目中有很多微服务或者公司内部有很多服务,可以专门做⼀个认证中心(充当认证平台角色),所有的服务都要到这个认证中心做认证,只做⼀次登录,就可以在多个授权范围内的服务中自由串行。

(三)OAuth2的颁发Token授权方式

  1. 授权码(authorization-code):适用于第三方登录的场景。
  2. 密码式(password):提供用户名+密码换取token令牌。如单点登录的时可以使用这种方式。
  3. 隐藏式(implicit)
  4. 客户端凭证(client credentials)

授权码模式使用到了回调地址,是最复杂的授权方式,微博、微信、QQ等第三方登录就是这种模式。我们重点讲解接⼝对接中常使用的password密码模式(提供用户名+密码换取token)。

(四)实现思路

微服务 协议 微服务对外开放的协议_spring_02


注意:

在我们统⼀认证的场景中,Resource Server其实就是我们的各种受保护的微服务,微服务中的各种API访问接⼝就是资源,发起http请求的浏览器就是Client客户端(对应为第三方应用)

二、搭建认证服务器(Authorization Server)

新建项目lagou-cloud-oauth-server-9999

(一)pom

<dependencies>
        <!--导入Eureka Client依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>


        <!--导入spring cloud oauth2依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.security.oauth.boot</groupId>
                    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.11.RELEASE</version>
        </dependency>
        <!--引入security对oauth2的支持-->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.4.RELEASE</version>
        </dependency>

    </dependencies>

(二)配置文件

没有任何特殊配置

server:
  port: 9999
spring:
  application:
    name: lagou-cloud-oauth-server
eureka:
  client:
    service-url:
      defaultZone: http://LagouCloudEurekaServerA:8762/eureka,http://LagouCloudEurekaServerB:8763/eureka
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

(三)启动类

没有任何特殊配置

package com.lagou.edu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;


@SpringBootApplication
@EnableDiscoveryClient
public class OauthServerApplication9999 {
    public static void main(String[] args) {
        SpringApplication.run(OauthServerApplication9999.class, args);
    }
}

(四)认证服务主配置类

package com.lagou.edu.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

/**
 * 当前类为Oauth2 server的配置类(需要继承特定的父类 AuthorizationServerConfigurerAdapter)
 */
@Configuration
@EnableAuthorizationServer // 开启认证服务器功能
public class OauthServerConfigurer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    /**
     * 认证服务器最终是以api接口的方式对外提供服务(校验合法性并生成令牌、校验令牌等)
     * 那么,以api接口方式对外的话,就涉及到接口的访问权限,我们需要在这里进行必要的配置
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        super.configure(security);

        // 相当于打开endpoints 访问接口的开关,这样的话后期我们能够访问该接口
        security.allowFormAuthenticationForClients() // 允许客户端表单认证
                .tokenKeyAccess("permitAll()") // 开启端口/oauth/token_key的访问权限(允许)
                .checkTokenAccess("permitAll()"); // 开启端口/oauth/check_token的访问权限(允许)
    }

    /**
     * 客户端详情配置,
     * 比如client_id,secret
     * 当前这个服务就如同QQ平台,拉勾网作为客户端需要qq平台进行登录授权认证等,提前需要到QQ平台注册,QQ平台会给拉勾网
     * 颁发client_id等必要参数,表明客户端是谁
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        super.configure(clients);

        // V1:从内存中加载客户端详情
        clients.inMemory() // 客户端信息存储在什么地方,可以在内存中,可以在数据库里
                .withClient("client_lagou") // 添加一个client配置,指定其client_id
                .secret("abcxyz") // 指定客户端的密码/安全码
                .resourceIds("autodeliver")  // 指定客户端所能访问资源id清单,此处的资源id是需要在具体的资源服务器上也配置一样
                // 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定
                .authorizedGrantTypes("password", "refresh_token")
                // 客户端的权限范围,此处配置为all全部即可
                .scopes("all");
    }

    /**
     * 认证服务器是玩转token的,那么这⾥配置token令牌管理相关(token此时就是⼀个字符串,当下的token需要在服务器端存储,
     * 那么存储在哪⾥呢?都是在这⾥配置)
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        super.configure(endpoints);

        endpoints.tokenStore(tokenStore()) // 指定token的存储方法
                .tokenServices(authorizationServerTokenServices())   // token服务的一个描述,可以认为是token生成细节的描述,比如有效时间多少等
                .authenticationManager(authenticationManager) // 指定认证管理器,随后注入一个到当前类使用即可
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
    }

    /**
     * 该方法用于创建tokenStore对象(令牌存储对象)
     * token以什么形式存储
     * @return
     */
    private TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

    /**
     * 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
     * @return
     */
    private AuthorizationServerTokenServices authorizationServerTokenServices(){
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
        defaultTokenServices.setTokenStore(tokenStore());
        // 设置令牌有效时间(一般设置为2个小时)
        defaultTokenServices.setAccessTokenValiditySeconds(20);// 20秒
        // 设置刷新令牌的有效时间
        defaultTokenServices.setRefreshTokenValiditySeconds(3*24*60*60);

        return defaultTokenServices;
    }
}

(五)辅助配置类:处理用户名和密码等校验信息

package com.lagou.edu.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.ArrayList;

/**
 * 该配置类,主要处理用户名和密码的校验等事宜
 */
@Configuration
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    // 密码编码对象(密码不进行加密处理)
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    // 注册一个认证管理器对象到容器
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 处理用户名和密码验证事宜
     * 1)客户端传递username和password参数到认证服务器
     * 2)⼀般来说,username和password会存储在数据库中的用户表中
     * 3)根据用户表中数据,验证当前传递过来的用户信息的合法性
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        super.configure(auth);
        // 在这个方法中就可以去关联数据库了,当前我们先把用户信息配置在内存中
        // TODO 实例化一个用户对象(相当于数据表中的一条用户记录)
        UserDetails userDetails = new User("admin", "123456", new ArrayList<>());
        auth.inMemoryAuthentication()
                .withUser(userDetails)
                .passwordEncoder(passwordEncoder);
    }

}

(六)测试

1、获取token

http://localhost:9999/oauth/token?client_secret=abcxyz&grant_type=password&username=admin&password=123456&client_id=client_lagou

出现如下信息,表示获取成功

微服务 协议 微服务对外开放的协议_微服务 协议_03

{
    "access_token": "cb66e9a3-818b-4d8e-9ae8-c0ffbe3c9fae",
    "token_type": "bearer",
    "refresh_token": "f4bee302-f9de-4ed2-857f-eec6c9f9f0a0",
    "expires_in": 20,
    "scope": "all"
}

2、校验token

http://localhost:9999/oauth/check_token?token=cb66e9a3-818b-4d8e-9ae8-c0ffbe3c9fae

①校验成功

微服务 协议 微服务对外开放的协议_spring_04

{
    "aud": [
        "autodeliver"
    ],
    "active": true,
    "exp": 1610035281,
    "user_name": "admin",
    "client_id": "client_lagou",
    "scope": [
        "all"
    ]
}
②校验失败

如,token过期等,会返回校验失败的提示信息

微服务 协议 微服务对外开放的协议_oauth2_05

{
    "error": "invalid_token",
    "error_description": "Token has expired"
}

3、刷新token

http://localhost:9999/oauth/token?grant_type=refresh_token&client_id=client_lagou&client_secret=abcxyz&refresh_token=f4bee302-f9de-4ed2-857f-eec6c9f9f0a0

如果是token过期,还可以使用获取token时返回的提示信息里的refresh_token,再重新获取一个新的token,此时,再次返回的获取成功的信息里面,refresh_token不变

微服务 协议 微服务对外开放的协议_客户端_06

{
    "access_token": "ba426a77-4d76-4cec-a6af-4c9b3235b9b0",
    "token_type": "bearer",
    "refresh_token": "f4bee302-f9de-4ed2-857f-eec6c9f9f0a0",
    "expires_in": 20,
    "scope": "all"
}

微服务 协议 微服务对外开放的协议_oauth2_07

三、资源服务器(希望访问被认证的微服务)Resource Server

(一)引入pom

同认证服务器

(二)主配置类

package com.lagou.edu.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;

@Configuration
@EnableResourceServer // 开启资源服务器功能
@EnableWebSecurity // 开启web访问安全
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {

    /**
     * 该方法用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜
     *
     * @param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//        super.configure(resources);

        // 1、设置当前资源服务的资源id
        resources.resourceId("autodeliver");

        // 2、定义token服务对象(token校验就应该靠token服务对象)
        RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        // 校验端点/接⼝设置
        remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
        remoteTokenServices.setClientId("client_lagou");
        remoteTokenServices.setClientSecret("abcxyz");
        resources.tokenServices(remoteTokenServices);
    }

    /**
     * 场景:⼀个服务中可能有很多资源(API接⼝)
     * 某⼀些API接⼝,需要先认证,才能访问
     * 某⼀些API接⼝,压根就不需要认证,本来就是对外开放的接⼝
     * 我们就需要对不同特点的接⼝区分对待(在当前configure方法中完成),设置是否需要经过认证
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http
                // 设置session的创建策略(根据需要创建即可)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .authorizeRequests()
                .antMatchers("/autoDeliver/**").authenticated()// autodeliver为前缀的请求需要认证
                .antMatchers("/demo/**").authenticated()// demo为前缀的请求需要认证
                .anyRequest().permitAll();// 其他请求不认证
    }
}

(三)测试

1、准备

在资源服务器项目上,添加几个controller以便测试。其路径需要对应配置类中配置的url

微服务 协议 微服务对外开放的协议_微服务 协议_08

微服务 协议 微服务对外开放的协议_ide_09

2、测试

首先,没有传递token的时候

微服务 协议 微服务对外开放的协议_ide_10


微服务 协议 微服务对外开放的协议_微服务 协议_11


微服务 协议 微服务对外开放的协议_ide_12

添加token之后

对于需要验证的两个接口,添加access_token(使用获取token的接口)之后

微服务 协议 微服务对外开放的协议_spring_13


微服务 协议 微服务对外开放的协议_微服务 协议_14

3、注意

添加了fallback的时候,直接进入了fallback,所以就去掉了这个

微服务 协议 微服务对外开放的协议_oauth2_15