1. 微服务架构下统一认证思路

  • 基于Session的认证方式

在分布式的环境下,基于session的认证会出现一个问题,每个应用服务都需要在session中存储用 户身份信息,通过负载均衡将本地的请求分配到另一个应用服务需要将session信息带过去,否则 会重新认证。我们可以使用Session共享、Session黏贴等方案。
Session方案也有缺点,比如基于cookie,移动端不能有效使用等

  • 基于token的认证方式

基于token的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把token 存在任 意地方,并且可以实现web和app统一认证机制。其缺点也很明显,token由于自包含信息,因此 一般数据量较大,而且每次请求 都需要传递,因此比较占带宽。另外,token的签名验签操作也会 给cpu带来额外的处理负担。

2. OAuth2开放授权协议/标准

2.1 OAuth2介绍

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

结合“使用QQ登录拉勾”这个场景拆分理解上述那句话
用户:我们自己
第三方应用:拉勾网
另外的服务提供者:QQ
OAuth2是OAuth协议的延续版本,但不向后兼容OAuth1即完全废止了OAuth1。

2.2 OAuth2协议⻆色和流程

拉勾网要开发使用QQ登录这个功能的话,那么拉勾网是需要提前到QQ平台进行登记的

  1. 拉勾网——登记——>QQ平台
  2. QQ 平台会颁发一些参数给拉勾网,后续上线进行授权登录的时候(刚才打开授权⻚面)需要携带下面这些参数

client_id :客户端id(QQ最终相当于一个认证授权服务器,拉勾网就相当于一个客户端了,所以会给一个客户端id),相当于账号
secret:相当于密码

流程图如下:

在这里插入图片描述

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

第三方授权登录的场景:比如,我们经常登录一些网站或者应用的时候,可以选择使用第三方授权登录 的方式,比如:微信授权登录、QQ授权登录、微博授权登录等,这是典型的 OAuth2 使用场景。
单点登录的场景:如果项目中有很多微服务或者公司内部有很多服务,可以专⻔做一个认证中心(充当 认证平台⻆色),所有的服务都要到这个认证中心做认证,只做一次登录,就可以在多个授权范围内的 服务中自由串行。

2.4 OAuth2的颁发Token授权方式
  1. 授权码(authorization-code)
  2. 密码式(password)提供用户名+密码换取token令牌
  3. 隐藏式(implicit)
  4. 客户端凭证(client credentials)

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

3. Spring Cloud OAuth2简介及使用

3.1 Spring Cloud OAuth2介绍

Spring Cloud OAuth2 是 Spring Cloud 体系对OAuth2协议的实现,可以用来做多个微服务的统一认证 (验证身份合法性)授权(验证权限)。通过向OAuth2服务(统一认证授权服务)发送某个类型的 grant_type进行集中认证和授权,从而获得access_token(访问令牌),而这个token是受其他微服务 信任的。

注意:使用OAuth2解决问题的本质是,引入了一个认证授权层,认证授权层连接了资源的拥有者,在 授权层里面,资源的拥有者可以给第三方应用授权去访问我们的某些受保护资源。

3.2 Spring Cloud OAuth2构建微服务统一认证服务思路

在这里插入图片描述
注意:在我们统一认证的场景中,Resource Server其实就是我们的各种受保护的微服务,微服务中的 各种API访问接口就是资源,发起http请求的浏览器就是Client客户端(对应为第三方应用)

3.3 搭建认证服务器(Authorization Server),负责颁发token
  1. pom.xml
<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>
  1. application.yml
server:
  port: 9999
Spring:
  application:
    name: lagou-cloud-oauth-server
eureka:
  client:
    serviceUrl: # eureka server的路径
      defaultZone: http://lagoucloudeurekaservera:8761/eureka/,http://lagoucloudeurekaserverb:8762/eureka/ #把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表
  instance:
    #使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip)
    prefer-ip-address: true
    #自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
  1. 入口类启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class OauthServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(OauthServerApplication.class,args);
    }
}
  1. 认证服务器配置类
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.*;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

/**
 * 当前类为Oauth2 server的配置类(需要继承特定的父类 AuthorizationServerConfigurerAdapter)
 */
@Configuration
@EnableAuthorizationServer  // 开启认证服务器功能
public class OauthServerConfiger 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()
                // 开启端口/oauth/token_key的访问权限(允许)
                .tokenKeyAccess("permitAll()")
                // 开启端口/oauth/check_token的访问权限(允许)
                .checkTokenAccess("permitAll()");
    }

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


        // 从内存中加载客户端详情

        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以什么形式存储
     */
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

    /**
     * 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
     */
    public AuthorizationServerTokenServices authorizationServerTokenServices() {
        // 使用默认实现
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
        defaultTokenServices.setTokenStore(tokenStore());

        // 设置令牌有效时间(一般设置为2个小时)
        defaultTokenServices.setAccessTokenValiditySeconds(20); // access_token就是我们请求资源需要携带的令牌
        // 设置刷新令牌的有效时间
        defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天

        return defaultTokenServices;
    }
}
  • 关于三个configure方法

configure(ClientDetailsServiceConfigurer clients)
用来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这里进行初始化,你 能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息

configure(AuthorizationServerEndpointsConfigurer endpoints)
用来配置令牌(token)的访问端点和令牌服务(token services)

configure(AuthorizationServerSecurityConfigurer oauthServer)
用来配置令牌端点的安全约束.

  • 关于 TokenStore

InMemoryTokenStore
默认采用,它可以完美的工作在单服务器上(即访问并发量 压力不大的情况下,并且它 在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行 尝试, 你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。

JdbcTokenStore
这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现 时, 你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring- jdbc"这个依赖加入到你的 classpath当中。

JwtTokenStore 这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数 据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),缺 点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息, JwtTokenStore 不会保存任何数据。

  1. 认证服务器安全配置类
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 SecurityConfiger extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

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


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

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

  • endpoint:/oauth/token

  • 获取token携带的参数
    client_id:客户端id
    client_secret:客户单密码
    grant_type:指定使用哪种颁发类型,
    password username:用户名
    password:密码
    在这里插入图片描述

  • 校验token:http://localhost:9999/oauth/check_token?token=db9de3b1-b540-490e-b16f-a6663b3f5e3c
    在这里插入图片描述

  • 刷新token:http://localhost:9999/oauth/token?grant_type=refresh_token&client_id=client_lagou&client_secret=abcxyz&refresh_token=d23df733-5dc0-4bb1-9b3a-8967f423530d

在这里插入图片描述

3.4 资源服务器(希望访问被认证的微服务)Resource Server配置
  1. 自定义资源服务配置类,继承ResourceServerConfigurerAdapter
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 ResourceServerConfiger extends ResourceServerConfigurerAdapter {

    private String sign_key = "lagou123"; // jwt签名密钥

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

        // 设置当前资源服务的资源id
        resources.resourceId("autodeliver");
        // 定义token服务对象(token校验就应该靠token服务对象)
        RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        // 校验端点/接口设置
        remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
        // 携带客户端id和客户端安全码
        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 {
        // 设置session的创建策略(根据需要创建即可)
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .authorizeRequests()
                .antMatchers("/autodeliver/**").authenticated() // autodeliver为前缀的请求需要认证
                .antMatchers("/demo/**").authenticated()  // demo为前缀的请求需要认证
                .anyRequest().permitAll();  //  其他请求不认证
    }

}
  1. 配置文件添加如下代码
oauth2:
  server:
    check-token-url: http://localhost:9999/oauth/check_token
  1. 编写测试类
@RestController
@RequestMapping("/autodeliver")
public class AutodeliverController {

    @GetMapping("/checkState/{userId}")
    public Long findResumeOpenState(@PathVariable Long userId) {
        return userId;
    }
}

@RestController
@RequestMapping("/demo")
public class DemoController {

    @GetMapping("/test")
    public String findResumeOpenState() {
        return "demo/test!";
    }
}

@RestController
@RequestMapping("/others")
public class OthersController {

    @GetMapping("/test")
    public String findResumeOpenState() {
        return "others/test!";
    }
}
  1. 测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 从认证服务器获取一个token,然后带上access_token参数重新请求
    在这里插入图片描述
    在这里插入图片描述
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐