循序渐进之单点登录(4)--分布式系统认证(OAuth2,JWT)

OAuth2参考
主要参考
有状态无状态分析
文章基于SpringCloud,SpringSecurity-OAuth2

1. 选型分析

不同种类的客户端(web端,H5、APP),均采用一致的认证、权限、会话机制,实现统一认证授权。

1.1 基于session的认证方式

在分布式的环境下,基于session的认证会出现一个问题,每个应用服务都需要在session中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用服务需要将session信息带过去,否则会重新认证。

之前实现的是,不用nginx带过去session信息,而是采用cookies的形式,通过cookie记录的牌子去认证中心认证,得到相应的access存在本地;

在这里插入图片描述

这个时候,通常的做法有下面几种:

Session复制:多台应用服务器之间同步session,使session保持一致,对外透明。
Session黏贴:当用户访问集群中某台服务器后,强制指定后续所有请求均落到此机器上。
Session集中存储:将Session存入分布式缓存中,所有服务器应用实例统一从分布式缓存中存取Session。

总体来讲,基于session认证的认证方式,可以更好的在服务端对会话进行控制,且安全性较高。但是,session机制方式基于cookie,在复杂多样的移动客户端上不能有效的使用,并且无法跨域,另外随着系统的扩展需提高session的复制、黏贴及存储的容错性。

2.2 基于token的认证方式

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

在这里插入图片描述

优点:
1、适合统一认证的机制,客户端、一方应用、三方应用都遵循一致的认证机制。
2、token认证方式对第三方应用接入更适合,因为它更开放,可使用当前有流行的开放协议Oauth2.0、JWT等。
3、一般情况服务端无需存储会话信息,减轻了服务端的压力。

在这里插入图片描述

流程描述:
(1)用户通过接入方(应用)登录,接入方采取OAuth2.0方式在统一认证服务(UAA)中认证。
(2)认证服务(UAA)调用验证该用户的身份是否合法,并获取用户权限信息。
(3)认证服务(UAA)获取接入方权限信息,并验证接入方是否合法。
(4)若登录用户以及接入方都合法,认证服务生成jwt令牌返回给接入方,其中jwt中包含了用户权限及接入方权
限。
(5)后续,接入方携带jwt令牌对API网关内的微服务资源进行访问。
(6)API网关对令牌解析、并验证接入方的权限是否能够访问本次请求的微服务。
(7)如果接入方的权限没问题,API网关将原请求header中附加解析后的明文Token,并将请求转发至微服务。
(8)微服务收到请求,明文token中包含登录用户的身份和权限信息。因此后续微服务自己可以干两件事:1,用
户授权拦截(看当前用户是否有权访问该资源)2,将用户信息存储进当前线程上下文(有利于后续业务逻辑随时
获取当前用户信息)

UAA服务、API网关组件职责如下:

1)统一认证服务(UAA)
它承载了OAuth2.0接入方认证、登入用户的认证、授权以及生成令牌的职责,完成实际的用户认证、授权功能。
2)API网关
作为系统的唯一入口,API网关为接入方提供定制的API集合,它可能还具有其它职责,如身份验证、监控、负载均
衡、缓存等。API网关方式的核心要点是,所有的接入方和消费端都通过统一的网关接入微服务,在网关层处理所
有的非业务功能。

2. 什么是OAuth2.0?

OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。

流程:
在这里插入图片描述
OAauth2.0包括以下角色:
1、客户端
本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端(浏
览器端)、微信客户端等。
2、资源拥有者
通常为用户,也可以是应用程序,即该资源的拥有者。
3、授权服务器(也称认证服务器)
用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌(access_token),作为客户端访问资源服务器的凭据。本例为微信的认证服务器。
4、资源服务器
存储资源的服务器,本例子为微信存储的用户信息。
现在还有一个问题,服务提供商能允许随便一个客户端就接入到它的授权服务器吗?答案是否定的,服务提供商会给准入的接入方一个身份,用于接入时的凭据:
client_id:客户端标识
client_secret:客户端秘钥

因此,准确来说,授权服务器对两种OAuth2.0中的两个角色进行认证授权,分别是资源拥有者、客户端。

3. Spring-Security-OAuth2

Spring-Security-OAuth2是对OAuth2的一种实现,并且跟我们之前学习的Spring Security相辅相成,与SpringCloud体系的集成也非常便利,接下来,我们需要对它进行学习,最终使用它来实现我们设计的分布式认证授权解决方案。

OAuth2.0的服务提供方涵盖两个服务,即授权服务 (Authorization Server,也叫认证服务) 和资源服务 (Resource Server),使用 Spring Security OAuth2 的时候你可以选择把它们在同一个应用程序中实现,也可以选择建立使用同一个授权服务的多个资源服务。

3.1 服务端配置

部分主要pom


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-commons</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>

        <dependency>
            <groupId>javax.interceptor</groupId>
            <artifactId>javax.interceptor-api</artifactId>
        </dependency>

授权服务器配置:
ClientDetailsServiceConfigurer 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService), ClientDetailsService负责查找ClientDetails,而ClientDetails有几个重要的属性如下列表:

  • clientId:(必须的)用来标识客户的Id。
  • secret:(需要值得信任的客户端)客户端安全码,如果有的话。
  • scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
  • authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
  • authorities:此客户端可以使用的权限(基于Spring Security authorities)。

客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户
端详情存储在一个关系数据库的表中,就可以使用 JdbcClientDetailsService)或者通过自己实现
ClientRegistrationService接口(同时你也可以实现 ClientDetailsService 接口)来进行管理。

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private ClientDetailsService clientDetailsService;
    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;
    @Autowired
    private AuthenticationManager authenticationManager;

    //令牌访问端点
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                .authenticationManager(authenticationManager)
                .authorizationCodeServices(authorizationCodeServices)
                .tokenServices(tokenService())
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }
	//注意这个部分,到时候和请求链接的参数比对
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory() //用内存存储
                .withClient("c1")
                .secret(new BCryptPasswordEncoder().encode("secret")) //客户端密钥
                .resourceIds("res1")  //资源列表
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "refresh_token")
                .scopes("all")//允许授权范围
                .autoApprove(false)
                .redirectUris("http://www.baidu.com"); //验证回调地址
    }

    //令牌管理服务
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service = new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService); //客户端服务信息
        service.setSupportRefreshToken(true); // 是否产生刷新令牌
        service.setTokenStore(tokenStore); //令牌存储策略
        service.setAccessTokenValiditySeconds(7200);// 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200);// 刷新令牌默认有效期3天
        return service;
    }

    // 令牌访问端点安全策略
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                .tokenKeyAccess("permitAll()")  //oauth2/token_key
                .checkTokenAccess("permitAll()") //oauth/check_key
                .allowFormAuthenticationForClients(); // 表单认证 (申请令牌)
    }

    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        //设置授权码模式的授权码如何 存取,暂时采用内存方式
        return new InMemoryAuthorizationCodeServices();
    }
}

如果要用数据库就在这个UserDetailsService 编码

@Component
public class CustomUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
       Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
       //授予权限
        grantedAuthorities.add(new SimpleGrantedAuthority("p1"));
        return new User(username,
                new BCryptPasswordEncoder().encode("123"), grantedAuthorities);
    }
}
/**
 * 在config包下定义TokenConfig,
 * 我们暂时先使用InMemoryTokenStore,生成一个普通的令牌。
 */
@Configuration
public class TokenConfig {
    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }
}
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //认证管理器
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .antMatchers("/login*").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
        ;

    }
}

3.2 验证:

在这里插入图片描述
在这里插入图片描述
确认授权:
在这里插入图片描述
获取到授权码:

在这里插入图片描述

将获取到的授权码填入 对应位置:
在这里插入图片描述

3.3 客户端配置

资源定义:

@RestController
public class TestController {
    @GetMapping("r1")
    @PreAuthorize("hasAnyAuthority('USER')")
    public String r(){
        return "访问到资源1";
    }
}
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
    public static final String RESOURCE_ID = "res1";

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID).tokenServices(tokenService()).stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/**").access("#oauth2.hasScope('all')").and().csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }



    //资源服务令牌解析服务
    @Bean
    public ResourceServerTokenServices tokenService() {
        //使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
        RemoteTokenServices service = new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }
}

3.4 资源服务测试

通过密码模式获取token
在这里插入图片描述
在这里插入图片描述
按照oauth2.0协议要求,请求资源需要携带token,如下: token的参数名称为:Authorization,值为:Bearer token值

在这里插入图片描述
成功访问到!

4. 什么是JWT?

通过上边的测试我们发现,当资源服务和授权服务不在一起时资源服务使用RemoteTokenServices 远程请求授权 服务验证token,如果访问量较大将会影响系统的性能 。

解决上边问题: 令牌采用JWT格式即可解决上边的问题,用户认证通过会得到一个JWT令牌,JWT令牌中已经包括了用户相关的信 息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证 服务完成授权。

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

参见

JWT令牌的优点:
1)jwt基于json,非常方便解析。
2)可以在令牌中自定义丰富的内容,易扩展。
3)通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
4)资源服务使用JWT可不依赖认证服务即可完成授权。

缺点: 1)JWT令牌较长,占存储空间比较大。

4.1 JWT令牌结构

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

  • 头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA) 一个例子如下:
    下边是Header部分的内容
{ "alg": "HS256", 
"typ": "JWT" 
}
  • Payload
    第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。 此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。 最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。
    一个例子:
{ "sub": "1234567890", "name": "456", "admin": true }
  • Signature
    第三部分是签名,此部分用于防止jwt内容被篡改。 这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明 签名算法进行签名。 一个例子:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

base64UrlEncode(header):jwt令牌的第一部分。 base64UrlEncode(payload):jwt令牌的第二部分。 secret:签名所使用的密钥。

4.2 配置JWT令牌服务

@Configuration
public class TokenConfig {

    private String SIGNING_KEY = "uaa123";

    @Bean
    public TokenStore tokenStore() {
        //JWT令牌存储方案
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    }

//    @Bean
//    public TokenStore tokenStore() {
//        //使用内存存储令牌(普通令牌)
//        return new InMemoryTokenStore();
//    }
}
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerJWT extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private ClientDetailsService clientDetailsService;
    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    //令牌访问端点
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                .authenticationManager(authenticationManager)
                .authorizationCodeServices(authorizationCodeServices)
                .tokenServices(tokenService())
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

    //注意这个部分,到时候和请求链接的参数比对
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory() //用内存存储
                .withClient("c1")
                .secret(new BCryptPasswordEncoder().encode("secret")) //客户端密钥
                .resourceIds("res1")  //资源列表
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "refresh_token")//授权方式
                .scopes("all")//允许授权范围
                .autoApprove(false)
                .redirectUris("http://www.baidu.com"); //验证回调地址
    }



    // 令牌访问端点安全策略
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                .tokenKeyAccess("permitAll()")  //oauth2/token_key
                .checkTokenAccess("permitAll()") //oauth/check_key
                .allowFormAuthenticationForClients(); // 表单认证 (申请令牌)
    }

    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        //设置授权码模式的授权码如何 存取,暂时采用内存方式
        return new InMemoryAuthorizationCodeServices();
    }

    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service = new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService);
        service.setSupportRefreshToken(true);
        service.setTokenStore(tokenStore);
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);
        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200);// 刷新令牌默认有效期3天
        return service;
    }
}

在这里插入图片描述

check_token 一下:
在这里插入图片描述

4.3 资源服务添加jwt

ResouceServerConfig:

  @Autowired
    TokenStore tokenStore;
    
     @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
//        resources.resourceId(RESOURCE_ID).tokenServices(tokenService()).stateless(true);
        resources.resourceId(RESOURCE_ID) .tokenStore(tokenStore) .stateless(true);
    }

config添加:

 private String SIGNING_KEY = "uaa123";

    @Bean
    public TokenStore tokenStore() {
        //JWT令牌存储方案
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    }

4.4 入库

截止目前客户端信息和授权码仍然存储在内存中,生产环境中通过会存储在数据库中
在这里插入图片描述
对应:
在这里插入图片描述
SQL:

CREATE TABLE `oauth_client_details` (
 `client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '客户端标 识',
 `resource_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '接入资源列表', 
 `client_secret` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '客户端秘钥',
  `scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authorized_grant_types` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, 
  `web_server_redirect_uri` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, 
  `authorities` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `access_token_validity` int(11) NULL DEFAULT NULL, 
  `refresh_token_validity` int(11) NULL DEFAULT NULL, 
  `additional_information` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL, 
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), `archived` tinyint(4) NULL DEFAULT NULL, 
  `trusted` tinyint(4) NULL DEFAULT NULL, 
  `autoapprove` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, 
  PRIMARY KEY (`client_id`) USING BTREE
   ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '接入客户端信息' ROW_FORMAT = Dynamic;

代码添加

   @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public ClientDetailsService clientDetailsService(DataSource dataSource) {
        ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        ((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder());
        return clientDetailsService;
    }
	//注意secret是加密了的
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService);
    }

5. 实现分布式系统授权

此实现未整合数据库:

再来看一眼技术方案:
在这里插入图片描述

认证流程如下:
1、UAA认证服务负责认证授权。
2、所有请求经过网关到达微服务
3、网关负责鉴权客户端以及请求转发
4、网关将token解析后传给微服务,微服务进行授权。

5.1 注册中心

pom:

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

    </dependencies>

配置文件:

spring:
  application:
    name: distributed-discovery

server:
  port: 53000 #启动端口

eureka:
  server:
    enable-self-preservation: false    #关闭服务器自我保护,客户端心跳检测15分钟内错误达到80%服务会保护,导致别人还认为是好用的服务
    eviction-interval-timer-in-ms: 10000 #清理间隔(单位毫秒,默认是60*10005秒将客户端剔除的服务在服务注册列表中剔除#
    shouldUseReadOnlyResponseCache: true #eureka是CAP理论种基于AP策略,为了保证强一致性关闭此切换CP 默认不关闭 false关闭
  client:
    register-with-eureka: false  #false:不作为一个客户端注册到注册中心
    fetch-registry: false      #为true时,可以启动,但报异常:Cannot execute request on any known server
    instance-info-replication-interval-seconds: 10
    serviceUrl:
      defaultZone: http://localhost:${server.port}/eureka/
  instance:
    hostname: ${spring.cloud.client.ip-address}
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}

5.2 网关配置

网关整合 OAuth2.0 有两种思路,
一种是认证服务器生成jwt令牌, 所有请求统一在网关层验证,判断权限等操作;
另一种是由各资源服务处理,网关只做请求转发。

我们选用第一种。我们把API网关作为OAuth2.0的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当 前登录用户信息(jsonToken)给微服务,这样下游微服务就不需要关心令牌格式解析以及OAuth2.0相关机制了。 API网关在认证授权体系里主要负责两件事:

(1)作为OAuth2.0的资源服务器角色,实现接入方权限拦截。
(2)令牌解析并转发当前登录用户信息(明文token)给微服务

微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事:
(1)用户授权拦截(看当前用户是否有权访问该资源)
(2)将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)
在这里插入图片描述
资源服务配置:

@Configuration
public class ResouceServerConfig  {

    public static final String RESOURCE_ID = "res1";
    //uaa资源服务配置
    @Configuration
    @EnableResourceServer
    public class UAAServerConfig extends ResourceServerConfigurerAdapter {
        @Autowired
        private TokenStore tokenStore;
        @Override
        public void configure(ResourceServerSecurityConfigurer resources){
            resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
                    .stateless(true);
        }
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                 .antMatchers("/uaa/**").permitAll();
        }
    }
    //order资源
    //uaa资源服务配置
    @Configuration
    @EnableResourceServer
    public class OrderServerConfig extends ResourceServerConfigurerAdapter {
        @Autowired
        private TokenStore tokenStore;
        @Override
        public void configure(ResourceServerSecurityConfigurer resources){
            resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
                    .stateless(true);
        }
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .antMatchers("/order/**").access("#oauth2.hasScope('all')");
        }
    }


    //配置其它的资源服务..


}

网关配置类:

@Configuration
public class ZuulConfig {

    @Bean
    public AuthFilter preFileter() {
        return new AuthFilter();
    }

    @Bean
    public FilterRegistrationBean corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setMaxAge(18000L);
        source.registerCorsConfiguration("/**", config);
        CorsFilter corsFilter = new CorsFilter(source);
        FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

网关过滤器:

public class AuthFilter extends ZuulFilter {

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        //从安全上下文中拿 到用户身份对象
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if(!(authentication instanceof OAuth2Authentication)){
            return null;
        }
        OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
        Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
        //取出用户身份信息
        String principal = userAuthentication.getName();

        //取出用户权限
        List<String> authorities = new ArrayList<>();
        //从userAuthentication取出权限,放在authorities
        userAuthentication.getAuthorities().stream().forEach(c->authorities.add(((GrantedAuthority) c).getAuthority()));

        OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
        Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
        Map<String,Object> jsonToken = new HashMap<>(requestParameters);
        if(userAuthentication!=null){
            jsonToken.put("principal",principal);
            jsonToken.put("authorities",authorities);
        }

        //把身份信息和权限信息放在json中,加入http的header中,转发给微服务
        ctx.addZuulRequestHeader("json-token", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));

        return null;
    }
}

配置文件:

spring.application.name=gateway-server
server.port=53010
spring.main.allow-bean-definition-overriding = true

logging.level.root = info
logging.level.org.springframework = info

zuul.retryable = true
zuul.ignoredServices = *
zuul.add-host-header = true
zuul.sensitiveHeaders = *

zuul.routes.uaa-service.stripPrefix = false
zuul.routes.uaa-service.path = /uaa/**

zuul.routes.order-service.stripPrefix = false
zuul.routes.order-service.path = /order/**

eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/
eureka.instance.preferIpAddress = true
eureka.instance.instance-id = ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env

feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime-types[0] = text/xml
feign.compression.request.mime-types[1] = application/xml
feign.compression.request.mime-types[2] = application/json
feign.compression.request.min-request-size = 2048
feign.compression.response.enabled = true

资源服务添加过滤器,以解析网关传过来的json型令牌:

@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {


    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
            //解析出头中的token
        String token = httpServletRequest.getHeader("json-token");
        if(token!=null){
            String json = EncryptUtil.decodeUTF8StringBase64(token);
            //将token转成json对象
            JSONObject jsonObject = JSON.parseObject(json);
            //用户身份信息
            UserDTO userDTO = new UserDTO();
            String principal = jsonObject.getString("principal");
            userDTO.setUsername(principal);
//         *****   UserDTO userDTO = JSON.parseObject(jsonObject.getString("principal"), UserDTO.class);
           // 用户权限
            JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
            String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);
            //将用户信息和权限填充 到用户身份token对象中
            UsernamePasswordAuthenticationToken authenticationToken
                    = new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
            //将authenticationToken填充到安全上下文
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        filterChain.doFilter(httpServletRequest,httpServletResponse);

    }
}

5.3 验证

GateWay端口:53010
在这里插入图片描述
获取admin2的令牌,因为密码123已经写死
在这里插入图片描述
验证令牌:
在这里插入图片描述
访问资源:
在这里插入图片描述
在这里插入图片描述
成功访问到资源;

6. 扩展用户信息

目前jwt令牌存储了用户的身份信息、权限信息,网关将token明文化转发给微服务使用,目前用户身份信息仅包括 了用户的账号,微服务还需要用户的ID、手机号等重要信息。

下边分析JWT令牌中扩展用户信息的方案: 在认证阶段DaoAuthenticationProvider会调用UserDetailService查询用户的信息,这里是可以获取到齐全的用户信息的。
由于JWT令牌中用户身份信息来源于UserDetails,UserDetails中仅定义了username为用户的身份信息
这里有两个思路:第一是可以扩展UserDetails,使之包括更多的自定义属性,第二也可以扩展username的内容 ,比如存入json数据内容作为username的内容。
相比较而言,方案二比较简单还不用破坏UserDetails的结构,我 们采用方案二。

6.1 修改UserDetailService

@Override 
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 
//登录账号 
System.out.println("username="+username); 
//根据账号去数据库查询...
 UserDto user = userDao.getUserByUsername(username); 
if(user == null){ return null;
}
//查询用户权限 
List<String> permissions = 
                 userDao.findPermissionsByUserId(user.getId()); 
String[] perarray = new String[permissions.size()]; 
permissions.toArray(perarray);
 //创建userDetails 
 //这里将user转为json,将整体user存入userDetails
 // 就是将withUsername里面存入的是用户的具体信息以json的形式传过去
  String principal = JSON.toJSONString(user); UserDetails userDetails = User.withUsername(principal).password(user.getPassword())
  .authorities(perarray).build(); 
  return userDetails;
 }

6.2 修改资源服务过滤器

if (token != null){ 
//1.解析token 
String json = EncryptUtil.decodeUTF8StringBase64(token); 
JSONObject userJson = JSON.parseObject(json); 
//取出用户身份信息 
String principal = userJson.getString("principal");
 //将json转成对象 
 UserDTO userDTO = JSON.parseObject(principal, UserDTO.class); JSONArray authoritiesArray = userJson.getJSONArray("authorities"); 
 ...

以上过程就完成自定义用户身份信息的方案。

7. 问题

  • 什么是认证、授权、会话。
    认证 :用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信 息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。

    授权: 授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有 权限则拒绝访问。

    会话:用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前 用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。

  • Java Servlet为支持http会话做了哪些事儿。

  • 基于session认证机制的运作流程。

在这里插入图片描述

  • 基于token认证机制的运作流程。
    在这里插入图片描述

  • 理解Spring Security的工作原理,Spring Security结构总览,认证流程和授权,中间涉及到哪些组件,这些组件分别处理什么,如何自定义这些组件满足个性需求。
    在这里插入图片描述

  • OAuth2.0认证的四种模式?它们的大体流程是什么?
    授权码模式:
    在这里插入图片描述
    (1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息。

    client_id:客户端准入标识。
    response_type:授权码模式固定为code。
    scope:客户端权限。
    redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。
    (2)浏览器出现向授权服务器授权页面,之后将用户同意授权。
    (3)授权服务器将授权码(AuthorizationCode)转经浏览器发送给client(通过redirect_uri)。
    (4)客户端拿着授权码向授权服务器索要访问access_token
    (5)授权服务器返回令牌(access_token)
    简化模式:
    在这里插入图片描述
    (1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息。
    参数描述同授权码模式 ,注意response_type=token,说明是简化模式。
    (2)浏览器出现向授权服务器授权页面,之后将用户同意授权。
    (3)授权服务器将授权码将令牌(access_token)以Hash的形式存放在重定向uri的fargment中发送给浏览器。

密码模式:
在这里插入图片描述
(1)资源拥有者将用户名、密码发送给客户端
(2)客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token)

参数列表如下:

  • client_id:客户端准入标识。
  • client_secret:客户端秘钥。
  • grant_type:授权类型,填写password表示密码模式
  • username:资源拥有者用户名。
  • password:资源拥有者密码。

(3)授权服务器将令牌(access_token)发送给client

客户端模式:
在这里插入图片描述
(1)客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)
(2)确认客户端身份无误后,将令牌(access_token)发送给client

参数列表如下:
client_id:客户端准入标识。
client_secret:客户端秘钥。
grant_type:授权类型,填写client_credentials表示客户端模式

  • Spring cloud Security OAuth2包括哪些组件?职责?
    请看本文目录 2
  • 分布式系统认证需要解决的问题?
    1、UAA认证服务负责认证授权。
    2、所有请求经过 网关到达微服务
    3、网关负责鉴权客户端以及请求转发
    4、网关将token解析后传给微服务,微服务进行授权。

再讨论:

结合Zuul的鉴权流程

我们逐步演进系统架构设计。需要注意的是:secret是签名的关键,因此一定要保密,我们放到鉴权中心保存,其它任何服务中都不能获取secret。
没有RSA加密时
在微服务架构中,我们可以把服务的鉴权操作放到网关中,将未通过鉴权的请求直接拦截,如图:

在这里插入图片描述
1、用户请求登录
2、Zuul将请求转发到授权中心,请求授权
3、授权中心校验完成,颁发JWT凭证
4、客户端请求其它功能,携带JWT
5、Zuul将jwt交给授权中心校验,通过后放行
6、用户请求到达微服务
7、微服务将jwt交给鉴权中心,鉴权同时解析用户信息
8、鉴权中心返回用户数据给微服务
9、微服务处理请求,返回响应
发现什么问题了?

每次鉴权都需要访问鉴权中心,系统间的网络请求频率过高,效率略差,鉴权中心的压力较大。

结合RSA的鉴权

通过公钥解析jwt获取信息,减少与授权中心之间的请求次数

直接看图:
在这里插入图片描述

我们首先利用RSA生成公钥和私钥。私钥保存在授权中心,公钥保存在Zuul和各个微服务
用户请求登录
授权中心校验,通过后用私钥对JWT进行签名加密
返回jwt给用户
用户携带JWT访问
Zuul直接通过公钥解密JWT,进行验证,验证通过则放行
请求到达微服务,微服务直接用公钥解析JWT,获取用户信息,无需访问授权中心

原文链接:https://blog.csdn.net/qq_41649078/article/details/90947459

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值