认证:验证用户的合法身份,比如输入用户名和密码,系统会在后台验证用户名和密码是否合法,合法的前提下,才能够进行后续的操作,访问受保护的资源

微服务架构下统一认证场景

分布式系统的每个服务都会有认证需求,如果每个服务都实现一套认证逻辑会非常冗余,考虑分布式系统共享性的特点,需要由独立的认证服务处理系统认证的请求。
图片.png

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

  • 基于Session的认证方式
    在分布式的环境下,基于session的认证会出现一个问题,每个应用服务都需要在session中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用服务需要将session信息带过去,否则会重新认证。我们可以使用Session共享、Session黏贴等方案。
    Session方案也有缺点,比如基于cookie,移动端不能有效使用等
  • 基于token的认证方式
    基于token的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把token 存在任意地方,并且可以实现web和app统一认证机制。其缺点也很明显,token由于自包含信息,因此一般数据量较大,而且每次请求 都需要传递,因此比较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理负担。

    OAuth2开放授权协议/标准

    OAuth2介绍

    OAuth(开放授权)是一个开放协议/标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。
    允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容
    结合“很多网站无需注册可直接使用QQ登录”这个场景拆分理解上述那句话
    用户:我们自己
    第三方应用:很多网站
    另外的服务提供者:QQ
    OAuth2是OAuth协议的延续版本,但不向后兼容OAuth1即完全废止了OAuth1。

    OAuth2协议⻆色和流程

    网站要开发使用QQ登录这个功能的话,是需要提前到QQ平台进行登记的(否则QQ凭什么陪你玩授权登录这件事)
    1)网站——登记——>QQ平台
    2)QQ平台会颁发一些参数给网站,后续上线进行授权登录的时候(刚才打开授权⻚面)需要携带这些参数
    client_id :客户端id(QQ最终相当于一个认证授权服务器,网站就相当于一个客户端了,所以会给一个客户端id),相当于账号
    secret:相当于密码
    图片.png

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

    什么情况下需要使用OAuth2

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

    OAuth2的颁发Token授权方式

    **1. 授权码(authorization-code)

  • 密码式(password)提供用户名+密码换取token令牌**
  • 隐藏式(implicit)
  • 客户端凭证(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构建微服务统一认证服务思路

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

    搭建认证服务器(Authorization Server)

    认证服务器(Authorization Server),负责颁发token

  • 新建项目cloud-oauth-server-9999
  • pom.xml

    <dependencies>
    <!--导入Eureka Client依赖-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eurekaclient</artifactId>
    </dependency>
    <!--导入spring cloud oauth2依赖-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-oauth2</artifactId>
      <!--排除spring-security低版本的oauth2-autoconfigure依赖-->
      <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>
    </dependencies>
  • application.yml(配置文件无特别之处)
  • 入口类无特殊之处
  • 认证服务器配置类

    package com.cloud.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 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()
             // 添加一个client配置,指定其client_id
             .withClient("client_cloud") 
             // 指定客户端的密码/安全码
             .secret("abcxyz") 
             // 指定客户端所能访问资源id清单,此处的资源id是需要在具体的资源服务器上也配置一样
             .resourceIds("consumer") 
             // 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发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
              // 指定token的存储方法
              .tokenStore(tokenStore()) 
              // token服务的一个描述,可以认为是token生成细节的描述,比如有效时间多少等
              .tokenServices(authorizationServerTokenServices()) 
              // 指定认证管理器,随后注入一个到当前类使用即可
              .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个小时)
      // access_token就是我们请求资源需要携带的令牌
      defaultTokenServices.setAccessTokenValiditySeconds(20); 
      // 设置刷新令牌的有效时间
      defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
      return defaultTokenServices;
    }
    
    }

    关于三个configure方法:

  • configure(ClientDetailsServiceConfigurer clients)
    用来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息
  • configure(AuthorizationServerEndpointsConfigurer endpoints)
    用来配置令牌(token)的访问端点和令牌服务(token services)
  • configure(AuthorizationServerSecurityConfigurer oauthServer)
    用来配置令牌端点的安全约束。

关于 TokenStore:

  1. InMemoryTokenStore
    默认采用,它可以完美的工作在单服务器上(即访问并发量 压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。
  2. JdbcTokenStore
    这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时,你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的classpath当中。
  3. JwtTokenStore
    这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息,JwtTokenStore 不会保存任何数据。
  4. 认证服务器安全配置类

    package com.cloud.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cglib.proxy.NoOp;
    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;
    
  5. 该配置类,主要处理用户名和密码的校验等事宜
    */
    @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","123456",new ArrayList<>());
      auth.inMemoryAuthentication()
      .withUser(user)
      .passwordEncoder(passwordEncoder);
      }
      }
  6. 测试(这里用的postman)
    获取token:http://localhost:9999/oauth/token?client_secret=abcxyz&grant_type=password&username=admin&password=123456&client_id=client_cloud
    参数说明:
    endpoint:/oauth/token
    获取token携带的参数:
    1.client_id:客户端id
    2.client_secret:客户单密码
    3.grant_type:指定使用哪种颁发类型,password
    4.username:用户名
    5.password:密码
    图片.png
    校验token:http://localhost:9999/oauth/check_token?token=a9979518-838c-49ff-b14a-ebdb7fde7d08
    图片.png
    刷新token:http://localhost:9999/oauth/token?grant_type=refresh_token&client_id=client_cloud&client_secret=abcxyz&refresh_token=8b640340-30a3-4307-93d4-ed60cc54fbc8
    图片.png
  7. 资源服务器(希望访问被认证的微服务)Resource Server配置
  8. 资源服务配置类

    package com.cloud.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.jwt.crypto.sign.MacSigner;
    import org.springframework.security.jwt.crypto.sign.RsaVerifier;
    import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
    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;
    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;
    
    @Configuration
    // 开启资源服务器功能
    @EnableResourceServer 
    // 开启web访问安全
    @EnableWebSecurity 
    public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
    
      // jwt签名密钥
      private String sign_key = "cloud123"; 
    
      /*
    * 该方法用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜
    * @param resources
    * @throws Exception
    */
      @Override
      public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
     // 设置当前资源服务的资源id
     resources.resourceId("consumer");
     // 定义token服务对象(token校验就应该靠token服务对象)
     RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
     // 校验端点/接口设置
     remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
     // 携带客户端id和客户端安全码
     remoteTokenServices.setClientId("client_cloud");
     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()
         // consumer为前缀的请求需要认证
         .antMatchers("/consumer/**").authenticated() 
         // demo为前缀的请求需要认证
         .antMatchers("/demo/**").authenticated() 
         // 其他请求不认证
         .anyRequest().permitAll();      
      }
    }

    思考:当我们第一次登陆之后,认证服务器颁发token并将其存储在认证服务器中,后期我们访问资源服务器时会携带token,资源服务器会请求认证服务器验证token有效性,如果资源服务器有很多,那么认证服务器压力会很大。
    另外,资源服务器向认证服务器check_token,获取的也是用户信息UserInfo,能否把用户信息存储到令牌中,让客户端一直持有这个令牌,令牌的验证也在资源服务器进行,这样避免和认证服务器频繁的交互。
    我们可以考虑使用 JWT 进行改造,使用JWT机制之后资源服务器不需要访问认证服务器。

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

    JWT令牌介绍
    通过上边的测试我们发现,当资源服务和授权服务不在一起时资源服务使用RemoteTokenServices远程请求授权服务验证token,如果访问量较大将会影响系统的性能。
    解决上边问题:令牌采用JWT格式即可解决上边的问题,用户认证通过会得到一个JWT令牌,JWT令牌中已经包括了用户相关的信 息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。
    1)什么是JWT?
    JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于 在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公 钥/私钥对来签名,防止被篡改。
    2)JWT令牌结构
    JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz

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

    {
      "alg": "HS256",
      "typ": "JWT"
    }

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

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

    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
  11. 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 令牌服务方法
    图片.png
    资源服务器校验JWT令牌
    不需要和远程认证服务器交互,添加本地tokenStore

    package com.cloud.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.jwt.crypto.sign.MacSigner;
    import org.springframework.security.jwt.crypto.sign.RsaVerifier;
    import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
    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;
    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;
    
    @Configuration
    // 开启资源服务器功能
    @EnableResourceServer 
    // 开启web访问安全
    @EnableWebSecurity 
    public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
    
      // jwt签名密钥
      private String sign_key = "cloud123"; 
    
      /*
    * 该方法用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜
    * @param resources
    * @throws Exception
    */
      @Override
      public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
     /*// 设置当前资源服务的资源id
     resources.resourceId("consumer");
     // 定义token服务对象(token校验就应该靠token服务对象)
     RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
     // 校验端点/接口设置
     remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
     // 携带客户端id和客户端安全码
     remoteTokenServices.setClientId("client_cloud");
     remoteTokenServices.setClientSecret("abcxyz");
     // 别忘了这一步
     resources.tokenServices(remoteTokenServices);*/
    
     // jwt令牌改造
     resources.resourceId("consumer").tokenStore(tokenStore()).stateless(true);// 无状态设置
      }
    
      /*
    * 场景:一个服务中可能有很多资源(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()
         // consumer为前缀的请求需要认证
         .antMatchers("/consumer/**").authenticated() 
         // demo为前缀的请求需要认证
         .antMatchers("/demo/**").authenticated() 
         // 其他请求不认证
         .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;
      }
    }

    从数据库加载Oauth2客户端信息

  12. 创建数据表并初始化数据(表名及字段保持固定)

    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_cloud123','consumer', 'abcxyz', 'all', 'password,refresh_token', NULL, NULL, 7200, 259200, NULL, NULL);
    COMMIT;
    SET FOREIGN_KEY_CHECKS = 1;
  13. 配置数据源

    Spring:
      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: 123456
     druid:
       initialSize: 10
       minIdle: 10
       maxActive: 30
       maxWait: 50000
  14. 认证服务器主配置类改造

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

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

    @Bean
    public JdbcClientDetailsService createJdbcClientDetailsService() {
    JdbcClientDetailsService jdbcClientDetailsService = new
    JdbcClientDetailsService(dataSource);
    return jdbcClientDetailsService;
    }

  21. 创建数据表users(表名不需固定),初始化数据

    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, 'cloud-user', 'iuxyzds');
    COMMIT;
    SET FOREIGN_KEY_CHECKS = 1;
  22. 操作数据表的JPA配置以及针对该表的操作的Dao接口此处省略
  23. 开发UserDetailsService接口的实现类,根据用户名从数据库加载用户信息

    package com.cloud.service;
    
    import com.cloud.dao.UsersRepository;
    import com.cloud.pojo.Users;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    
    import java.util.ArrayList;
    
    @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<>());
      }
    }
  24. 使用自定义的用户详情服务对象

    @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);
    }

    基于Oauth2的 JWT 令牌信息扩展

    OAuth2帮我们生成的JWT令牌载荷部分信息有限,关于用户信息只有一个user_name,有些场景下我们希望放入一些扩展信息项,比如,之前我们经常向session中存入userId,或者现在我希望在JWT的载荷部分存入当时请求令牌的客户端IP,客户端携带令牌访问资源服务时,可以对比当前请求的客户端真实IP和令牌中存放的客户端IP是否匹配,不匹配拒绝请求,以此进一步提高安全性。那么如何在OAuth2环境下向JWT令牌中存如扩展信息?

  25. 认证服务器生成JWT令牌时存入扩展信息(比如clientIp)
    继承DefaultAccessTokenConverter类,重写convertAccessToken方法存入扩展信息

    package com.cloud.config;
    
    import org.springframework.security.oauth2.common.OAuth2AccessToken;
    import org.springframework.security.oauth2.provider.OAuth2Authentication;
    import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    import javax.servlet.http.HttpServletRequest;
    
    import java.util.Map;
    
    @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);
     return stringMap;
      }
    }

    将自定义的转换器对象注入
    图片.png

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

    资源服务器也需要自定义一个转换器类,继承DefaultAccessTokenConverter,重写extractAuthentication提取方法,把载荷信息设置到认证对象的details属性中

    package com.cloud.config;
    
    import org.springframework.security.oauth2.common.OAuth2AccessToken;
    import org.springframework.security.oauth2.provider.OAuth2Authentication;
    import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.Map;
    
    @Component
    public class AccessTokenConvertor extends DefaultAccessTokenConverter {
    
      @Override
      public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
     OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
     // 将map放入认证对象中,认证对象在controller中可以拿到
     oAuth2Authentication.setDetails(map); 
     return oAuth2Authentication;
      }
    }

    将自定义的转换器对象注入
    图片.png
    业务类比如Controller类中,可以通过SecurityContextHolder.getContext().getAuthentication()获取到认证对象,进一步获取到扩展信息

    Object details = SecurityContextHolder.getContext().getAuthentication().getDetails();

    获取到扩展信息后,就可以做其他的处理了,比如根据userId进一步处理,或者根据clientIp处理,或者其他都是可以的了

    其他

    关于JWT令牌我们需要注意

  26. JWT令牌就是一种可以被验证的数据组织格式,它的玩法很灵活,我们这里是基于Spring Cloud Oauth2 创建、校验JWT令牌
  27. 我们也可以自己写工具类生成、校验JWT令牌
  28. JWT令牌中不要存放过于敏感的信息,因为我们知道拿到令牌后,我们可以解码看到载荷部分的信息
  29. JWT令牌每次请求都会携带,内容过多,会增加网络带宽占用

标签: OAuth2, JWT, Spring Cloud

评论已关闭