分布式Security-OAuth2结合网关

1,582 阅读3分钟

在这里插入图片描述

下面我们来讲下分布式 Security的授权 流程,只是蜻蜓点水,大佬勿喷!

一、需求分析

  1. UUA认证服务负责认证授权
  2. 所有请求经过网关到达微服务
  3. 网关负责鉴权客户端以及请求的转发
  4. 网关将 token解析后传给微服务,微服务进行授权

二、服务搭建

代码和环境搭建忽略,详情见:github.com/hucheng1997…

2.1 注册中心 Eureka 搭建

参考 https://hucheng.blog.csdn.net/article/details/105547229

2.2 网关 Zuul 搭建

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

我们选用第一种,我们把 API网关作为 OAuth2的资源服务器角色,实现接入客户端权限拦截,令牌解析并转发当前登录用户信息(jsonToken)给微服务,这样下游微服务就需要关心令牌格式解析以及 OAuth2的相关机制。

API网关在认证授权体系里主要负责两件事:

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

微服务拿到明文 token(包含登录用户的身份和权限信息)后也需要做两件事:

  1. 用户授权拦截(看当前用户是否有权访问该资源)
  2. 将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)

Zuul中定义资源服务配置,主要配置的内容就是定义一些匹配规则,描述某个接入客户端需要什么样的权限才能访问某个微服务

@Configuration
public class ResourceServerConfig {

    public static final String RESOURCE_ID = "res1";

    @Configuration
    @EnableResourceServer
    public class UUAServerConfig 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("/uua/**").permitAll();
        }
    }

    @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("/resource/**").access("#oauth2.hasScope('ALL')");
        }
    }
}

2.2.1 转发明文token给微服务

通过 Zuul过滤器的方式实现,目的是让下游微服务能够很方便的获取到当前的登录用户信息(明文 token

①实现Zuul前置过滤器,完成当前登录用户信息提取,并放入转发微服务的request中

**
 * token传递拦截
 */
public class AuthFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "pre";
    }

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

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

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

        List<String> authorities = new ArrayList<>();
        userAuthentication.getAuthorities().stream().forEach(s -> authorities.add(((GrantedAuthority) s).getAuthority()));
        OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
        Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
        Map<String,Object> jsonToken = new HashMap<>(requestParameters);
        if (userAuthentication!=null){
            jsonToken.put("principal",userAuthentication.getName());
            jsonToken.put("authorities",authorities);
        }

        ctx.addZuulRequestHeader("json-token", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));
        return null;
    }
}

②将filter纳入spring 容器

@Configuration
public class ZuulConfig {

    @Bean
    public AuthFilter preFilter() {
        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;
    }
}

2.2.2 微服务处理 token

微服务定义filter拦截token,并形成Spring Security的Authentication对象

@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("json-token");
        if (token != null) {
            String json = EncryptUtil.decodeUTF8StringBase64(token);

            JSONObject jsonObject = JSON.parseObject(json);
            UserDTO userDTO = JSON.parseObject(jsonObject.getString("principal"), UserDTO.class);

            JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
            String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);

            UsernamePasswordAuthenticationToken authenticationToken
                    = new UsernamePasswordAuthenticationToken(userDTO, null, AuthorityUtils.createAuthorityList(authorities));
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        filterChain.doFilter(request, response);
    }
}

2.2.3 SpringSecurity 自定义 UserDetailsService

@Service
public class SpringDataUserDetailsService implements UserDetailsService {

    @Autowired
    UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        UserDto userDto = userDao.getUserByUsername(username);
        if(userDto == null){

            return null;
        }

        List<String> permissions = userDao.findPermissionsByUserId(userDto.getId());

        String[] permissionArray = new String[permissions.size()];
        permissions.toArray(permissionArray);

        String principal = JSON.toJSONString(userDto);
        UserDetails userDetails = User.withUsername(principal).password(userDto.getPassword()).authorities(permissionArray).build();
        return userDetails;
    }
}