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

传统的服务认证计划有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渠道进⾏挂号注册的;

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

大致流程是:当咱们运用 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构建微服务统⼀认证服务思路

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

在咱们统⼀认证的场景中,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

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

参数阐明

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是否能够拜访

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

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

装备资源服务装备类

装备需求检验的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拜访的时分 直接会报没有认证权限

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

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

当咱们带着token之后能够看到 正确回来信息

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

一个接口恳求认证校验流程在认证服务和资源服务之间测验流程完毕
由于涉及到微服务,此刻咱们的微服务只有一个认证个一个资源服务,在分布式架构中,资源服务往往有很多个,认证服务器恳求压力剧增,占用很多资源,验证可能导致认证服务呈现故障,终究导致咱们服务甚至不可用。

假如咱们将认证服务器生成的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令牌格局

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

运用令牌恳求接口,恳求成功

微服务下统一认证解决方案 Spring Cloud OAuth2 + 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;
    }

能够看到这儿的客户端装备概况 有两个子类

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

clients.withClientDetails(createJdbcClientDetailsService()); 咱们运用从数据库中查询,不适用内存的方法。

在子类完结类中能够看到 查询语句

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

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

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

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

db的信息

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

能够看到恳求令牌成功

恳求服务

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

默许状况下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;
    }
}

将此组件增加到扩展器中

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

一起资源服务器取出 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字段。

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

在没验证的状况也能够看到客户端的信息,所以不要放灵敏信息

然后将其增加到网关层运用

#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

调用 能够正常回来令牌

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