SpringCloud之微服务安全解决方案(1)

微服务安全如何保障?

很多人都会问微服务如何保证安全?我们知道一个系统即使做得再好,可能也会出现一些意料之外的Bug,安全也是一样的,防不胜防,但是如果我们能够尽量避免一些低级的错误或者本身代码的问题,那么我们可以将系统的风险降到最低。
随着这几年微服务的崛起,可以说很多技术的交流、讨论、实施都离不开微服务,微服务解决了传统项目中的很多问题,我想小伙伴们一定非常熟悉微服务开发的流程,那么这里我就不用很多文字来阐述,不清楚的可以看我的SpringCloud其它的文章,这里我就不多说了。
首先微服务的切入点那么大家能够想到的就是用户的登陆认证,那么在微服务中我们肯定有一个用户模块,这个用户模块负责登陆,那么登陆的话会考虑很多因素,比如传统的Session或者是比较流行的Token认证(颁发令牌)来完成认证,我们知道Session容易伪造和篡改,不安全,Token经常用在前后端分离项目中,前端一般会发起一个获取Token的一个请求,得到Token后,携带上这串Token去访问相关的服务,当然还有的就是直接请求登陆接口然后用户名和密码正确的情况下,直接将用户信息和Token返回(这种是不安全的),要知道我们不能把token暴露给前端,不能让前端使用字符串来拼接Token,我们尽量由后端来一气呵成,所谓的一气呵成指的是Token由后端来拼接或者通过网关来转发,而不是直接返回,这样能够降低被拦截的风险,从而保证系统的安全。
当然我认为系统没有绝对的安全,即使是BAT公司的项目也会存在安全的问题,但是Spring官方给我们提供了安全的解决方案,可以在spring.io的官网上的project栏目中找到Spring Security Oauth的栏目,如下图所示
在这里插入图片描述如果没有Oauth相关的经验的话,我建议去看一下阮一峰老师Oauth2
讲的特别好,这里我就不用太多篇幅来讲一些Oauth的基础,下面我们就开始今天这个话题的目的,让大家学会如何在微服务中如何保证系统的安全。

项目结构

首先这个是基于最新版本的SpringBoot以及GSR2的Cloud版本,这个项目没有使用到注册中心,因为注册中心由很多,eureka、zookeeper等,不统一,所以这里不用注册中心,为了大家能够看清项目结构我能截图项目结构给大家看看

在这里插入图片描述
首先login-api是一个用户登陆的一个模块

login-template是一个前端的一个客户端,由vue编写

order-api 是一个模拟订单的微服务

server-auth 这是授权服务器

server-gateway-zuul 是微服务网关

首先看项目的名称大家应该也知道这里会使用Oauth2的密码模式,这种模式适合App项目,这里不做太多解释,详情可以看阮一峰老师的Oauth2精讲,这个多级项目的重点是在server-auth这个项目中,我们可以看下这个项目的结构还是蛮简单清爽的

在这里插入图片描述

首先来讲解一下UserDetailsServiceImpl这个类

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return User.withUsername(username)
                .password(passwordEncoder.encode("123456"))
                .authorities("ROLE_ADMIN")
                .build();
    }

}

这串代码非常的简单,这个类直接实现了Spring Security的UserDetailsService这个接口,而这个接口就只有一个方法loadUserByUsername,这个方法就是根据什么条件去查询用户信息,默认是username就是根据用户名查询,我这个方法呢比较简单,没有从数据库去查询用户的信息,只要密码是123456我就认为它是正确的,主要是让业务简化,在实际项目中,这里你需要注入mapper或者repository从数据库中查询,这里只是为了演示方便以及让大家能够简单的串起来,这里使用了Spring Security 官方提供的密码加密器PasswordEncoder,关于这个加密器大家可以去看官方文档,这个加密器非常的强大可以说是在Spring 中最强大的加密器。

我们继续来看一下Oauth2AuthServerConfig的代码

@Configuration
@EnableAuthorizationServer
public class Oauth2AuthServerConfig  extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private DataSource dataSource;

    @Bean
    public TokenStore tokenStore () {
        return new JdbcTokenStore(dataSource);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .userDetailsService(userDetailsService)
                .tokenStore(tokenStore())
                .authenticationManager(authenticationManager);
    }


    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.checkTokenAccess("isAuthenticated()");
    }
}

我们来解析一下代码,第一个注解就不解释了,直接看第二个注解,@EnableAuthorizationServer 这个注解是告诉Spring 我是一个授权服务器,这里使用了原生的JdbcTokenStore作为数据源,这个表结构可以从JdbcTokenStore这个源码中获取

 private static final Log LOG = LogFactory.getLog(JdbcTokenStore.class);
    private static final String DEFAULT_ACCESS_TOKEN_INSERT_STATEMENT = "insert into oauth_access_token (token_id, token, authentication_id, user_name, client_id, authentication, refresh_token) values (?, ?, ?, ?, ?, ?, ?)";
    private static final String DEFAULT_ACCESS_TOKEN_SELECT_STATEMENT = "select token_id, token from oauth_access_token where token_id = ?";
    private static final String DEFAULT_ACCESS_TOKEN_AUTHENTICATION_SELECT_STATEMENT = "select token_id, authentication from oauth_access_token where token_id = ?";
    private static final String DEFAULT_ACCESS_TOKEN_FROM_AUTHENTICATION_SELECT_STATEMENT = "select token_id, token from oauth_access_token where authentication_id = ?";
    private static final String DEFAULT_ACCESS_TOKENS_FROM_USERNAME_AND_CLIENT_SELECT_STATEMENT = "select token_id, token from oauth_access_token where user_name = ? and client_id = ?";
    private static final String DEFAULT_ACCESS_TOKENS_FROM_USERNAME_SELECT_STATEMENT = "select token_id, token from oauth_access_token where user_name = ?";
    private static final String DEFAULT_ACCESS_TOKENS_FROM_CLIENTID_SELECT_STATEMENT = "select token_id, token from oauth_access_token where client_id = ?";
    private static final String DEFAULT_ACCESS_TOKEN_DELETE_STATEMENT = "delete from oauth_access_token where token_id = ?";
    private static final String DEFAULT_ACCESS_TOKEN_DELETE_FROM_REFRESH_TOKEN_STATEMENT = "delete from oauth_access_token where refresh_token = ?";
    private static final String DEFAULT_REFRESH_TOKEN_INSERT_STATEMENT = "insert into oauth_refresh_token (token_id, token, authentication) values (?, ?, ?)";
    private static final String DEFAULT_REFRESH_TOKEN_SELECT_STATEMENT = "select token_id, token from oauth_refresh_token where token_id = ?";
    private static final String DEFAULT_REFRESH_TOKEN_AUTHENTICATION_SELECT_STATEMENT = "select token_id, authentication from oauth_refresh_token where token_id = ?";
    private static final String DEFAULT_REFRESH_TOKEN_DELETE_STATEMENT = "delete from oauth_refresh_token where token_id = ?";
    private String insertAccessTokenSql = "insert into oauth_access_token (token_id, token, authentication_id, user_name, client_id, authentication, refresh_token) values (?, ?, ?, ?, ?, ?, ?)";
    private String selectAccessTokenSql = "select token_id, token from oauth_access_token where token_id = ?";
    private String selectAccessTokenAuthenticationSql = "select token_id, authentication from oauth_access_token where token_id = ?";
    private String selectAccessTokenFromAuthenticationSql = "select token_id, token from oauth_access_token where authentication_id = ?";
    private String selectAccessTokensFromUserNameAndClientIdSql = "select token_id, token from oauth_access_token where user_name = ? and client_id = ?";
    private String selectAccessTokensFromUserNameSql = "select token_id, token from oauth_access_token where user_name = ?";
    private String selectAccessTokensFromClientIdSql = "select token_id, token from oauth_access_token where client_id = ?";
    private String deleteAccessTokenSql = "delete from oauth_access_token where token_id = ?";
    private String insertRefreshTokenSql = "insert into oauth_refresh_token (token_id, token, authentication) values (?, ?, ?)";
    private String selectRefreshTokenSql = "select token_id, token from oauth_refresh_token where token_id = ?";
    private String selectRefreshTokenAuthenticationSql = "select token_id, authentication from oauth_refresh_token where token_id = ?";
    private String deleteRefreshTokenSql = "delete from oauth_refresh_token where token_id = ?";
    private String deleteAccessTokenFromRefreshTokenSql = "delete from oauth_access_token where refresh_token = ?";
    private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
    private final JdbcTemplate jdbcTemplate;

这里我就不解释这些参数了,相信大家看sql就明白了,这个sql文件我放在了resouce目录中,大家copy然后执行即可。

我们的这个类集成的是AuthorizationServerConfigurerAdapter这个适配器,这里呢我们实现了这三个configure方法其中ClientDetailsServiceConfigurer从那里获取clientid呢?当然这里是从数据库中获取,所以这里使用的是jdbc模式,将我们的dataSource注入进去。

AuthorizationServerEndpointsConfigurer这个是授权服务Endpoints配置类,说简单一点就是代理配置类,这个配置类里面我们端点配置了两项,分别是tokenstroe、authenticationManager(凭证信息管理)

AuthorizationServerSecurityConfigurer这个类呢是授权服务访问授权配置类,它也是一个代理配置类,置客户端信息:从Spring容器中加载所有AuthorizationServerConfigurer配置类来完成ClientDetailsServiceConfigurer的配置

其中checkTokenAccess是如何检查token的有效,这里使用的是表达式isAuthenticated() 意思是开启/oauth/check_token验证端口认证权限访问

那么关于这个server-auth的重要的类我就讲完了,下面我们来看一下网关 server-gateway-zuul这个项目

在这里插入图片描述

这个项目里面只关心AuthorizationFilter和OAuthFilter这两个类

我们来看一下AuthorizationFilter的代码

Slf4j
@Component
public class AuthorizationFilter extends ZuulFilter {

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

    @Override
    public Object run() throws ZuulException {

        log.info("authorization start");

        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        if (isNeedAuth(request)) {

            TokenInfo tokenInfo = (TokenInfo) request.getAttribute("tokenInfo");

            if (tokenInfo != null && tokenInfo.isActive()) {
                if (!hasPermission(tokenInfo, request)) {
                    log.info("audit log update fail 403");
                    handleError(403, requestContext);
                }

                requestContext.addZuulRequestHeader("username", tokenInfo.getUser_name());
            } else {
                if (!StringUtils.startsWith(request.getRequestURI(), "/token")) {
                    log.info("audit log update fail 401");
                    handleError(401, requestContext);
                }
            }
        }

        return null;
    }

    private void handleError(int status, RequestContext requestContext) {
        requestContext.getResponse().setContentType("application/json");
        requestContext.setResponseStatusCode(status);
        requestContext.setResponseBody("{\"message\":\"auth fail\"}");
        requestContext.setSendZuulResponse(false);
    }

    private boolean hasPermission(TokenInfo tokenInfo, HttpServletRequest request) {
        return true; //RandomUtils.nextInt() % 2 == 0;
    }

    private boolean isNeedAuth(HttpServletRequest request) {
        return true;
    }


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

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

这个类实现类ZuulFilter这个过滤器,这个过滤器是网关提供的,这里过滤器的类型我使用的是pre前置过滤器,其重点run这个方法里面,因为run方法是我们逻辑的实现,其中由于我们的run方法本身是个没有带任何参数的方法,我们只能通过RequestContext获取上下文的HttpServletRequest对象,从里面获取我们需要的信息,这里看代码可以知道我从从request域对象里面获取tokeninfo的信息,我们来看一下TokenInfo这个类的代码

@Data
public class TokenInfo {
    private boolean active; //是否有效
    private String client_id;//客户端id
    private String [] scope;//权限
    private String user_name; //用户名
    private String[] aud;//从那些资源服务访问
    private Date exp;//过期时间
    private String [] authorities;//角色集
}

这里核心的认证就是下面这串代码

   if (isNeedAuth(request)) {

            TokenInfo tokenInfo = (TokenInfo) request.getAttribute("tokenInfo");

            if (tokenInfo != null && tokenInfo.isActive()) {
                if (!hasPermission(tokenInfo, request)) {
                    log.info("audit log update fail 403");
                    handleError(403, requestContext);
                }

                requestContext.addZuulRequestHeader("username", tokenInfo.getUser_name());
            } else {
                if (!StringUtils.startsWith(request.getRequestURI(), "/token")) {
                    log.info("audit log update fail 401");
                    handleError(401, requestContext);
                }
            }
        }

        return null;

这里每次都会进入因为isNeedAuth永远返回的是true,每次进来之后,会判断从request域对象里面获取的tokeninfo信息是否为null并且是否在有效期,如果都满足那么就会进入hasPermission进行权限判断,这个hasPermission方法也是永远返回true,永远是有效的,如果都满足那么网关就会放行,并且添加Header头。

这个就是基本的网关认证Filter过滤器,下面我们再来看一下OAuthFilter,这个过滤器负责的是发起Oauth请求并且拿到AccessToken,我们来看一下代码

@Slf4j
@Component
public class OAuthFilter extends ZuulFilter {

    private RestTemplate restTemplate = new RestTemplate();

    public boolean shouldFilter() {
        return true;
    }

    public Object run() throws ZuulException {

        log.info("oauth start");

        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        if(StringUtils.startsWith(request.getRequestURI(), "/token")) {
            return null;
        }

        String authHeader = request.getHeader("Authorization");

        if(StringUtils.isBlank(authHeader)) {
            return null;
        }

        if(!StringUtils.startsWithIgnoreCase(authHeader, "bearer ")) {
            return null;
        }

        try {

            TokenInfo info = getTokenInfo(authHeader);
            request.setAttribute("tokenInfo", info);

        } catch (Exception e) {
            log.error("get token info fail", e);
        }

        return null;
    }

    private TokenInfo getTokenInfo(String authHeader) {

        String token = StringUtils.substringAfter(authHeader, "bearer ");
        String oauthServiceUrl = "http://localhost:9090/oauth/check_token";

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.setBasicAuth("gateway", "123456");

        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("token", token);

        HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers);

        ResponseEntity<TokenInfo> response = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, TokenInfo.class);

        log.info("token info :" + response.getBody().toString());

        return response.getBody();
    }

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

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

这个代码也很简单,其中最重要的也是run方法和getTokenInfo这个方法,
run方法里面的代码很简单,这里我主要讲一下getTokenInfo 方法,这方法目的是主要是发起oauth的认证和有效性检查,这里注意使用的是HttpHeaders这个类来封装我们需要传递的一些头部信息,MultiValueMap是键值对,我们将封装好的header头呢添加到MultiValueMap里面,最后通过restTemplate这个Spring提供的http请求工具发起http请求,最后完成相应得到相应的信息。

关于order-api这个项目很简单,里面的代码非常少,可以自己去看

我们看一下login-api这个登陆的项目

在这里插入图片描述

首先我们看这个项目的控制器的login方法

      @PostMapping("/login")
    public void login(@RequestBody Auth auth, HttpServletRequest request, HttpSession session) {
        String oauthServiceUrl = "http://gateway.ityoudream.com:9070/token/oauth/token"; //请求的uri
        HttpHeaders headers = new HttpHeaders();//组装header头
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.setBasicAuth("auth","123456");//从数据库查询

        MultiValueMap<String,String> params = new LinkedMultiValueMap<String, String>();//组装最后发送的参数
        params.add("username", auth.getUsername());
        params.add("password", auth.getPassword());
        params.add("grant_type", "password");
        params.add("scope", "read write");

        log.info("params:" + params);
        HttpEntity< MultiValueMap<String,String>> entity = new HttpEntity<MultiValueMap<String,String>>(params,headers);
        ResponseEntity<TokenInfo> response = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, TokenInfo.class);
        session.setAttribute("token", response.getBody());//认证成功后就保存token
        log.info("sessionId1:" + session.getId());
    }

下面我们来看一下SessionTokenFilter这个类

@Component
@Slf4j
public class SessionTokenFilter extends ZuulFilter {
   public String filterType() {
       return "pre";
   }

   public int filterOrder() {
       return 0;
   }

   public boolean shouldFilter() {
       return true;
   }

   @Override
   public Object run() throws ZuulException {
       RequestContext requestContext = RequestContext.getCurrentContext(); //获取上下文
       HttpServletRequest request = requestContext.getRequest();
       HttpSession session = request.getSession();//得到session
       TokenInfo token = (TokenInfo)session.getAttribute("token");//从session获取token
       log.info("sessionId2:" + session.getId());
       log.info("token:" + token);
       if (token != null) {
           //添加header给zuul
           requestContext.addZuulRequestHeader("Authorization", "bearer "+token.getAccess_token());
       }
       return null;
   }
}

这个类就是主要是从session获取这个token信息,然后添加到zuul里面。

最后就是前端的login-template这个是vue编写的,但是代码相当简单,我写的比较简单。

最后我们来看一下总体的效果

在这里插入图片描述

总结

在微服务中保证安全其主要方向在于后端,在后端中应该尽量避免前端来传递Token信息,这样可以降低风险,同时保证系统的稳定性和效率,本案例使用的是Session和Token的混合方式,这种方式只适合小中型项目,用户数量在80w以下,如果高于80w那么本方案就不适合了,可能要考虑jwt的方式了,下面一节将讲如何使用授权码模式以及session的有效期和token有效期。

源码地址

github

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值