分布式系统架构越来越多地应用于日常开发中。而分布式系统之间的认证和鉴权同时也是作为开发者的我们需要去考虑的。

话不多说,直接上干货。

🚀 源码地址:https://github.com/NekoChips/SpringDemo/17.springcloud-zuul-oauth2

1. 项目结构

👀 项目背景:基于 SpringBoot 2.1.9.RElEASESpringCloud Greenwich.SR3 搭建,使用 SpringCloud Zuul 作为服务网关,使用 nacos 作为服务的注册和发现中心。

流程图大致如下:

sequenceDiagram
    Client->>Gateway: 不带 access_token 请求资源
    Gateway-->>Client: 不存在 acess_token,返回 401 Unauthenticated
    Client->>AuthServer: 用户登录并申请 access_token 
    AuthServer->>AuthServer: 用户认证,认证成功后生成 acess_token
    AuthServer-->>Client: 返回 access_token 至客户端
    Client->>Gateway: 携带 access_token 请求资源
    Gateway->>Registry: 携带 access_token 访问下游资源
    Registry->>ResourceServer: 路由至请求对应的 ResourceServer
    ResourceServer->>ResourceServer: 校验 access_token, 并进行鉴权 
    ResourceServer-->>Client: 若无权访问,返回 403 UnAuthorized
    ResourceServer-->>Client: 若有权访问,返回 200 Success

项目大致结构如下:

├── springcloud-zuul-oauth2
	├── auth-server		## 认证服务
	├── zuul-server		## 网关
	├── user-service	## 资源服务
	├── ......		## 其他资源服务
	└── pom.xml

具体的项目构建这里不做赘述,相关依赖请参考源码

2. 服务配置

2.1. 认证服务

认证服务模块 auth-server 基于 Spring Security Oauth2 ,使用 JWT 传递用户信息,避免跨域问题。

认证服务器配置

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private UserDetailsService userDetailsService;

    @Resource
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Resource
    private TokenStore jwtTokenStore;

    @Resource
    private JwtTokenEnhancer jwtTokenEnhancer;

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

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(jdbcClientDetailsService());
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> enhancers = new ArrayList<>();
        enhancers.add(jwtTokenEnhancer);
        enhancers.add(jwtAccessTokenConverter);
        enhancerChain.setTokenEnhancers(enhancers);

        endpoints
                // 配置 password 方式获取 token 令牌,则需要添加认证管理器以及用户查询接口
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                // 使用 jwtToken 方式管理 token
                .tokenStore(jwtTokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(enhancerChain);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
		// 认证后才能访问 /oauth/token_key 
                .tokenKeyAccess("isAuthenticated()")
		// 认证后才能校验 /oauth/check_token
                .checkTokenAccess("isAuthenticated()");
    }
}

application.yml 文件配置

server:
  address: localhost
  port: 8100

spring:
  application:
    name: auth-server
  cloud:
    nacos:
      discovery:
        ip: ${server.address}
        port: ${server.port}
        server-addr: localhost:8848

2.2. 资源服务

项目中的需要授权才能访问的资源模块均可作为资源服务器存在,所有的认证授权均交给认证服务来处理。

applicaton.yml 文件中配置 token 校验

server:
  address: localhost
  port: 9001

spring:
  application:
    name: user-service

  cloud:
    nacos:
      discovery:
        ip: ${server.address}
        port: ${server.port}
        server-addr: localhost:8848

## security oauth2 配置
security:
  oauth2:
    client:
      client-id: app-user
      client-secret: user_123456
    ## 配置资源服务
    resource:
      ## 指定认证服务器 /oauth/check_token 用于检验 token 合法性
      token-info-uri: http://localhost:8100/oauth/check_token
      jwt:
        ## 指定认证服务器 /oauth/token_key 用于获取 token的加密密钥
        key-uri: http://localhost:8100/oauth/token_key
  • /oauth/check_token : 用于校验请求中的 access_token 是否有效。
  • /oauth/token_key : 用于获取 access_token 的加密密钥,这里我认证服务器中使用 RSA 非对称加密方式对 JWT 进行加密。

添加 @EnableResourceServer 注解指定模块为资源服务器

@SpringBootApplication
@EnableDiscoveryClient
@EnableResourceServer
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }
}

关闭 SpringSecurity 默认的表单认证

@Order(101)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().permitAll()
                .and().csrf().disable();
    }
}
  • Order 注解:认证服务器中配置了 Security 且默认的 Order100 ,这里保证在认证服务器的配置后配置即可。

2.3. Zuul 网关服务

这里的 Zuul 只处理请求路由,自定义一个 ZuulFilter 用于校验请求是否合法,并将 access_token 传递至下游服务。

application.yml 文件配置

server:
  address: localhost
  port: 8000

spring:
  application:
    name: zuul-server
  cloud:
    nacos:
      discovery:
        ip: ${server.address}
        port: ${server.port}
        server-addr: localhost:8848

zuul:
  routes:
    auth-server:
      path: /auth/**
      sensitiveHeaders:
      serviceId: auth-server

    user-service:
      path: /user/**
      sensitiveHeaders:
      serviceId: user-service

    order-service:
      path: /order/**
      sensitiveHeaders:
      serviceId: order-service
  host:
    connect-timeout-millis: 5000
    socket-timeout-millis: 10000

自定义 AuthenticationZuulFilter 继承 ZuulFilter

public class AuthenticationZuulFilter extends ZuulFilter {

    private Logger logger = LoggerFactory.getLogger(AuthenticationZuulFilter.class);

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

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

    @Override
    public boolean shouldFilter() {
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        String uri = request.getRequestURI();
        if (logger.isDebugEnabled()) {
            logger.debug("request uri: {}", uri);
        }

        return !StringUtils.equals(uri, "/auth/login");
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        // 判断请求头部中是否包含token信息
        String header = request.getHeader("Authorization");
        if (StringUtils.isEmpty(header)) {
            // 请求头中没有,判断请求参数是否存在token信息
            header = request.getParameter("Authorization");
            if (StringUtils.isEmpty(header)) {
                // 请求中不包含token信息,提示登录
                currentContext.setResponseStatusCode(HttpServletResponse.SC_UNAUTHORIZED);
                currentContext.setResponseBody("please login first!");
            }
        }
        // 向下游服务器传递token
        currentContext.addZuulRequestHeader("Authorization", header);

        return null;
    }

}

🚀 ZuulFilter 在这里简单说一下。详细的可以看一下 ZuulFilter 的源码

  • filterType: 指定过滤器的类型,ZuulFilter 有四种默认的过滤类型 PRE、ROUTING、POST、ERROR 以及自定义过滤类型 STATIC
  • filterOrder: 指定过滤器的执行顺序,不同的过滤器允许返回相同的值。
  • shouldFilter: 指定过滤器执行的条件。
  • run:过滤器的具体逻辑。

上述过滤器在对不是登录的所有请求进行过滤,过滤器检查请求是否携带 Authorization 参数(即是否携带 token 信息),对不包含 token 的请求返回 401,对包含 token 的请求将其 token 信息传递至下游服务。

在启动类中注册自定义过滤器使其生效

@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }

    @Bean
    public AuthenticationZuulFilter authenticationZuulFilter() {
        return new AuthenticationZuulFilter();
    }
}

3. 开启权限控制

在资源服务器的 SecurityConfig 上添加 @EnableGlobalMethodSecurity 注解开启权限注解。

@Order(101)
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().permitAll()
                .and().csrf().disable();
    }
}

在接口上添加权限控制

@RestController
@RequestMapping("test")
public class TestController {

    @GetMapping("info")
//    @PreAuthorize("hasAuthority('user:hello')")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public String test() {
        return "Hello user";
    }

    @GetMapping("auth")
    public Authentication authentication(Authentication authentication) {
        return authentication;
    }
}

4. 总结

使用网关统一鉴权的核心就在于将认证服务器生成的 acess_token 传递给下游服务,下游服务通过 access_token 对请求是否能访问对应资源进行检查。

这里还是有很多东西不是很清楚,再者 SpringCloud 已经不打算对 Zuul 进行维护,甚至官网上已经没有了 Zuul 的身影。后面还要考虑使用 SpringCloud Gateway 实现统一鉴权。

🚀 源码地址:https://github.com/NekoChips/SpringDemo/17.springcloud-zuul-oauth2

🚀 相关链接:


关于作者:NekoChips
本文地址:https://chenyangjie.com.cn/articles/2020/03/22/1584852458180.html
版权声明:本篇所有文章仅用于学习和技术交流,本作品采用 BY-NC-SA 4.0 许可协议,如需转载请注明出处!
许可协议:知识共享许可协议