网关安全(四)-重构代码以适应真实环境

1、使用scope来实现权限控制

  认证服务器,可以根据不同的应用发出不同scope的令牌,同样,资源服务器也可以通过scope来控制访问权限。

  场景:对于订单服务器,GET请求要求令牌的scope必须有read,POST请求必须有write。

  1.1、订单服务提供两个请求一个POST,一个GET

/**
 * 订单控制层
 *
 * @author caofanqi
 * @date 2020/1/31 14:26
 */
@Slf4j
@RestController
@RequestMapping("/orders")
public class OrderController {

    private RestTemplate restTemplate = new RestTemplate();

    @PostMapping
    public OrderDTO create(@RequestBody OrderDTO orderDTO, @AuthenticationPrincipal String username) {
        log.info("username is :{}",username);
        PriceDTO price = restTemplate.getForObject("http://127.0.0.1:9070/prices/" + orderDTO.getProductId(), PriceDTO.class);
        log.info("price is : {}", price.getPrice());
        return orderDTO;
    }


    @GetMapping("/{productId}")
    public OrderDTO get(@PathVariable Long productId) {
        OrderDTO orderDTO = new OrderDTO();
        orderDTO.setProductId(productId);
        return orderDTO;
    }

}

  1.2、资源服务器配置类重写public void configure(HttpSecurity http)方法。

/**
 * 资源服务器配置
 *
 * @author caofanqi
 * @date 2020/2/1 20:10
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {


    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        //该资源服务器id
        resources.resourceId("order-server");
    }


    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(HttpMethod.GET).access("#oauth2.hasScope('read')")
                .antMatchers(HttpMethod.POST).access("#oauth2.hasScope('write')")
                .anyRequest().authenticated();
    }

}

  1.3、测试访问权限

    1.3.1、申请scope为read,write的令牌,既可以访问POST请求,又可以访问GET请求。

    

    1.3.2、申请scope为read的令牌,只能访问GET请求,POST请求没有权限

    

    1.3.3、同样,申请scope为write的令牌,只能访问POST请求,GET请求没有权限(图略)。

2、通过@AuthenticationPrincipal获取用户信息

  在前面我们通过@AuthenticationPrincipal可以直接获得用户的username,现在我们想要获得整个user对象。

  2.1、WebSecurityConfig配置类中,为ResourceServerTokenServices添加token转换器

    /**
     *  远程校验令牌相关配置
     */
    @Bean
    public ResourceServerTokenServices tokenServices(){
        RemoteTokenServices tokenServices = new RemoteTokenServices();
        tokenServices.setClientId("orderService");
        tokenServices.setClientSecret("123456");
        tokenServices.setCheckTokenEndpointUrl("http://127.0.0.1:9020/oauth/check_token");
        tokenServices.setAccessTokenConverter(getAccessTokenConverter());
        return tokenServices;
    }

    private AccessTokenConverter getAccessTokenConverter() {
        DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
        DefaultUserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
        userTokenConverter.setUserDetailsService(userDetailsService);
        accessTokenConverter.setUserTokenConverter(userTokenConverter);
        return accessTokenConverter;
    }

  2.2、实现UserDetailsService接口

/**
 * 查找用户信息
 *
 * @author caofanqi
 * @date 2020/2/2 0:34
 */
@Component
public class UserDetailsServiceImpl implements UserDetailsService {


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserDO userDO = new UserDO();
        userDO.setId(1234L);
        userDO.setUsername(username);
        userDO.setPassword(new BCryptPasswordEncoder().encode("123456"));
        return userDO;
    }
}

  2.3、@AuthenticationPrincipal后直接跟loadUserByUsername方法返回的对象即可

    @PostMapping
    public OrderDTO create(@RequestBody OrderDTO orderDTO, @AuthenticationPrincipal UserDO userDO) {
        log.info("userId is :{}", userDO.getId());
        PriceDTO price = restTemplate.getForObject("http://127.0.0.1:9070/prices/" + orderDTO.getProductId(), PriceDTO.class);
        log.info("price is : {}", price.getPrice());
        return orderDTO;
    }

  2.4、测试,控制台打印结果如下

  2.5、@AuthenticationPrincipal支持Spel表达式

    @PostMapping
    public OrderDTO create(@RequestBody OrderDTO orderDTO, @AuthenticationPrincipal(expression = "#this.id") Long userId) {
        log.info("userId is :{}",userId);
        PriceDTO price = restTemplate.getForObject("http://127.0.0.1:9070/prices/" + orderDTO.getProductId(), PriceDTO.class);
        log.info("price is : {}", price.getPrice());
        return orderDTO;
    }

3、将认证服务器中的客户端信息与token存放到数据库中

  前面我们在认证服务器配置的客户端信息是直接写在代码里的,存在内存中,这样新增或删除一个客户端应用,都要改代码,重启认证服务器。token信息也都是存在内存里的,重启认证服务器后,token就没了。而且如果认证服务器是集群的话,发令牌的是A机器,验令牌的可能是B机器,这样也是不行的,需要将token持久化到数据库或者redis。我们选择将数据存放到MySQL数据库。

  3.1、创建认证服务器需要的表,其中oauth_client_details存放客户端应用信息,oauth_access_token存放发放的令牌信息

create table oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY,
  resource_ids VARCHAR(256),
  client_secret VARCHAR(256),
  scope VARCHAR(256),
  authorized_grant_types VARCHAR(256),
  web_server_redirect_uri VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(256)
);

create table oauth_client_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256)
);

create table oauth_access_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication BLOB,
  refresh_token VARCHAR(256)
);

create table oauth_refresh_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication BLOB
);

create table oauth_code (
  code VARCHAR(256), authentication BLOB
);

create table oauth_approvals (
    userId VARCHAR(256),
    clientId VARCHAR(256),
    scope VARCHAR(256),
    status VARCHAR(10),
    expiresAt DATETIME,
    lastModifiedAt DATETIME
);

  3.2、将客户端相关信息存放到oauth_client_details表中

  3.3、pom中添加数据库连接等依赖,application.yml中添加数据库连接信息

  3.4、修改认证服务器配置类

/**
 * OAuth2认证服务器配置类
 * 需要继承AuthorizationServerConfigurerAdapter类,覆盖里面三个configure方法
 * 并添加@EnableAuthorizationServer注解,指定当前应用做为认证服务器
 *
 * @author caofanqi
 * @date 2020/1/31 18:04
 */
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {


    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private DataSource dataSource;

    /**
     * 配置授权服务器的安全性
     * checkTokenAccess:验证令牌需要什么条件,isAuthenticated():需要经过身份认证。
     * 此处的passwordEncoders是为client secrets配置的。
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.checkTokenAccess("isAuthenticated()").passwordEncoder(new BCryptPasswordEncoder());
    }


    /**
     * 配置客户端服务
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //从数据库中读取
        clients.jdbc(dataSource);
    }

    /**
     * 配置授权服务器终端的非安全特征
     * authenticationManager 校验用户信息是否合法
     * tokenStore:token存储
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager).tokenStore(new JdbcTokenStore(dataSource));
    }

}

  3.5、启动项目,请求令牌,可以发现oauth_access_token中多了一条记录

   使用该令牌去访问资源服务器,可以正常访问,重启 认证服务器依然生效。

项目源码:https://github.com/caofanqi/study-security/tree/dev-Refactoring

原文地址:https://www.cnblogs.com/caofanqi/p/12251591.html