微服务下统一认证解决方案 Spring Cloud OAuth2 + JWT

微服务架构下统⼀认证思路

传统的服务认证方案有Session(基于cookie),以及token等方案。

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

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

微服务下统一认证解决方案 Spring Cloud OAuth2 + JWT

OAuth2开放授权协议/标准

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

允许⽤户授权第三⽅应⽤访问他们存储在另外的服务提供者上的信息,⽽不需要将⽤户名和密码提供给第三⽅应⽤或分享他们数据的所有内容

OAuth2协议⻆⾊和流程

Boss直聘需要开发使⽤QQ登录这个功能、Boss直聘是需要到QQ平台进⾏登记注册的;

在这里插入图片描述

大致流程是:当我们使用 Boss直聘,开始登陆,然后请求到QQ平台,QQ通过之后将一些参数返回给Boss,然后进行授权登陆,会涉及到这么几个参数。

client_id :客户端id(QQ 相当于⼀个认证授权服务器,Boss就相当于⼀个客户端了,所以会给⼀个客户端id),相当于账号,secret:相当于密码。

  • 资源所有者(Resource Owner):可以理解为⽤户⾃⼰
  • 客户端(Client):我们想登陆的⽹站或应⽤,⽐如boss,一些认证登录的网站。
  • 认证服务器(Authorization Server):可以理解为微信或者QQ
  • 资源服务器(Resource Server):可以理解为微信或者QQ

什么情况下需要使⽤OAuth2?

一些第三方登陆场景: 我们不想注册自己账号,比如可以微信,QQ等授权登陆,是典型的 OAuth2 使⽤场景。

单点登录的场景:如果项⽬中有很多微服务或者公司内部有很多服务,可以专⻔做⼀个认证中⼼(充当认证平台⻆⾊),所有的服务都要到这个认证中⼼做认证,只做⼀次登录,就可以在多个授权范围内的服务中⾃由串⾏。

OAuth2的颁发Token授权⽅式

  • 1)授权码(authorization-code)
  • 2)密码式(password)提供⽤户名+密码换取token令牌
  • 3)隐藏式(implicit)
  • 4)客户端凭证(client credentials)

授权码模式使⽤到了回调地址,是最复杂的授权⽅式,微博、微信、QQ等第三⽅登录就是这种模式。

微服务这儿使用接⼝对接中常使⽤的password密码模式(提供⽤户名+密码换取token)。

Spring Cloud OAuth2 + JWT 实现

Spring Cloud OAuth2是什么?

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

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

Spring Cloud OAuth2构建微服务统⼀认证服务思路
在这里插入图片描述

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

具体操作需要搭建一个认证服务,主要用来通过用户的一些信息,生成token,颁发token给客户端,
一个资源服务,用来对外提供资源访问,同时像认证服务去请求校验所携带的token令牌。

新建认证服务 cloud-oauth-server-9999

引入依赖

 <!--导⼊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>

        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.4.RELEASE</version>
        </dependency>
复制代码

yml配置 将其注册到注册中心

#eureka server服务端口
server:
  port: 9999
spring:
  application:
    name: cloud-oauth-server-9999 # 应用名称,应用名称会在Eureka中作为服务名称

  cloud:
    inetutils:
      # 指定此客户端的ip
      default-ip-address: springcloud


#eureka配置
eureka:
  instance:
    hostname: springcloud  # 当前eureka实例的主机名
    ip-address: springcloud
  client:
    service-url:
      prefer-ip-address: true
      lease-renewal-interval-in-seconds: 30
      # 租约到期,服务时效时间,默认值90秒,服务超过90秒没有发⽣⼼跳,服务注册中心会将服务从列表移除
      lease-expiration-duration-in-seconds: 90
      # 配置客户端所交互的Eureka Server的地址(Eureka Server集群中每一个Server其实相对于其它Server来说都是Client)
      # 集群模式下,defaultZone应该指向其它Eureka Server,如果有更多其它Server实例,逗号拼接即可
      defaultZone: http://127.0.0.1:8761/eureka,http://127.0.0.1:8762/eureka # 注册到集群汇总,多个用,拼接
    register-with-eureka: true  # 集群模式下可以改成true
    fetch-registry: true # 集群模式下可以改成true
复制代码

新建我们的认证服务配置

这儿需要继承父类AuthorizationServerConfigurerAdapter然后重写配置

package com.udeam;


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)
 */
@EnableAuthorizationServer //开启认证服务器功能
@Configuration
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平台,boss⽹作为客户端需要qq平台进⾏登录授权认证等,提前需要到QQ平台注册,QQ平台会给boss⽹
     * 颁发client_id等必要参数,表明客户端是谁
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer
                                  clients) throws Exception {
        super.configure(clients);
        clients.inMemory()// 客户端信息存储在什么地⽅,可以在内存中,可 以在数据库⾥
                .withClient("client_test") // 添加⼀个client配 置,指定其client_id
                .secret("abcxyz") // 指定客户端 的密码 / 安全码
                .resourceIds("cloud-oauth-server-9998") // 指定客户端 所能访问资源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(200); //这儿设置200s
        // access_token就是我们请求资源需要携带的令牌
        // 设置刷新令牌的有效时间
        defaultTokenServices.setRefreshTokenValiditySeconds(259200); //3 天
        return defaultTokenServices;
    }
}
复制代码
关于三个configure⽅法
  • configure(ClientDetailsServiceConfigurer clients)

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

  • configure(AuthorizationServerEndpointsConfigurer endpoints)

⽤来配置令牌(token)的访问端点和令牌服务(token services)

  • configure(AuthorizationServerSecurityConfigureroauthServer)

⽤来配置令牌端点的安全约束.

关于 TokenStore
  • InMemoryTokenStore

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

  • JdbcTokenStore

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

  • JwtTokenStore

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

配置生成验证用户名和生成token的配置

package com.udeam;

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 {
    /**
     * 注册⼀个认证管理器对象到容器
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean()
            throws Exception {
        return super.authenticationManagerBean();
    }

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

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 处理⽤户名和密码验证事宜
     * 1)客户端传递username和password参数 到认证服务器
     * 2)⼀般来说,username和password会存储在数据库中的⽤户表中
     * 3)根据⽤户表中数据,验证当前传递过来的⽤户信息的合法性
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth)
            throws Exception {

        // 在这个⽅法中就可以去关联数据库了,当前我们先把⽤户信息配置在内存中
        // 实例化⼀个⽤户对象(相当于数据表中的⼀条⽤户记录)
        UserDetails user = new User("admin", "admin", new
                ArrayList<>());
        auth.inMemoryAuthentication()
                .withUser(user).passwordEncoder(passwordEncoder);
    }
}
复制代码

这儿先写死配置,以及将生成的token存放在内存中。

测试

获取token:

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

在这里插入图片描述

参数说明

endpoint:/oauth/token

获取token携带的参数

  • client_id:客户端id
  • client_secret:客户单密码
  • grant_type:指定使⽤哪种颁发类型,password
  • username:⽤户名
  • password:密码

校验token:

http://localhost:9999/oauth/check_token?token=a9979518-838c-49ff-b14a-ebdb7fde7d08

刷新token:

http://localhost:9999/oauth/token?grant_type=refresh_token&client_id=client_lagou&client_secret=abcxyz&refresh_token=8b640340-30a3-4307-93d4-ed60cc54fbc8

新建资源服务器ResourceTestServer

希望访问被认证的微服务
Resource Server配置

pom和认证的一样

yml基本也一样

这儿我们提供/api/test 以及 /demo/test接口用来测试携带token是否可以访问

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

配置资源服务配置类

配置需要检验的url
配置token认证的服务

package com;

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 = "test123"; // jwt签名密钥

    /**
     * 该⽅法⽤于定义资源服务器向远程认证服务器发起请求,进⾏token校验
     * 等事宜
     *
     * @param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer
                                  resources) throws Exception {
            // 设置当前资源服务的资源id 与认证id保持一致  可以不设置
        resources.resourceId("cloud-oauth-server-9998");

        // 定义token服务对象(token校验就应该靠token服务对象)
        RemoteTokenServices remoteTokenServices = new
                RemoteTokenServices();
        // 校验端点/接⼝设置
        remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
        // 携带客户端id和客户端安全码
        remoteTokenServices.setClientId("client_test");
        remoteTokenServices.setClientSecret("abcxyz");
        // 别忘了这⼀步
        resources.tokenServices(remoteTokenServices);
    }

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

测试

当我们不携带token访问的时候 直接会报没有认证权限

在这里插入图片描述

在这里插入图片描述

当我们携带token之后可以看到 正确返回信息

在这里插入图片描述

一个接口请求认证校验流程在认证服务和资源服务之间测试流程结束
因为涉及到微服务,此时我们的微服务只有一个认证个一个资源服务,在分布式架构中,资源服务往往有很多个,认证服务器请求压力剧增,占用很多资源,验证可能导致认证服务出现故障,最终导致我们服务甚至不可用。

如果我们将认证服务器生成的token让客户端存储,每次请求避免与认证服务器交互。

JWT改造统⼀认证授权中⼼的令牌存储机制

JWT令牌介绍

  • 什么是JWT?

JSON Web Token(JWT)是⼀个开放的⾏业标准(RFC 7519),它定义了⼀种简介
的、⾃包含的协议格式,⽤于 在通信双⽅传递json对象,传递的信息经过数字签名
可以被验证和信任。JWT可以使⽤HMAC算法或使⽤RSA的公 钥/私钥对来签名,防
⽌被篡改。

  • JWT令牌结构

JWT令牌由三部分组成,每部分中间使⽤点(.)分隔,⽐如:xxxxx.yyyyy.zzzzz
Header

头部包括令牌的类型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA),例如

"alg": "HS256",
"typ": "JWT"
}
复制代码

将上边的内容使⽤Base64Url编码,得到⼀个字符串就是JWT令牌的第⼀部分。

  • Payload

第⼆部分是负载,内容也是⼀个json对象,它是存放有效信息的地⽅,它可以存
放jwt提供的现成字段,⽐ 如:iss(签发者),exp(过期时间戳), sub(⾯向的
⽤户)等,也可⾃定义字段。 此部分不建议存放敏感信息,因为此部分可以解
码还原原始内容。 最后将第⼆部分负载使⽤Base64Url编码,得到⼀个字符串
就是JWT令牌的第⼆部分。 ⼀个例⼦:

{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
复制代码
  • Signature

第三部分是签名,此部分⽤于防⽌jwt内容被篡改。 这个部分使⽤base64url将
前两部分进⾏编码,编码后使⽤点(.)连接组成字符串,最后使⽤header中声
明 签名算法进⾏签名。

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
base64UrlEncode(header):jwt令牌的第⼀部分。
base64UrlEncode(payload):jwt令牌的第⼆部分。
secret:签名所使⽤的密钥。
认证服务器端JWT改造(改造主配置类)
/*
该⽅法⽤于创建tokenStore对象(令牌存储对象)
token以什么形式存储
*/
public TokenStore tokenStore(){
//return new InMemoryTokenStore();
// 使⽤jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
 }
/**
* 返回jwt令牌转换器(帮助我们⽣成jwt令牌的)
* 在这⾥,我们可以把签名密钥传递进去给转换器对象
* @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new
JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密
钥
jwtAccessTokenConverter.setVerifier(new
MacSigner(sign_key)); // 验证时使⽤的密钥,和签名密钥保持⼀致
return jwtAccessTokenConverter;
 }
复制代码

认证服务器端JWT 代码改造

将生成token的代码进行改造,添加jwt

package com.jwt;


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.jwt.crypto.sign.MacSigner;
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;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

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

    @Autowired
    private AuthenticationManager authenticationManager;


    private String sign_key = "test123";

    /**
     * 认证服务器最终是以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
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer
                                  clients) throws Exception {
        super.configure(clients);
        clients.inMemory()// 客户端信息存储在什么地⽅,可以在内存中,可 以在数据库⾥
                .withClient("client_test") // 添加⼀个client配 置,指定其client_id
                .secret("abcxyz") // 指定客户端 的密码 / 安全码
                .resourceIds("cloud-oauth-server-9998") // 指定客户端 所能访问资源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();
        // 使⽤jwt令牌
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * 返回jwt令牌转换器(帮助我们⽣成jwt令牌的)
     * 在这⾥,我们可以把签名密钥传递进去给转换器对象
     * @return
     */
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new
                JwtAccessTokenConverter();

        jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
        jwtAccessTokenConverter.setVerifier(new
                MacSigner(sign_key)); // 验证时使⽤的密钥,和签名密钥保持⼀致
        return jwtAccessTokenConverter;
    }


    /**
     * 该⽅法⽤户获取⼀个token服务对象(该对象描述了token有效期等信息)
     */
    public AuthorizationServerTokenServices  authorizationServerTokenServices() {
        // 使⽤默认实现
        DefaultTokenServices defaultTokenServices = new
                DefaultTokenServices();

        /**
         * 添加使用jwt令牌
         */
        defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());

        defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
        defaultTokenServices.setTokenStore(tokenStore());
        // 设置令牌有效时间(⼀般设置为2个⼩时)
        defaultTokenServices.setAccessTokenValiditySeconds(200);
        // access_token就是我们请求资源需要携带的令牌
        // 设置刷新令牌的有效时间
        defaultTokenServices.setRefreshTokenValiditySeconds(259200); //3 天
        return defaultTokenServices;
    }
}
复制代码

资源服务

资源服务器校验JWT令牌,不需要和远程认证服务器交互,添加本地tokenStore。


package com;

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.jwt.crypto.sign.MacSigner;
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.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;


/**
 *
 * 资源服务配置类 使用jwt 校验
 */
@Configuration
@EnableResourceServer // 开启资源服务器功能
@EnableWebSecurity // 开启web访问安全
public class ResourceServerConfigerForJWT extends ResourceServerConfigurerAdapter {


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

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

        // 设置当前资源服务的资源id 与认证id保持一致  可以不设置
        resources.resourceId("cloud-oauth-server-9998")
                .tokenStore(tokenStore())
                .stateless(true);// ⽆状态设置
    }

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






    /**
    该⽅法⽤于创建tokenStore对象(令牌存储对象)token以什么形式存储
    */
    public TokenStore tokenStore(){
        //return new InMemoryTokenStore();
        // 使⽤jwt令牌
        return new JwtTokenStore(jwtAccessTokenConverter());
    }


    /**
     * 返回jwt令牌转换器(帮助我们⽣成jwt令牌的)
     * 在这⾥,我们可以把签名密钥传递进去给转换器对象
     * @return
     */
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new
                JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
        jwtAccessTokenConverter.setVerifier(new
                MacSigner(sign_key)); // 验证时使⽤的密钥,和签名密钥保持⼀致
        return jwtAccessTokenConverter;
    }
}
复制代码

需要注意的是认证和资源端的秘钥需要保持一致

测试

我们再请求认证服务器发现返回的token是 jwt令牌格式

在这里插入图片描述

使用令牌请求接口,请求成功
在这里插入图片描述

一般开发中我们的用户信息,以及客户端信息都是从数据库进行查询的。

客户端详情配置
  @Override
    public void configure(ClientDetailsServiceConfigurer
                                  clients) throws Exception {
        super.configure(clients);

        //从数据库中加载客户端详情
        clients.withClientDetails(createJdbcClientDetailsService());
    }


    /**
     * 从数据库中加载客户端详情
     * @return
     */
    @Bean
    public JdbcClientDetailsService createJdbcClientDetailsService() {
        JdbcClientDetailsService jdbcClientDetailsService = new
                JdbcClientDetailsService(dataSource); //需要注入DataSource
        return jdbcClientDetailsService;
    }
复制代码

可以看到这儿的客户端配置详情 有两个子类
在这里插入图片描述
clients.withClientDetails(createJdbcClientDetailsService()); 我们使用从数据库中查询,不适用内存的方式。

在子类实现类中可以看到 查询语句

在这里插入图片描述

在这里插入图片描述
this.selectClientDetailsSql 完整的Sql如下

 "select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?";
   
复制代码

可以看到是通过client_idoauth_client_details表中查询,字段有以上select后面那些。

我们新建表oauth_client_details

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
 `client_id` varchar(48) NOT NULL,
 `resource_ids` varchar(256) DEFAULT NULL,
 `client_secret` varchar(256) DEFAULT NULL,
 `scope` varchar(256) DEFAULT NULL,
 `authorized_grant_types` varchar(256) DEFAULT NULL,
 `web_server_redirect_uri` varchar(256) DEFAULT NULL,
 `authorities` varchar(256) DEFAULT NULL,
 `access_token_validity` int(11) DEFAULT NULL,
 `refresh_token_validity` int(11) DEFAULT NULL,
 `additional_information` varchar(4096) DEFAULT NULL,
 `autoapprove` varchar(256) DEFAULT NULL,
 PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
BEGIN;
INSERT INTO `oauth_client_details` VALUES ('client_test',
'test,resume', 'abcxyz', 'all', 'password,refresh_token',
NULL, NULL, 7200, 259200, NULL, NULL);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
复制代码

resource_ids 多个服务可以使用逗号进行拼接

然后代码会自动为我们进行查询匹配的。

用户查询配置

用户表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `username` char(10) DEFAULT NULL,
 `password` char(100) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of users
-- ----------------------------
BEGIN;
INSERT INTO `users` VALUES (4, 'admin', 'iuxyzds');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
复制代码

用户代码查询

在认证配置SecurityConfiger 类中


    @Autowired
    private JdbcUserDetailsService jdbcUserDetailsService;

    /**
     * 处理⽤户名和密码验证事宜
     * 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);*/
        auth.userDetailsService(jdbcUserDetailsService).passwordEncoder(passwordEncoder);
    }
复制代码

这儿我们使用jpa 根据用户名从db查询 用户详情。

serives


@Service
public class JdbcUserDetailsService implements UserDetailsService {

    @Autowired
    private UsersRepository usersRepository;

    /**
     * 根据username查询出该用户的所有信息,封装成UserDetails类型的对象返回,至于密码,框架会自动匹配
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Users users = usersRepository.findByUsername(username);
        return new User(users.getUsername(),users.getPassword(),new ArrayList<>());
    }
}
复制代码

dao


public interface UsersRepository extends JpaRepository<Users,Long> {

    Users findByUsername(String username);
}
复制代码

依赖


        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--操作数据库需要事务控制-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>


        <!-- jpa-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
复制代码

yml配置

spring:
  application:
    name: cloud-oauth-server-9999 # 应用名称,应用名称会在Eureka中作为服务名称

  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/oauth2?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
    username: root
    password: root
    druid:
      initialSize: 10
      minIdle: 10
      maxActive: 30
      maxWait: 50000
复制代码

测试一下

首先是认证服务获取token

在这里插入图片描述
db的信息

在这里插入图片描述
可以看到请求令牌成功

请求服务

在这里插入图片描述

默认情况下jwt令牌存储的客户端信息是特定的,如果我们想额外增加信息呢,就需要扩展jwt信息。

认证服务器⽣成JWT令牌时存⼊扩展信息(⽐如clientIp)

继承DefaultAccessTokenConverter类,重写convertAccessToken⽅法存⼊扩展信息



/**
 * 扩展jwt令牌 存放客户信息
 */
@Component
public class AccessTokenConvertor extends DefaultAccessTokenConverter {
    @Override
    public Map<String, ?> convertAccessToken(OAuth2AccessToken
                                                     token, OAuth2Authentication authentication) {
        // 获取到request对象
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest();
        // 获取客户端ip(注意:如果是经过代理之后到达当前服务的话,那么这种⽅式获取的并不是真实的浏览器客户端ip)
        String remoteAddr = request.getRemoteAddr();
        Map<String, String> stringMap = (Map<String, String>)
                super.convertAccessToken(token, authentication);
        stringMap.put("clientIp", remoteAddr); //扩展客户端ip 等信息
        return stringMap;
    }
}
复制代码

将此组件添加到扩展器中
在这里插入图片描述

同时资源服务器取出 JWT 令牌扩展信息

也需要⾃定义⼀个转换器类,继承DefaultAccessTokenConverter,重写extractAuthentication提取⽅法,把载荷信息设置到认证对象的details属性中


@Component
public class AccessTokenConvertor extends DefaultAccessTokenConverter {
    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ? > map) {
        OAuth2Authentication oAuth2Authentication =
                super.extractAuthentication(map);
        oAuth2Authentication.setDetails(map); // 将map放⼊认证对象中,认证对象在controller中可以拿到
        return oAuth2Authentication;
    }
}
复制代码

同时也需要将此组件添加到扩展器中,客户端需要校验

然后我们可以通过 Object details = SecurityContextHolder.getContext().getAuthentication().getDetails()代码在业务代码中获取到用户信息。

同理我们请求一下拿到令牌,然后验证一下网站https://jwt.io/
可以看到解析之后我们的客户端信息,以及扩展的ip字段。
在这里插入图片描述
在没验证的情况也可以看到客户端的信息,所以不要放敏感信息

然后将其添加到网关层使用

#eureka server服务端口
server:
  port: 9000
spring:
  application:
    name: server-pruduce-9000-getWay # 应用名称,应用名称会在Eureka中作为服务名称

  cloud:
    inetutils:
      # 指定此客户端的ip
      default-ip-address: springcloud
      #getway配置

      ##### 动态路由设置时,uri以 lb: //开头(lb代表从注册中⼼获取服务),后⾯是需要转发到的服务名称
    gateway:
      routes: # 路由可以有多个
#        - id: service-router # 我们⾃定义的路由 ID,保持唯⼀
#          #uri: http://127.0.0.1:8082 # ⽬标服务地址 部署多实例) 动态路由:uri配置的应该是⼀个服务名称,⽽不应该是⼀个具体的服务实例的地址
#          uri: lb://server-pruduce-8082-feign # ⽬标服务地址 部署多实例) 动态路由:uri配置的应该是⼀个服务名称,⽽不应该是⼀个具体的服务实例的地址
#          # gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
#          predicates: #断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默 认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
#            - Path=/api/**

        - id: cloud-oauth-server-9999 # 我们⾃定义的路由 ID,保持唯⼀
          #uri: http://127.0.0.1:8082 # ⽬标服务地址 部署多实例) 动态路由:uri配置的应该是⼀个服务名称,⽽不应该是⼀个具体的服务实例的地址
          uri: lb://cloud-oauth-server-9999 # ⽬标服务地址 部署多实例) 动态路由:uri配置的应该是⼀个服务名称,⽽不应该是⼀个具体的服务实例的地址
          # gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
          predicates: #断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默 认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
            - Path=/oauth/**


#eureka配置
eureka:
  instance:
    hostname: springcloud  # 当前eureka实例的主机名
    ip-address: springcloud
  client:
    service-url:
      prefer-ip-address: true
      lease-renewal-interval-in-seconds: 30
      # 租约到期,服务时效时间,默认值90秒,服务超过90秒没有发⽣⼼跳,服务注册中心会将服务从列表移除
      lease-expiration-duration-in-seconds: 90
      # 配置客户端所交互的Eureka Server的地址(Eureka Server集群中每一个Server其实相对于其它Server来说都是Client)
      # 集群模式下,defaultZone应该指向其它Eureka Server,如果有更多其它Server实例,逗号拼接即可
      defaultZone: http://127.0.0.1:8761/eureka,http://127.0.0.1:8762/eureka # 注册到集群汇总,多个用,拼接
    register-with-eureka: true  # 集群模式下可以改成true
    fetch-registry: true # 集群模式下可以改成true




# 分布式链路追踪
logging:
  level:
    org.springframework.web.servlet.DispatcherServlet: debug
    org.springframework.cloud.sleuth: debug
复制代码

调用 可以正常返回令牌

在这里插入图片描述

猜你喜欢

转载自juejin.im/post/7231803859889881148